为了构建更好的Laravel应用程序请勿跳过单元测试

为了构建更好的Laravel应用程序请勿跳过单元测试

单元测试在软件开发中至关重要,它能确保应用程序的各个组件在隔离状态下按预期运行。通过为特定代码单元编写测试,您可以在开发早期发现并修复错误,从而开发出更可靠、更稳定的软件。

在持续集成/持续交付(CI/CD)管道中,您可以在更改代码库后自动运行这些测试。这可确保新代码不会引入错误或破坏现有功能。

本文强调了单元测试在 Laravel 应用程序中的重要性,详细介绍了如何为 Laravel 应用程序编写单元测试。

PHPUnit 简介

PHPUnit 是 PHP 生态系统中广泛使用的测试框架,专为单元测试而设计。它拥有一套用于创建和运行测试的强大工具,是确保代码库可靠性和质量的重要资源。

Laravel 支持使用 PHPUnit 进行测试,并提供了方便的辅助方法,让你可以测试应用程序。

在 Laravel 项目中设置 PHPUnit 只需极少的配置。Laravel 提供了一个预配置的测试环境,包括一个 phpunit.xml 文件和一个专门的 tests 文件目录。

你也可以修改 phpunit.xml 文件,定义自定义选项,获得量身定制的测试体验。您也可以在项目根目录下创建 .env.testing 环境文件,而不是使用 .env 文件。

Laravel 中的默认测试布局

Laravel 提供了一个结构化的默认目录布局。Laravel 项目的根目录包含一个 tests 目录,其中有 FeatureUnit 子目录。这种布局可以简单地分离不同的测试类型,并保持一个整洁有序的测试环境。

Laravel 项目中的 phpunit.xml 文件对于协调测试过程、确保测试运行的一致性以及根据项目要求定制 PHPUnit 的行为至关重要。它允许你定义如何运行测试,包括定义测试套件、指定测试环境和设置数据库连接。

该文件还指定会话、缓存和电子邮件应设置为数组驱动程序,以确保在运行测试时不会持续存在会话、缓存或电子邮件数据。

注:driver 一词指的是一种配置设置,在测试过程中将数据存储在内存阵列中,以保持隔离并防止测试运行之间的数据持久化。

你可以在 Laravel 应用程序上执行几种类型的测试:

  • 单元测试 – 主要针对代码的各个组件,如类、方法和函数。这些测试与 Laravel 应用程序隔离,验证特定代码单元是否按预期运行。请注意,在 tests/Unit 目录中定义的测试不能启动 Laravel 应用程序,这意味着它们不能访问数据库或框架提供的其他服务。
  • 功能测试 – 验证应用程序更广泛的功能。这些测试模拟 HTTP 请求和响应,让你测试路由、控制器和各种组件的集成。功能测试有助于确保应用程序的不同部分按预期协同工作。
  • 浏览器测试 – 通过自动化浏览器交互,更进一步。测试使用浏览器自动化和测试工具 Laravel Dusk 来模拟用户交互,如填写表格和点击按钮。浏览器测试对于验证应用程序在真实浏览器中的行为和用户体验至关重要。

测试驱动开发概念

测试驱动开发(TDD)是一种软件开发方法,强调在执行代码前进行测试。这种方法遵循一个称为 “red-green-refactor(红-绿-重构)” 循环的过程。

测试驱动的开发周期显示为 "红-绿-重构"

 

测试驱动的开发周期显示为 “红-绿-重构”。

以下是对该周期的解释:

  • 红色阶段 – 在实现实际代码之前,编写一个新测试来定义功能或改进现有功能。测试应该失败(”红色” 表示失败),因为没有相应的代码使其通过。
  • 绿色阶段 – 编写足够的代码使失败的测试通过,将其从红色变为绿色。代码不会是最佳的,但它能满足相应测试用例的要求。
  • 重构阶段 – 在不改变代码行为的前提下,重构代码以提高其可读性、可维护性和性能。在这个阶段,你可以放心地修改代码,而不必担心任何回归问题,因为现有的测试用例会捕捉到它们。

