货物

了解如何使用 PHPUnit 和 PEST 通过简单示例在 Laravel 中进行测试

当谈到自动化测试或单元测试时,在任何编程语言中,都存在两种相反的观点:

  • 浪费时间
  • 你不能没有它

因此,通过本文,我们将尝试说服前者,特别是通过演示在 Laravel 中开始自动化测试是多么容易。

首先让我们谈谈“为什么”,然后让我们看一些如何做的例子。

为什么我们需要自动化测试

自动化测试运行部分代码并报告任何错误。 这是描述它们的最简单的方式。 想象一下,在应用程序中推出一项新功能,然后个人机器人助手会手动测试新功能,同时还测试新代码是否不会破坏任何旧功能。

这是主要优点:自动重新测试所有功能。 这似乎是额外的工作,但如果你不告诉“机器人”去做,我们应该手动去做,对吧? 

或者可以在不测试新功能是否有效的情况下发布新功能,希望用户报告错误。

自动化测试可以给我们带来几个优势:

  • 节省人工测试时间;
  • 它们使您可以通过避免回归来节省实施新功能和合并功能的时间;
  • 将这一优势乘以所有新功能和所有已实现的功能;
  • 前三点适用于每个新版本;
  • ...

尝试想象一两年后您的应用程序,团队中的新开发人员不知道前几年编写的代码,甚至不知道如何测试它。 

我们的第一个自动化测试

执行第一个 Laravel 中的自动化测试,您不需要编写任何代码。 是的,你没有看错。 一切都已在预安装中配置和准备defiLaravel 的一晚,包括第一个基本示例。

您可以尝试安装 Laravel 项目并立即运行第一个测试:

laravel new project
cd project
php artisan test

这应该是控制台中的结果:

如果我们看一下预defiLaravel 之夜 /tests,我们有两个文件:

测试/功能/ExampleTest.php:

class ExampleTest extends TestCase
{
    public function test_the_application_returns_a_successful_response()
    {
        $response = $this->get('/');
 
        $response->assertStatus(200);
    }
}

您无需了解任何语法即可了解此处发生的情况:加载主页并检查状态代码是否 HTTP 是“200 OK“。

也称为方法名称 test_the_application_returns_a_successful_response() 当您查看测试结果时,只需将下划线符号替换为空格即可变为可读文本。

测试/Unit/ExampleTest.php:

class ExampleTest extends TestCase
{
    public function test_that_true_is_true()
    {
        $this->assertTrue(true);
    }
}

似乎有点无意义,看看这是不是真的? 

稍后我们将具体讨论单元测试。 现在,您需要了解每个测试中通常会发生什么。

  • 文件夹中的每个测试文件 /tests 是一个 PHP 类,扩展了 TestCase PHPUnit
  • 在每个类中,您可以创建多个方法,通常一个方法用于测试一种情况
  • 每种方法都包含三个行动:准备情况,然后采取行动,然后验证(确认)结果是否符合预期

从结构上讲,这就是您需要知道的全部内容,其他一切都取决于您想要测试的具体内容。

要生成空测试类,只需运行以下命令:

php artisan make:test HomepageTest

文件已生成 tests/Feature/HomepageTest.php:

class HomepageTest extends TestCase
{
    // Replace this method with your own ones
    public function test_example()
    {
        $response = $this->get('/');
 
        $response->assertStatus(200);
    }
}

现在让我们看看如果 Laravel 中的测试代码失败会发生什么

现在让我们看看如果测试断言没有返回预期结果会发生什么。

让我们将示例测试更改为:

class ExampleTest extends TestCase
{
    public function test_the_application_returns_a_successful_response()
    {
        $response = $this->get('/non-existing-url');
 
        $response->assertStatus(200);
    }
}
 
 
class ExampleTest extends TestCase
{
    public function test_that_true_is_false()
    {
        $this->assertTrue(false);
    }
}

现在,如果我们运行命令 php artisan test 再次:

 FAIL  Tests\Unit\ExampleTest
⨯ that true is true
 
 FAIL  Tests\Feature\ExampleTest
⨯ the application returns a successful response
 
---
 
• Tests\Unit\ExampleTest > that true is true
Failed asserting that false is true.
 
at tests/Unit/ExampleTest.php:16
   12▕      * @return void
   13▕      */
   14▕     public function test_that_true_is_true()
   15▕     {
➜  16▕         $this->assertTrue(false);
   17▕     }
   18▕ }
   19▕
 
• Tests\Feature\ExampleTest > the application returns a successful response
Expected response status code [200] but received 404.
Failed asserting that 200 is identical to 404.
 
