通过 Behat 注册功能

既然登录功能的 Behat 测试已经失败,那么我们就尽量减少工作量来完成该功能,并通过测试。幸运的是,Symfony 可以轻松实现安全性。我们可以使用 symfony/security-bundle Composer 包为应用程序添加身份验证和授权,而无需从头开始构建一切。

有关 Symfony 安全文档的更多信息,请访问 https://symfony.com/doc/current/security.html

为了通过不合格的 Behat 注册功能,因为 Behat 模拟的是使用 Web 浏览器的用户,所以我们必须创建真实用户所需的所有程序,以便用户能够通过 Web 浏览器在我们的应用程序中注册账户,然后访问控制器、服务,最后进入数据库持久化流程。让我们从控制器开始。

编写失败的控制器测试

在通过主要的 Behat 功能测试(也可视为功能测试)之前,我们先在 Symfony 应用程序中编写一些控制器测试。尽管 Behat 测试也会对控制器进行测试,但这些 Symfony 控制器测试的复杂程度要低于 Behat 功能测试。

通过阅读我们之前创建的 Behat 注册功能,我们可以很容易地确定我们至少需要两个控制器:一个主页控制器和一个注册页面控制器。主页是用户开始旅程的地方,而注册页面则是办事员注册新账户的地方。

创建包含以下内容的主页测试类:

codebase/symfony/tests/Integration/Controller/HomeControllerTest.php
<?php

namespace App\Tests\Integration\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class HomeControllerTest extends WebTestCase
{
    public function testCanLoadIndex(): void
    {
        $client = static::createClient();
        $client->request('GET', '/');

        $this->assertResponseIsSuccessful();
    }
}

接下来,创建一个注册页面测试类,内容如下:

codebase/symfony/tests/Integration/Controller/RegistrationControllerTest.php
<?php

namespace App\Tests\Integration\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class RegistrationControllerTest extends WebTestCase
{
    public function testCanLoadRegister(): void
    {
        $client = static::createClient();
        $client->request('GET', '/register');

        $this->assertResponseIsSuccessful();
        $this->markTestIncomplete();
    }
}

现在我们已经对用于通过 Behat 功能测试的主控制器进行了测试,让我们首先看看是否通过了这些 Symfony 测试。

运行以下命令:

/var/www/html/symfony# php bin/phpunit --testsuite Functional

运行测试后,应该会有两个测试失败。我们使用了 --testsuite 参数,这样就只能执行我们刚刚创建的两个控制器测试。

现在我们知道必须通过这两个测试,因此我们可以继续研究通过这两个测试的解决方案。在这一阶段,我们处于本章前面讨论的 红-绿-重构模式 中的 "红" 阶段。

现在,我们可以首先开始研究注册和注册解决方案。

使用 Symfony 实现注册解决方案

使用开源框架的好处在于,我们开发人员需要为自己的项目构建的许多软件很有可能已经作为开源库或软件包构建好了。为了通过我们的失败注册测试,让我们使用 Symfony 的 security-bundle 包,而不是从头开始编写一切。

请记住,作为软件开发人员,我们不仅仅是开发代码那么简单。我们开发的是解决方案。如果有现成的软件包或库可以帮助你加快解决方案的开发,而且符合你的要求,你就可以考虑使用它们。否则,你就必须从头开始编写代码。

关于 Symfony 的安全解决方案,你可以在其官方文档页面上阅读: https://symfony.com/doc/current/security.html

我们可以通过运行以下命令来使用 Symfony 的安全解决方案:

/var/www/html/symfony# php bin/console make:user

阅读提示并输入建议的默认值。

接下来,我们需要设置所需的数据库。请记住,我们不仅要使用一个数据库,还需要一个单独的测试数据库。有关这方面的更多信息,请参阅 第 5 章 "单元测试"

数据库设置

我们需要创建两个数据库:carscars_test 数据库。cars 数据库将作为我们的主数据库,而 cars_test 数据库将作为我们的自动测试使用的副本数据库。毕竟,你不想在生产数据库中运行数据突变测试。