TDD 有以下几个好处:

  • 早期错误检测 – TDD 有助于在开发过程的早期发现错误,从而减少在开发周期后期修复问题的成本和时间。
  • 改进设计 – TDD 鼓励使用模块化和松散耦合的代码来改进软件设计。它鼓励你在实施前考虑界面和组件的交互。
  • 重构的信心 – 您可以放心地重构代码,因为您知道现有的测试可以快速识别重构过程中引入的任何回归。
  • 活文档 – 测试用例提供了代码行为的示例,可作为活文档使用。由于测试失败表明代码中存在问题,因此这种文档始终是最新的。

在 Laravel 开发中,你可以应用 TDD 原则,在实现控制器、模型和服务等组件之前为它们编写测试用例。

Laravel 的测试环境,包括 PHPUnit,提供了方便的方法和断言,以促进 TDD,确保您可以创建有意义的测试,并有效地遵循红-绿-重构循环。

单元测试的基本示例

本节将介绍如何编写一个简单的测试来检查模型的功能。

前提条件

要继续学习,你需要具备以下条件:

  • 满足 Laravel 博客指南中列出的先决条件。
  • 一个 Laravel 应用程序。本教程使用上面链接的指南中创建的应用程序。你可以阅读它并创建博客应用程序,但如果你只需要源代码来实现测试,请按照下面的步骤进行。
  • 安装并配置 Xdebug,并启用覆盖模式

设置项目

  1. 在终端窗口中执行此命令克隆项目。
    git clone https://github.com/VirtuaCreative/kinsta-laravel-blog.git
  2. 移动到项目文件夹,执行 composer install 命令安装项目依赖项。
  3. env.example 文件重命名为 .env
  4. 执行 php artisan key:generate 命令生成应用密钥。

创建并运行测试

首先,确保您的机器上有项目代码。您要测试的模型是 app/Http/Models/Post.php 文件中定义的 Post 模型。该模型包含多个可填写属性,如 titledescription, 和 image

您的任务是为该模型设计简单明了的单元测试。其中一个测试验证属性是否设置正确,另一个测试则通过尝试分配不可填充属性来检查批量分配。

  1. 执行 php artisan make:test PostModelFunctionalityTest --unit 命令创建新的测试用例。--unit 选项指定这是一个单元测试,并将其保存在 tests/Unit 目录中。
  2. 打开 tests/Unit/PostModelFunctionalityTest.php 文件,用以下代码替换 test_example 函数:
    public function test_attributes_are_set_correctly()
    {
    // create a new post instance with attributes
    $post = new Post([
    'title' => 'Sample Post Title',
    'description' => 'Sample Post Description',
    'image' => 'sample_image.jpg',
    ]);
    // check if you set the attributes correctly
    $this->assertEquals('Sample Post Title', $post->title);
    $this->assertEquals('Sample Post Description', $post->description);
    $this->assertEquals('sample_image.jpg', $post->image);
    }
    public function test_non_fillable_attributes_are_not_set()
    {
    // Attempt to create a post with additional attributes (non-fillable)
    $post = new Post([
    'title' => 'Sample Post Title',
    'description' => 'Sample Post Description',
    'image' => 'sample_image.jpg',
    'author' => 'John Doe',
    ]);
    // check that the non-fillable attribute is not set on the post instance
    $this->assertArrayNotHasKey('author', $post->getAttributes());
    }

    这段代码定义了两个测试方法。

    第一个方法创建一个带有指定属性的 Post 实例,并使用 assertEquals 断言方法断言你正确设置了 titledescription, 和 image 属性。

    第二个方法尝试创建一个带有额外不可填充属性(author)的 Post 实例,并使用 assertArrayNotHasKey 断言方法断言模型实例上未设置该属性。

  3. 确保在同一文件中添加以下 use 语句:
    use App\Models\Post;
  4. 运行 php artisan config:clear 命令清除配置缓存。
  5. 要运行这些测试,请执行以下命令:
    php artisan test tests/Unit/PostModelFunctionalityTest.php

    所有测试都应通过,终端应显示结果和运行测试的总时间。

调试测试