at tests/Feature/ExampleTest.php:19
   15▕     public function test_the_application_returns_a_successful_response()
   16▕     {
   17▕         $response = $this->get('/non-existing-url');
   18▕
➜  19▕         $response->assertStatus(200);
   20▕     }
   21▕ }
   22▕
 
 
Tests:  2 failed
Time:   0.11s

有两个失败的测试,标记为 FAIL,解释如下,箭头指向失败的测试的确切行。 错误以这种方式指示。

示例:在 Laravel 中测试注册表单代码

假设我们有一个表单,我们需要测试各种情况:我们检查它是否因无效数据而失败,我们检查它是否因正确输入而成功,等等。

官方入门套件 通过拉拉维尔微风 包括我 测试其中的功能。 让我们看一些例子:

tests/Feature/RegistrationTest.php

use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
 
class RegistrationTest extends TestCase
{
    use RefreshDatabase;
 
    public function test_registration_screen_can_be_rendered()
    {
        $response = $this->get('/register');
 
        $response->assertStatus(200);
    }
 
    public function test_new_users_can_register()
    {
        $response = $this->post('/register', [
            'name' => 'Test User',
            'email' => 'test@example.com',
            'password' => 'password',
            'password_confirmation' => 'password',
        ]);
 
        $this->assertAuthenticated();
        $response->assertRedirect(RouteServiceProvider::HOME);
    }
}

这里我们在一个类中有两个测试,因为它们都与注册表相关:一个检查表单是否正确加载,另一个检查提交是否正常。

让我们熟悉另外两种验证结果的方法,另外两个断言: $this->assertAuthenticated()$response->assertRedirect()。 您可以检查官方文档中可用的所有断言 PHPUnit e Laravel 响应 。 请注意,关于该主题的一些一般性断言 $this,而其他人则检查具体 $response从路由调用。

另一个重要的事情是 use RefreshDatabase;语句,用笔画插入类上方。 当测试操作可能影响数据库时,这是必要的,如本例所示,日志记录在 users数据库表。 为此,您应该创建一个单独的测试数据库,该数据库将更新为 php artisan migrate:fresh每次运行测试时。

您有两个选择:物理创建一个单独的数据库或使用内存 SQLite 数据库。 两者都在文件中配置 phpunit.xml默认提供defi妮塔与 Laravel。 具体来说,您需要这部分:

<php>
    <env name="APP_ENV" value="testing"/>
    <env name="BCRYPT_ROUNDS" value="4"/>
    <env name="CACHE_DRIVER" value="array"/>
    <!-- <env name="DB_CONNECTION" value="sqlite"/> -->
    <!-- <env name="DB_DATABASE" value=":memory:"/> -->
    <env name="MAIL_MAILER" value="array"/>
    <env name="QUEUE_CONNECTION" value="sync"/>
    <env name="SESSION_DRIVER" value="array"/>
    <env name="TELESCOPE_ENABLED" value="false"/>
</php>

请参阅 DB_CONNECTIONDB_DATABASE哪些被评论了? 如果您的服务器上有 SQLite,最简单的操作就是取消注释这些行,您的测试将针对该内存数据库运行。

在这个测试中,我们说用户已成功通过身份验证并重定向到正确的主页,但我们也可以测试数据库中的实际数据。

除了这段代码之外:

$this->assertAuthenticated();
$response->assertRedirect(RouteServiceProvider::HOME);

我们还可以使用 数据库测试断言 并做这样的事情:

$this->assertDatabaseCount('users', 1);
 
// Or...
$this->assertDatabaseHas('users', [
    'email' => 'test@example.com',
]);

登录页面示例

现在让我们看看 Laravel Breeze 登录页面的另一个示例

tests/Feature/AuthenticationTest.php:

class AuthenticationTest extends TestCase
{
    use RefreshDatabase;
 
    public function test_login_screen_can_be_rendered()
    {
        $response = $this->get('/login');
 
        $response->assertStatus(200);
    }
 
    public function test_users_can_authenticate_using_the_login_screen()
    {
        $user = User::factory()->create();
 
        $response = $this->post('/login', [
            'email' => $user->email,
            'password' => 'password',
        ]);
 
        $this->assertAuthenticated();
        $response->assertRedirect(RouteServiceProvider::HOME);
    }
 
    public function test_users_can_not_authenticate_with_invalid_password()
    {
        $user = User::factory()->create();
 
        $this->post('/login', [
            'email' => $user->email,
            'password' => 'wrong-password',
        ]);
 
        $this->assertGuest();
    }
}