运行以下命令设置数据库:

/var/www/html/symfony# php bin/console doctrine:database:create --env=test
/var/www/html/symfony# php bin/console doctrine:database:create
/var/www/html/symfony# php bin/console make:migration
/var/www/html/symfony# php bin/console doctrine:migrations:migrate -n --env=test
/var/www/html/symfony# php bin/console doctrine:migrations:migrate -n

正如我们在 第 5 章“单元测试” 中所做的那样,我们基于 codebase/symfony/src/Entity 目录中的 Doctrine 实体创建了 MySQL 数据库和表。

接下来,让我们使用 Symfony 的 security-bundle 包创建一个注册表单。

使用 Symfony 的注册表

接下来,我们可以使用 Symfony 的注册表单。基本解决方案代码已经在 composer.json 文件中声明了所有的依赖关系,因此只需运行以下命令即可生成注册代码:

/var/www/html/symfony# php bin/console make:registration-form

上述命令将生成几个文件,其中一个是 RegistrationController.php 类。打开该类,你会发现它有一个 register 方法。我们还为该控制器和方法创建了一个测试。让我们看看它现在是否正常工作。

运行以下命令:

/var/www/html/symfony# php bin/phpunit --filter RegistrationControllerTest

运行测试后,我们现在应该能够通过此测试:

image 2023 10 24 08 36 55 525
Figure 1. Figure 7.9 – Passing the register route test

现阶段,我们正处于 "红-绿-重构" 模式 的 "绿" 阶段。这是否意味着我们已经完成了注册功能?绝对不是。由于我们尚未完成这项测试,通常我会使用 PHPUnit$this->markTestIncomplete(); 方法,并将其添加到测试类中。这有助于提醒开发人员,测试已经编写完成,解决方案也部分完成,但仍不完整。继续在 codebase/symfony/tests/Functional/ Controller/RegistrationControllerTest.php 测试类的 testCanLoadRegister 方法中添加 $this->markTestIncomplete(); 方法。

现在,再次运行测试:

/var/www/html/symfony# php bin/phpunit --filter RegistrationControllerTest

您应该看到以下结果:

image 2023 10 24 08 38 54 194
Figure 2. Figure 7.10 – Incomplete register route test

现在,测试被标记为未完成,我们可以稍后再进行测试。你可以自行决定是否使用这项功能,但我发现它在大型项目中非常有用。我唯一不喜欢的是,有时它不像失败的测试那样能引起我的注意。现在,让我们移除 "未完成" 标记。

创建 home 控制器

现在,让我们创建一个主页控制器,用户通常会首先进入该页面。在这里,我们还将找到注册链接,用户点击后将被重定向到注册页面。

运行以下命令创建主控制器:

/var/www/html/symfony# php bin/console make:controller HomeController

运行该命令后,我们将在 codebase/symfony/src/Controller/HomeController.php 中创建一个新的 Symfony 控制器。编辑控制器内的路由,将 /home 替换为斜线 (/)。

现在,让我们看看控制器测试是否通过。再次运行 Symfony 功能测试:

/var/www/html/symfony# php bin/phpunit --testsuite Functional --debug

您现在应该看到以下结果:

image 2023 10 24 08 42 33 094
Figure 3. Figure 7.11 – Passing controller tests

由于我们的控制器测试非常简单,因此基本上只是测试路由的页面响应是否成功;现在我们可以确保两个测试都通过了。不过,这并不能满足 Behat 注册功能测试的要求。因此,让我们继续努力!

让我们修改主页控制器的 twig 模板内容。打开以下文件,将 example-wrapper div 内容全部替换为以下内容:

codebase/symfony/templates/home/index.html.twig
<div class="example-wrapper">
    <h1>{{ controller_name }}</h1>
    <ul>
        <li>
            <a href="/register" id="lnk-register">Register</a>
        </li>
    </ul>
</div>

我们刚刚在注册页面添加了一个链接。如果您尝试通过浏览器访问主页,会看到类似下面的内容:

image 2023 10 24 08 45 14 099
Figure 4. Figure 7.12 – HomeController

接下来,让我们回到 behat 目录中的 BDD 测试。让我们尝试编写一些测试代码,看看最终是否可以注册新用户。

通过 Behat 功能

我们的 Behat 注册功能会模拟用户访问主页、点击注册链接、被重定向到注册页面、填写注册表、点击注册按钮,然后被重定向到某个当选页面。

这与手动测试人员测试注册功能的过程如出一辙。与其使用浏览器手动完成这些步骤,不如使用 Behat 来完成所有这些步骤。

打开以下 Behat 上下文文件,并将内容替换为以下内容:

codebase/behat/features/bootstrap/InventoryClerkRegistrationContext.php
<?php

use Behat\Mink\Mink;
use Behat\Mink\Session;
use Behat\Mink\Driver\GoutteDriver;
use Behat\MinkExtension\Context\MinkContext;
use Behat\MinkExtension\Context\MinkAwareContext;

/**
* Defines application features from the specific context.
*/
class InventoryClerkRegistrationContext extends MinkContext implements MinkAwareContext
{
    /**
    * Initializes context.
    *
    * Every scenario gets its own context instance.
    * You can also pass arbitrary arguments to the
    * context constructor through behat.yml.
    */
    public function __construct()
    {
        $mink = new Mink([
            'goutte' => new Session(new GoutteDriver()), // Headless browser
        ]);

        $this->setMink($mink);
        $this->getMink()->getSession('goutte')->start();
    }
}

在前面的代码片段中,我们从构造函数开始。我们声明了将在类中使用的模拟器和会话对象。

接下来,添加以下代码:

/**
* @Given I am in the home :arg1 path
*/
public function iAmInTheHomePath($arg1)
{
    $sessionHeadless = $this->getMink()->getSession('goutte');
    $sessionHeadless->visit($arg1);

    // Make sure the register link exists.
    $assertHeadless = $this->assertSession('goutte');
    $assertHeadless->elementExists('css', '#lnk-register');
}

/**
* @When I click the :arg1 link
*/
public function iClickTheLink($arg1)
{
    $sessionHeadless = $this->getMink()->getSession('goutte');
    $homePage = $sessionHeadless->getPage();
    $homePage->clickLink($arg1);
}

前面的代码将模拟用户位于主页上,然后单击 “注册” 链接。

在下一个片段中,Behat 将尝试确认它已重定向到注册控制器页面:

/**
* @Then I should be redirected to the registration page
*/
public function iShouldBeRedirectedToTheRegistrationPage()
{
    // Make sure we are in the correct page.
    $assertHeadless = $this->assertSession('goutte');

    $assertHeadless->pageTextContains('Register');
    $assertHeadless->elementExists('css', '#registration_form_email');
}

您可以通过检查路由轻松检查是否在正确的页面上,但前面的代码段显示,您可以检查控制器返回的 DOM 本身。

接下来,添加以下代码,模仿用户在输入表单中输入值:

/**
* @When I fill in Email :arg1 with :arg2
*/
public function iFillInEmailWith($arg1, $arg2)
{
    $sessionHeadless = $this->getMink()->getSession('goutte');
    $registrationPage = $sessionHeadless->getPage();
    $registrationPage->fillField($arg1, $arg2);
}

/**
* @When I fill in Password :arg1 with :arg2
*/
public function iFillInPasswordWith($arg1, $arg2)
{
    $sessionHeadless = $this->getMink()->getSession('goutte');
    $registrationPage = $sessionHeadless->getPage();
    $registrationPage->fillField($arg1, $arg2);
}

在前面的代码段中,代码模拟在电子邮件和密码字段中输入文本。接下来,我们将模拟选中复选框并点击提交按钮。添加以下代码:

/**
* @When I check the :arg1 checkbox
*/
public function iCheckTheCheckbox($arg1)
{
    $sessionHeadless = $this->getMink()->getSession('goutte');
    $registrationPage = $sessionHeadless->getPage();
    $registrationPage->checkField($arg1);
}