如果测试失败,可以按照以下步骤进行调试:

  1. 查看终端中的错误信息。Laravel 提供了详细的错误信息,可以指出问题所在。仔细阅读错误信息,了解测试失败的原因。
  2. 检查正在测试的测试和代码,找出差异。
  3. 确保正确设置测试所需的数据和依赖关系。
  4. 使用调试工具(如 Laravel 的 dd() 函数)检查测试代码中特定点的变量和数据。
  5. 确定问题所在后,进行必要的修改并重新运行测试,直到通过为止。

测试与数据库

Laravel 提供了一种方便的方法,使用内存 SQLite 数据库来建立测试环境,这种数据库速度快,而且不会在测试运行之间持久化数据。要配置测试数据库环境并编写与数据库交互的测试,请按以下步骤操作:

  1. 打开 phpunit.xml 文件,取消下面几行代码的注释:
    <env name="DB_CONNECTION" value="sqlite"/>
    <env name="DB_DATABASE" value=":memory:"/>
  2. 执行 php artisan make:test PostCreationTest --unit 命令创建新的测试用例。
  3. 打开 tests/Unit/PostCreationTest.php 文件,用下面的代码替换 test_example 方法:
    public function testPostCreation()
    {
    // Create a new post and save it to the database
    $post = Post::create([
    'title' => 'Sample Post Title',
    'description' => 'Sample Post Description',
    'image' => 'sample_image.jpg',
    ]);
    // Retrieve the post from the database and assert its existence
    $createdPost = Post::find($post->id);
    $this->assertNotNull($createdPost);
    $this->assertEquals('Sample Post Title', $createdPost->title);
    }
  4. 确保添加了以下 use  声明:
    use App\Models\Post;

    目前, PostCreationTest 类扩展了 PHPUnitFrameworkTestCase 基类。该基类通常用于在 Laravel 之外直接使用 PHPUnit 进行单元测试,或为与 Laravel 不紧密耦合的组件编写测试。然而,你需要访问数据库,这意味着你必须修改 PostCreationTest 类来扩展 TestsTestCase 类。

    后者是为 Laravel 应用程序量身定制的 PHPUnitFrameworkTestCase 类。它提供了额外的功能和 Laravel 特有的设置,如数据库播种和测试环境配置。

  5. 确保将 use PHPUnitFrameworkTestCase; 语句替换为 use TestsTestCase;。请记住,您设置的测试环境使用的是内存 SQLite 数据库。因此,必须在运行测试前迁移数据库。请使用 IlluminateFoundationTestingRefreshDatabase 特性来完成这项工作。如果模式不是最新的,该特性就会迁移数据库,并在每次测试后重置数据库,以确保上一次测试的数据不会干扰后续测试。
  6. tests/Unit/PostCreationTest.php 文件中添加以下 use 语句,以便在代码中使用该特质:
    use Illuminate\Foundation\Testing\RefreshDatabase;
  7. 接下来,在 testPostCreation 方法之前添加以下代码行:
    use RefreshDatabase;
  8. 运行 php artisan config:clear 命令清除配置缓存。
  9. 要运行此测试,请执行以下命令:
    php artisan test tests/Unit/PostCreationTest.php

    测试应该通过,终端应该显示测试结果和总测试时间。

功能测试

单元测试是孤立地检查单个应用程序组件,而功能测试则是检查代码中较大的部分,如多个对象如何交互。功能测试至关重要,原因如下:

  1. 端到端验证 – 确认整个功能的无缝运行,包括控制器、模型、视图甚至数据库等不同组件之间的交互。
  2. 端到端测试 – 涵盖从初始请求到最终响应的整个用户流,从而发现单元测试可能遗漏的问题。这种能力使它们在测试用户旅程和复杂场景时非常有价值。
  3. 用户体验保证 – 模拟用户交互,帮助验证一致的用户体验以及功能是否符合预期。
  4. 回归检测 – 在引入新代码时捕捉回归和代码破坏性更改。如果现有功能在功能测试中开始失效,则表明有东西被破坏了。

现在,在 app/Http/Controllers/PostController.php 文件中为 PostController 创建一个功能测试。重点是 store 方法,验证输入的数据,在数据库中创建并存储文章。