这是关于登录表单的。 逻辑和注册类似吧? 但是是三种方法而不是两种,因此这是测试好场景和坏场景的示例。 因此,常见的逻辑是您应该测试两种情况:当事情进展顺利时和当事情失败时。

创新通讯
不要错过有关创新的最重要新闻。 注册以通过电子邮件接收它们。

另外,您在此测试中看到的是使用 数据库工厂 :Laravel 创建假用户( 再次,在您更新的测试数据库上 ),然后尝试使用正确或不正确的凭据登录。

Laravel 再次生成工厂预置defi妮塔(nita)的虚假数据 User模型,在盒子外面。

database/factories/UserFactory.php:

class UserFactory extends Factory
{
    public function definition()
    {
        return [
            'name' => $this->faker->name(),
            'email' => $this->faker->unique()->safeEmail(),
            'email_verified_at' => now(),
            'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
            'remember_token' => Str::random(10),
        ];
    }
}

你看,Laravel 自己准备了多少东西,那么我们开始测试是不是很容易呢?

所以如果我们执行 php artisan test安装 Laravel Breeze 后,我们应该看到如下内容:

 PASS  Tests\Unit\ExampleTest
✓ that true is true
 
 PASS  Tests\Feature\Auth\AuthenticationTest
✓ login screen can be rendered
✓ users can authenticate using the login screen
✓ users can not authenticate with invalid password
 
 PASS  Tests\Feature\Auth\EmailVerificationTest
✓ email verification screen can be rendered
✓ email can be verified
✓ email is not verified with invalid hash
 
 PASS  Tests\Feature\Auth\PasswordConfirmationTest
✓ confirm password screen can be rendered
✓ password can be confirmed
✓ password is not confirmed with invalid password
 
 PASS  Tests\Feature\Auth\PasswordResetTest
✓ reset password link screen can be rendered
✓ reset password link can be requested
✓ reset password screen can be rendered
✓ password can be reset with valid token
 
 PASS  Tests\Feature\Auth\RegistrationTest
✓ registration screen can be rendered
✓ new users can register
 
 PASS  Tests\Feature\ExampleTest
✓ the application returns a successful response
 
Tests:  17 passed
Time:   0.61s

功能测试与单元测试和其他测试的比较

您已经看到了子文件夹 tests/Feature e tests/Unit ?. 

它们之间有什么区别? 

在全球范围内,在 Laravel/PHP 生态系统之外,还有多种类型的自动化测试。 您可以找到如下术语:

  • 单元测试
  • 功能测试
  • 集成测试
  • 功能测试
  • 端到端测试
  • 验收测试
  • 烟雾测试
  • 等等

这听起来很复杂,而且这些类型的测试之间的实际差异有时是模糊的。 这就是为什么 Laravel 简化了所有这些令人困惑的术语,并将它们分为两部分:单元/功能。

简而言之,功能测试尝试执行应用程序的实际功能:获取 URL、调用 API、模仿填写表单等确切行为。 功能测试通常执行与任何项目用户在现实生活中手动执行的相同或相似的操作。

单元测试有两个含义。 一般来说,你可能会发现任何自动化测试都称为“单元测试”,整个过程都可以称为“单元测试”。 但在功能与单元的背景下,这个过程是关于单独测试特定的非公共代码单元。 例如,您有一个 Laravel 类,其中包含一个计算某些内容的方法,例如带参数的总订单价格。 因此,单元测试将说明是否使用不同的参数从该方法(代码单元)返回正确的结果。

要生成单元测试,您需要添加一个标志:

php artisan make:test OrderPriceTest --unit

生成的代码与预单元测试相同defi拉拉维尔系统:

class OrderPriceTest extends TestCase
{
    public function test_example()
    {
        $this->assertTrue(true);
    }
}

正如你所看到的,它不存在 RefreshDatabase,这是其中之一 defi最常见的单元测试定义:它不接触数据库,它作为一个“黑匣子”工作,与正在运行的应用程序隔离。

尝试模仿我之前提到的示例,假设我们有一个服务类 OrderPrice.

app/Services/OrderPriceService.php:

class OrderPriceService
{
    public function calculatePrice($productId, $quantity, $tax = 0.0)
    {
        // Some kind of calculation logic
    }
}

然后,单元测试可能如下所示:

class OrderPriceTest extends TestCase
{
    public function test_single_product_no_taxes()
    {
        $product = Product::factory()->create(); // generate a fake product
        $price = (new OrderPriceService())->calculatePrice($product->id, 1);
        $this->assertEquals(1, $price);
    }
 
    public function test_single_product_with_taxes()
    {
        $price = (new OrderPriceService())->calculatePrice($product->id, 1, 20);
        $this->assertEquals(1.2, $price);
    }
 
