学习如何在 Laravel 中使用 PHPUnit 和 PEST 进行简单的测试示例

发布时间:作者:

Learn how to start Testing in Laravel with Simple Examples using PHPUnit and PEST image

谈到任何编程语言中的自动化测试或单元测试,有两类人

  • 那些不写自动化测试并认为它们是浪费时间的人
  • 那些编写测试,然后无法想象没有它们的工作的人

所以,在这篇文章中,我将尝试说服前者看看另一面,了解它的好处,以及如何在 Laravel 中轻松开始自动化测试。

首先,让我们谈谈“为什么”,然后我会展示几个“如何”的非常基本的例子。


为什么需要自动化测试

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

这是主要好处:自动重新测试所有功能。这似乎是额外的工作,但如果你不告诉“机器人”去做,那你应该手动自己去做,对吧?或者你只是在没有太多测试的情况下发布新功能,希望用户会报告错误?我讽刺地称这种方法为“祈祷式开发”。

随着应用程序的每个新功能,自动化测试的回报越来越高。

  • 功能 1:节省 X 分钟手动测试
  • 功能 2:节省 2X 分钟 - 针对功能 2 和功能 1 再次测试
  • 功能 3:节省 3X 分钟...
  • 等等。

你明白了吧。想象一下你的应用程序在一年或两年后,团队中有新开发人员,他们甚至不知道“功能 1”是如何工作的,也不知道如何复制它进行测试。因此,你未来的自己会非常感谢你编写了自动化测试。

当然,如果你认为你的项目是一个非常短期的项目,你并不关心它的未来......不,我相信你的好意,所以让我告诉你如何轻松地开始测试。


我们的第一个自动化测试

要在 Laravel 中运行第一个自动化测试,你不需要编写任何代码。是的,你没看错。默认的 Laravel 安装中已经配置并准备好了所有内容,包括第一个真正的基本示例。

你可以尝试安装一个 Laravel 项目,然后立即运行第一个测试

laravel new project
cd project
php artisan test

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

PASS Tests\Unit\ExampleTest
that true is true
 
PASS Tests\Feature\ExampleTest
the application returns a successful response
 
Tests: 2 passed
Time: 0.10s

如果我们看一下默认的 Laravel /tests 文件夹,我们有两个文件。

tests/Feature/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() 在查看测试结果时会变成可读文本,只需用空格替换下划线符号。

tests/Unit/ExampleTest.php:

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

这看起来有点毫无意义,检查 true 是否为 true?我们稍后会专门谈论单元测试。现在,你需要了解每个测试中通常发生的事情。

  • tests/ 文件夹中的每个测试文件都是一个 PHP 类,扩展了 PHPUnit 的 TestCase。
  • 在每个类内部,你可以创建多个方法,通常一个方法用于测试一种情况。
  • 在每个方法内部,有三个操作:准备情况,然后执行操作,然后检查(断言)结果是否如预期。

从结构上来说,你只需要知道这些,其他一切取决于你想要测试的具体内容。

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

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);
}
}

如果测试失败怎么办?

让我告诉你如果测试断言没有返回预期结果会发生什么。

让我们将示例测试编辑为以下内容

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 Breeze 启动工具包带有一个 内部功能测试 吗?所以,让我们看看其中的一些例子。

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' => '[email protected]',
'password' => 'password',
'password_confirmation' => 'password',
]);
 
$this->assertAuthenticated();
$response->assertRedirect(RouteServiceProvider::HOME);
}
}

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

我们熟悉了另外两种检查结果的方法,另外两个断言:$this->assertAuthenticated()$response->assertRedirect()。你可以在 PHPUnitLaravel 响应 的官方文档中查看所有可用的断言。请记住,一些通用断言发生在 $this 对象上,而另一些则检查来自路由调用的特定 $response

另一个重要的事情是 use RefreshDatabase; 语句,以及该特质,在类上方包含。当你的测试操作可能影响数据库时,它就很有必要,例如在这个示例中,注册会在 users 数据库表中添加一个新条目。为此,你需要创建一个单独的测试数据库,每次执行测试时,都会使用 php artisan migrate:fresh 来刷新它。

你有两种选择:物理上创建一个单独的数据库,或者使用内存中的 SQLite 数据库。它都配置在默认情况下与 Laravel 一起提供的 phpunit.xml 文件中。具体来说,你需要以下部分