该测试模拟用户通过网络界面创建新文章,确保代码将文章存储在数据库中,并在创建后将用户重定向到文章索引页面。为此,请执行以下步骤:

  1. 执行 php artisan make:test PostControllerTest 命令,在 tests/Features 目录下创建一个新的测试用例。
  2. 打开 tests/Feature/PostControllerTest.php 文件,用以下代码替换 test_example 方法:
    use RefreshDatabase; // Refresh the database after each test
    public function test_create_post()
    {
    // Simulate a user creating a new post through the web interface
    $response = $this->post(route('posts.store'), [
    'title' => 'New Post Title',
    'description' => 'New Post Description',
    'image' => $this->create_test_image(),
    ]);
    // Assert that the post is successfully stored in the database
    $this->assertCount(1, Post::all());
    // Assert that the user is redirected to the Posts Index page after post creation
    $response->assertRedirect(route('posts.index'));
    }
    // Helper function to create a test image for the post
    private function create_test_image()
    {
    // Create a mock image file using Laravel's UploadedFile class
    $file = UploadedFile::fake()->image('test_image.jpg');
    // Return the path to the temporary image file
    return $file;
    }

    test_create_post 函数通过向 posts.store 路由发出带有特定属性的 POST 请求,模拟用户创建新文章的过程,其中包括使用 Laravel 的 UploadedFile 类生成的模拟图片。

    然后,测试通过检查 Post::all() 的计数,断言代码已成功将文章存储到数据库中。它验证了代码是否在创建文章后将用户重定向到文章索引页面。

    该测试可确保文章创建功能正常工作,应用程序能正确处理数据库交互和文章提交后的重定向。

  3. 在同一文件中添加以下 use 语句:
    use App\Models\Post;
    use Illuminate\Http\UploadedFile;
  4. 运行命令 php artisan config:clear 清除配置缓存。
  5. 要运行此测试,请执行以下命令:
    php artisan test tests/Feature/PostControllerTest.php

    测试应该通过,终端应该显示测试结果和运行测试的总时间。

确认测试覆盖率

测试覆盖率是指单元测试、功能测试或浏览器测试在代码库中的检查范围,以百分比表示。它能帮助你识别代码库中未经测试的区域,以及可能包含错误的测试不足区域。

PHPUnit 的代码覆盖率功能和 Laravel 的内置覆盖率报告等工具会生成报告,显示测试覆盖了代码库中的哪些部分。这一过程提供了有关测试质量的重要信息,有助于您关注可能需要额外测试的区域。

生成报告

  1. 删除 tests/Feature/ExampleTest.phptests/Unit/ExampleTest.php,因为您没有修改它们,它们可能会导致错误。
  2. 在终端窗口中执行 php artisan test --coverage 命令。您应该会收到类似下面的输出:<img src=”https://kinsta.com/wp-content/uploads/2024/03/code-coverage-report.png” alt=”Screen capture showing the execution of the command php artisan test --coverage. It shows the total number of tests that passed and the time to execute the results. It also lists each component in your codebase and its code coverage percentage.” width=”1001″ height=”471″ />。执行命令 php artisan test --coverage.code 覆盖率报告会显示测试结果、通过的测试总数以及执行结果所需的时间。它还会列出代码库中的每个组件及其代码覆盖率。百分比表示测试覆盖代码的比例。例如, Models/Post 的覆盖率为 100%,这意味着该模型的所有方法和代码行都已覆盖。代码覆盖率报告还会显示 “Total Coverage“–整个代码库的总体代码覆盖率。在本例中,测试只覆盖了 65.3% 的代码。
  3. 要指定最小覆盖率阈值,请执行 php artisan test --coverage --min=85 命令。 该命令设置的最小阈值为 85%。您将收到以下输出:测试最低阈值为 85%测试最低阈值为 85%。

    测试套件失败的原因是代码没有达到 85% 的最低阈值。虽然实现更高的代码覆盖率(通常是 100%)是我们的目标,但更重要的是彻底测试应用程序的关键和复杂部分。

小结

通过采用本文概述的最佳实践,如编写有意义的综合测试、坚持 TDD 中的红-绿-重构循环,以及利用 Laravel 和 PHPUnit 提供的测试功能,你可以创建健壮而高质量的应用程序。

评论留言