    // More cases with more parameters
}

根据我个人对 Laravel 项目的经验,绝大多数测试都是功能测试,而不是单元测试。 首先,您需要测试您的应用程序是否能够正常工作,就像人们使用它的方式一样。

接下来,如果您有特殊的计算或逻辑,您可以 definire 作为一个单元,带有参数,您可以专门为此创建单元测试。

有时,编写测试需要修改代码本身并重构它以使其更加“可测试”:将单元分成特殊的类或方法。

何时/如何进行测试?

这个有什么实际用途 php artisan test,你应该什么时候运行它?

根据您的业务工作流程,有不同的方法,但通常您需要确保在将最新代码更改推送到存储库之前所有测试都是“绿色”(即无错误)。

然后,您在本地完成任务,当您认为完成时,运行一些测试以确保没有破坏任何内容。 请记住,您的代码不仅可能会导致逻辑错误,而且可能会无意中破坏别人很久以前编写的代码中的某些其他行为。

如果我们更进一步,自动化是可能的 许多 事物。 使用各种 CI/CD 工具,您可以指定每当有人将更改推送到特定 Git 分支时或在将代码合并到生产分支之前运行的测试。 最简单的工作流程是使用 Github Actions,我有 一个单独的视频 这证明了这一点。

你应该测试什么?

对于所谓的“测试覆盖率”应该有多大,存在不同的意见:在每个页面上尝试每一种可能的操作和案例,或者将工作限制在最重要的部分。

事实上,我同意那些指责自动化测试花费更多时间而不提供实际好处的人​​的观点。 如果您为每个细节编写测试,就会发生这种情况。 也就是说,您的项目可能需要:主要问题是“潜在错误的代价是多少”。

换句话说,您需要通过询问“如果此代码失败会发生什么?”的问题来确定测试工作的优先级。 如果您的支付系统出现错误,将直接影响业务。 因此,如果您的角色/权限的功能被破坏,这将是一个巨大的安全问题。

我喜欢马特·斯托弗在一次会议上的说法:“你必须首先测试那些事情,如果失败,你就会被解雇。” 当然,这有点夸张,但你明白了:首先尝试重要的事情。 如果你有时间的话,还有其他功能。

PEST:PHPUnit 的新替代品

以上所有示例均基于Laravel预测试工具defi晚上: PHPUnit 。 但多年来,生态系统中出现了其他工具,其中最新流行的工具之一是 PEST 。 由 Laravel 官方员工创建 努诺·马杜罗 ,旨在简化语法,使测试代码的编写速度更快。

在引擎盖下,它运行 su PHPUnit,作为一个附加层,只是试图最小化一些预先重复的部分defiPHPUnit 代码的一部分。

让我们看一个例子。 记住预功能测试类defi在 Laravel 中使用? 我会提醒你:

namespace Tests\Feature;
 
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
 
class ExampleTest extends TestCase
{
    public function test_the_application_returns_a_successful_response()
    {
        $response = $this->get('/');
 
        $response->assertStatus(200);
    }
}

您知道使用 PEST 进行相同的测试会是什么样子吗?

test('the application returns a successful response')->get('/')->assertStatus(200);

是的,一行代码就可以了。 因此,PEST 的目标是消除以下开销:

  • 为一切创建类和方法;
  • 测试用例扩展;
  • 通过将操作放在单独的行上:在 PEST 中,您可以将它们链接在一起。

要在 Laravel 中生成 PEST 测试,您需要指定一个附加标志:

php artisan make:test HomepageTest --pest

截至撰写本文时,PEST 在 Laravel 开发人员中相当流行,但是否使用这个附加工具并学习其语法以及 PHPUnit 注释取决于您个人的喜好。

BlogInnovazione.it

创新通讯
不要错过有关创新的最重要新闻。 注册以通过电子邮件接收它们。

Articoli最新回应

儿童涂色页的好处 - 适合所有年龄段的魔法世界

通过着色培养精细运动技能可以帮助孩子们为写作等更复杂的技能做好准备。填色…

2 2024五月

未来已来:航运业如何彻底改变全球经济

海军部门是真正的全球经济力量,已迈向 150 亿美元的市场……

1 2024五月

出版商和 OpenAI 签署协议以规范人工智能处理的信息流

上周一,英国《金融时报》宣布与 OpenAI 达成协议。英国《金融时报》授予其世界级新闻报道许可……

四月30 2024

在线支付:流媒体服务如何让您永远付款

数百万人为流媒体服务付费,每月支付订阅费。人们普遍认为您...

四月29 2024