/**
* @When I click on the :arg1 button
*/
public function iClickOnTheButton($arg1)
{
    $sessionHeadless = $this->getMink()->getSession('goutte');
    $registrationPage = $sessionHeadless->getPage();
    $registrationPage->pressButton($arg1);
}

在前面的代码中,我们选中了 同意条款 复选框,然后单击 注册 按钮。

接下来添加以下代码完成测试:

/**
* @Then I should be able to register a new account
*/
public function iShouldBeAbleToRegisterANewAccount()
{
    $sessionHeadless = $this->getMink()->getSession('goutte');
    $thePage = $sessionHeadless->getPage()->getText();

    if (!str_contains($thePage, 'There is already an account with this email')) {
        $assertHeadless = $this->assertSession('goutte');
        $assertHeadless->addressEquals('/home');
    }
}

由于在 Symfony 应用程序中,我们会在成功后将用户重定向回主页控制器,因此我们可以检查是否重定向到了主页。你会注意到,它还会检查用户是否已经存在;你可以根据自己的需要进一步细分这个测试,以便将类似的情况区分开来。

在前面的代码块中,我们将 codebase/behat/features/inventory_clerk_registration.feature 文件中的场景分解为 PHP 方法。然后,我们编写了 PHP 代码来点击链接和按钮、填充文本字段、选中复选框等。

让我们来看看这是否有效。运行以下命令来进行测试:

/var/www/html/behat# ./runBehat.sh --suite=suite_a features/inventory_clerk_registration.feature

执行需要几秒钟,但您应该得到以下结果:

image 2023 10 24 08 58 01 746
Figure 5. Figure 7.13 – Registration feature test

通过运行 Behat 测试,我们可以取代通常在浏览器上进行的手动测试过程。但我们需要确认我们是否真的能够注册,并使用 Doctrine ORM 将数据持久化到 MySQL 数据库中!此时,我们正处于 红-绿-重构模式 中的 "重构" 阶段,我个人认为 "重构" 阶段可以更具开放性和可解释性。

您可以使用自己的 MySQL 客户端或我们在 第 3 章 "使用 Docker 容器设置开发环境" 中配置的 phpMyAdmin 应用程序来验证数据。

MySQL 容器中使用命令行将得到以下结果:

image 2023 10 24 08 59 43 355
Figure 6. Figure 7.14 – User successfully registered: view from the CLI

这是使用我们配置的 phpMyAdmin 应用程序的结果,可以使用本地浏览器访问 http://127.0.0.1:3333:

image 2023 10 24 09 00 19 454
Figure 7. Figure 7.15 – User successfully registered: view from phpMyAdmin

我们可以从数据库中看到,我们能够持久保存注册详细信息。在这一阶段,我们可以说我们的注册功能正常运行!而且我们无需手动打开桌面浏览器输入表单详细信息就能进行测试。

现在,我们有一个 PHP 程序在为我们进行注册功能测试,但我们还需要建立登录功能和最重要的部分:库存系统本身。我们还有很多其他功能需要构建,但这是一个很好的开始!

总结

在本章中,我们首先根据 Jira 票据创建了一个易于理解的功能和场景列表,详细说明了需要构建的功能和场景。在编写解决方案代码之前,我们首先从名为 "库存员注册" 的 Gherkin 功能开始。任何人都可以阅读该功能,即使是非开发人员也能理解。该功能解释了我们的系统应该如何运行。然后,我们利用这一行为,在 Symfony 应用程序中创建了简单的失败功能测试。通过创建这些失败的测试,我们列出了需要构建的内容。然后,我们继续开发解决方案,以通过这些失败的功能测试。最后,我们编写代码,告诉 Behat 点击链接或按钮以及填写字段的复杂步骤。BDDTDD 不仅仅是编写自动测试,而是将它们作为开发解决方案的过程。

在下一章中,我们将继续构建测试和解决方案代码。我们将学习 SOLID 原则,以帮助我们构建自己的代码,确保代码更具可维护性和可测试性。