<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' => '[email protected]',
]);

另一个现实生活中的示例:登录表单

让我们看一下 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 默认情况下会为 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 自己准备了多少东西,所以我们可以轻松地开始测试?

所以,如果我们在安装 Laravel Breeze 后运行 php artisan test,我们应该看到类似这样的内容

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

功能测试 VS 单元测试 VS 其他

你可能已经见过 tests/Featuretests/Unit 子文件夹。它们之间有什么区别?答案有点“哲学”。

在全局范围内,除了 Laravel/PHP 生态系统之外,还有不同类型的自动化测试。你可以找到诸如以下术语:

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

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

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

单元测试有两个含义。通常,你可能会发现任何自动化测试都被称为“单元测试”,整个过程也可能被称为“单元测试”。但在功能与单元的语境下,这个过程指的是测试代码中的某个特定非公共单元,处于隔离状态。例如,你有一些 Laravel 类,其中有一个方法用来计算某些内容,比如带有参数的订单总价。因此,你的单元测试将断言该方法(代码单元)在使用不同的参数时是否返回了正确的结果。

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

php artisan make:test OrderPriceTest --unit

生成的代码与 Laravel 中的默认单元测试相同

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

如你所见,这里没有 RefreshDatabase,这是单元测试最常见的定义之一:它不接触数据库,它就像一个“黑盒”,与正在运行的应用程序隔离。

让我们尝试模仿我之前提到的示例,假设我们有一个服务类 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 项目中的经验表明,绝大多数测试都是功能测试,而不是单元测试。首先,你需要测试你的应用程序是否按预期工作,就像真实用户使用它那样。

其次,如果你有一些特殊的计算或逻辑可以定义为一个单元,带有参数,你就可以专门为它创建单元测试。

有时,编写测试需要改变代码本身,并对其进行重构,使其更易于测试:将单元分离到专门的类或方法中。


何时/如何运行测试?

当你应该运行 php artisan test 时,它的实际用途是什么?

根据你公司的开发流程,有不同的方法,但通常情况下,你需要确保所有测试都是“绿色的”(意味着没有错误),然后你才能将最新的代码更改推送到代码库中。

因此,你可以在本地进行任务开发,当你觉得完成了时,运行测试以确保你没有破坏任何东西。请记住,你的代码不仅可能导致你的逻辑出现错误,而且还会无意中破坏很久以前写的其他人的代码中的某些行为。

如果更进一步,可以将许多事情自动化。使用各种 CI/CD 工具,你可以指定在有人将更改推送到某个特定 Git 分支时,或者在将代码合并到生产分支之前,执行你的测试。最简单的流程是使用 Github Actions,我有一个单独的视频来演示它。


你应该测试什么?

关于你的所谓的“测试覆盖率”应该有多大,有很多不同的观点:你应该测试每个操作和每个页面上的所有可能情况,还是只将工作限制在最重要的部分?

的确,我同意那些指责自动化测试占用时间超过实际收益的人的观点。如果你是为每一个细节编写测试,这种情况就可能发生。也就是说,这可能是你的项目要求的:主要问题是“潜在错误的代价是多少”。

换句话说,你需要用“如果这段代码失败会发生什么?”这个问题来优先考虑你的测试工作。如果你的支付系统存在错误,它会直接影响业务。然后,如果你的角色/权限功能被破坏,那么这是一个巨大的安全问题。

我喜欢 Matt Stauffer 在一次会议上说的话:“你需要先测试那些如果失败会让你被解雇的东西”。当然,这是一种夸张的说法,但你懂我的意思:先测试重要的东西。如果你有时间,再测试其他功能。


PEST:PHPUnit 的流行替代方案

上面所有示例都是基于 Laravel 的默认测试工具:PHPUnit。但在这些年来,生态系统中出现了其他工具,其中一个最新的流行工具是PEST。它由 Laravel 官方员工Nuno Maduro创建,其目标是简化语法,使编写测试代码变得更加快捷。

在底层,它作为 PHPUnit 的附加层运行在 PHPUnit 之上,只是试图最大程度地减少 PHPUnit 代码中一些默认的重复部分。

让我们看一下示例。还记得 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 的目标是消除以下方面的开销:

  • 为所有内容创建类和方法;
  • 扩展 TestCase;
  • 将操作放在单独的行上 - 在 PEST 中,你可以将它们链接起来。

要在 Laravel 中生成 PEST 测试,你需要指定一个额外的标志

php artisan make:test HomepageTest --pest

在撰写本文时,PEST 在 Laravel 开发人员中非常流行,但是否使用这个额外的工具并学习它的语法,取决于你的个人喜好,除了众所周知的 PHPUnit 之外。


所以,这就是你需要了解的关于自动化测试基础知识的所有内容。从这里开始,你就可以选择创建哪些测试以及如何在你的项目中运行它们。

有关更多信息,请参阅Laravel 关于测试的官方文档

PovilasKorop photo

Laravel Daily 的课程和教程创建者Laravel Daily

Cube

Laravel 新闻

加入 40,000 多名其他开发者,绝不错过新的技巧、教程等信息。

Laravel Forge logo

Laravel Forge

轻松创建和管理你的服务器,并在几秒钟内部署你的 Laravel 应用程序。

Laravel Forge
Tinkerwell logo

Tinkerwell

Laravel 开发人员必备的代码运行器。使用 AI、自动完成和本地和生产环境的即时反馈进行调试。

Tinkerwell
No Compromises logo

无妥协

Joel 和 Aaron,来自 No Compromises 播客的两位经验丰富的开发人员,现在可以为你的 Laravel 项目提供服务。 ⬧ 固定价格 7500 美元/月。 ⬧ 无需冗长的销售流程。 ⬧ 无需合同。 ⬧ 100% 退款保证。

无妥协
Kirschbaum logo

Kirschbaum

提供创新和稳定性,以确保你的 Web 应用程序取得成功。

Kirschbaum
Shift logo

Shift

正在运行旧版本的 Laravel?立即执行自动化的 Laravel 升级和代码现代化,让你的应用程序焕然一新。

Shift
Bacancy logo

Bacancy

只需每月 2500 美元,就可以为你的项目配备一名拥有 4-6 年经验的经验丰富的 Laravel 开发人员。获得 160 小时的专业知识和 15 天的无风险试用。立即预约电话!

Bacancy
Lucky Media logo

Lucky Media

立即获得 Lucky - Laravel 开发的理想选择,拥有十多年的经验!

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar:Laravel 电子商务

Laravel 的电子商务。一个开源软件包,将现代无头电子商务功能的力量带到 Laravel。

Lunar:Laravel 电子商务
LaraJobs logo

LaraJobs

官方 Laravel 招聘网站

LaraJobs
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit:Laravel SaaS 启动工具包

SaaSykit 是一个 Laravel SaaS 启动工具包,包含运行现代 SaaS 所需的所有功能。支付、漂亮的结账、管理面板、用户仪表板、身份验证、现成组件、统计数据、博客、文档等等。

SaaSykit:Laravel SaaS 启动工具包
Rector logo

Rector

你无缝升级 Laravel、降低成本和加速创新的合作伙伴,为成功企业提供服务

Rector
MongoDB logo

MongoDB

通过 MongoDB 和 Laravel 的强大集成来增强你的 PHP 应用程序,使开发人员能够轻松高效地构建应用程序。支持事务、搜索、分析和移动用例,同时使用熟悉的 Eloquent API。探索 MongoDB 灵活的现代数据库如何改变你的 Laravel 应用程序。

MongoDB
Maska is a Simple Zero-dependency Input Mask Library image

Maska 是一个简单的零依赖输入掩码库

阅读文章
Add Swagger UI to Your Laravel Application image

在你的 Laravel 应用程序中添加 Swagger UI

阅读文章
Assert the Exact JSON Structure of a Response in Laravel 11.19 image

在 Laravel 11.19 中断言响应的精确 JSON 结构

阅读文章
Build SSH Apps with PHP and Laravel Prompts image

使用 PHP 和 Laravel 提示构建 SSH 应用程序

阅读文章
Building fast, fuzzy site search with Laravel and Typesense image

使用 Laravel 和 Typesense 构建快速、模糊的网站搜索

阅读文章
Add Comments to your Laravel Application with the Commenter Package image

使用 Commenter 软件包为你的 Laravel 应用程序添加评论

阅读文章