通过PHP和PHPUNIT进行行为驱动的测试
#php #开发人员 #phpunit #bdt

单元测试是大多数开发人员在职业生涯中遇到的事情。对于某些开发人员而言,从第一天开始,对于其他开发人员来说,它来来去去。根据我的经验,似乎大多数开发人员都不喜欢编写清洁和高效代码的单元测试部分,直到他们意识到自己写了错误的测试为止。

写几天的写作测试

大多数开发人员将专注于他们创建的每种方法的编写实施测试。这很乏味,需要持续的更新,并且效率低下。当您为每种方法编写三个或四个测试时,很容易害怕编写这些测试,但是大多数开发人员从未教过任何其他方法来编写或思考这些测试。相同的基于实现的测试可能会导致错误和假设,即您的代码将在任何给定状态下正确执行。不要误会我的实施测试,但是它们应该非常技术性,并专注于验证代码中关键方法的特定功能。实施测试是严格的,并遵循对程序员的基于指令的思维,测试了沿途行为正确的每个步骤,但不一定测试给定特定状态的代码作为整个身体的行为正确。

什么是行为驱动的测试

行为驱动的测试(BDT)是实现测试的对立面,它在描述应用程序的行为时从外部看代码。设计这些测试甚至可以包括利益相关者和项目经理,因为它们有助于创建共享的语言和对功能的理解。您可以将BDT视为描述和测试软件应通过定义功能和方案以及执行易于测试的清洁和模块化代码的方法。此外,BDT通常会创建更好的代码覆盖范围(定义了更多分支机构和路径的测试),有助于创建清晰的文档,并通过允许开发人员更多地专注于编写代码,而不是对其进行测试或重构测试来提高代码质量。通常,以行为驱动的测试在更改代码时不需要修改。

BDT与实施

比较两个BDT以下更多内容:

I order a package, there are no holidays and no weather events.

Given my order is successfully completed

It should arrive on time and without damage.

您关心总过程的结果,而不是验证过程的每个步骤。实施测试类似于:

I place my order
It is pulled from the shelf
It is put in a box
A Shipping label is created
The box is put on a truck
The truck travels 30 miles to the shipping Partner
...

您用BDT覆盖了所有这些,但是实现测试的关键区别在于,您与应用程序的其余部分隔离介绍了这些方法。这意味着您必须假设是状态和其他方法的响应。您可以为其中一种方法编写10个测试,但是您错过了在达到此方法之前发生的状态变化的可能性呢?如果您正在测试卡车的出发时间,并且错过了从架子上拉出的延迟,这如何改变您的测试?实施测试迫使您为每种可能的分支/路径编写特定测试。

在没有程序员眼镜的情况下考虑一下

BDT和行为驱动设计的一个例子是与您的朋友或家人制作棋盘游戏。您是游戏的主要想法,但需要确保在赋予特定状态时,在发生措施时,结果是可以预测的。因此,您可能会开始定义行为If a player has won 3 battles when they complete board section A then they should be awarded 100 XP。这清楚地定义了应与给定状态一致的可测试行为。

考虑一下作为程序员

就编程而言,可以将BDT分解为相当简单的句子Given a specific state when an action or series of actions are completed the outcome should be predictable and repeatable。这到底是什么意思,我们如何定义行为?功能文件和方案是BDT中用于以通用语言定义行为的工具(通常是Gherkin)。让我们以身份验证用户为例。

FEATURE: User Authentication
  As a user I want to be able to log in and out of my account

  SCENARIO: Successful login
    GIVEN I am on the login page and supply the correct credentials
    WHEN the login form is submitted
    THEN I should be logged in
    AND a JWT should be returned
    AND I should be redirected to my home page

  SCENARIO: Failed login
    GIVEN I am on the login page and use incorrect credentials
    WHEN the login form is submitted
    THEN I should not be logged in
    AND I should be given an error message
    AND I should not be redirected
    AND I should not be allowed to manually access my home page

  SCENARIO: Logged out
    GIVEN I am logged in successfully
    WHEN I hit the logout button in the main navigation
    THEN I should be logged out
    AND the JWT destroyed
    AND I should be redirected to the home page
    AND I should not be allowed to manually access my home page

您可以在上面的示例中看到用户身份验证的不同行为从用户的角度描述。这使我们能够从三种行为中测试全部身份验证功能。考虑到这一点的另一种方法是测试鉴于起始状态,结果将永远是相同的。在上面给定正确的用户凭证的示例中,结果应始终如THEN部分所定义。

测试行为

在测试行为方面,您应该尽可能少地模拟以确保从头到尾的所有代码都正确运行,这有助于避免以后进行实施测试。一些要模拟的事情是数据库调用或HTTP请求。如果您在这些领域或诸如第三方图书馆之类的领域使用适配器,那么嘲笑它们的适配器就变得更加容易。总体而言,您嘲笑的次数越少,您可以相信行为测试的越多地代表执行代码的结果。

您可以使用专门为BDT设计的Behat之类的工具,并利用Gherkin功能文件来帮助自动化测试构建。在大多数情况下,您可能正在使用phpunit,因此我们可以从那里开始。您已经完成了一些定义方案的工作,这些工作转化为测试方法。

<?php

use PHPUnit\Framework\TestCase;

class UserAuthenticationTest extends TestCase {
    public function testThatGivenCorrectCredentialsTheUserIsLoggedIn() {
        //
    }

    public function thestThatGivenIncorrectCredentialsTheUserIsNotLoggedIn() {
        //
    }

    public function testThatTheLoggedInUserCanLogOut() {
        //
    }
}

这三个测试将涵盖您身份验证工作流程的可能行为。假设您有一个控制登录的控制器,我们可以编写以下内容。

<?php

use PHPUnit\Framework\TestCase;
use App\Controllers\Login_Controller;
use App\Core\Database;

class UserAuthenticationTest extends TestCase {
    public function testThatGivenCorrectCredentialsTheUserIsLoggedIn() {
        $Login_Controller = new Login_Controller;
        $Mock_DB = $this->getMockBuilder(Database::class)->onlyMethods(['fetchRow'])->getMock();
        $Mock_DB->expects($this->once())->method('fetchRow')->willReturn(['username' => 'user', 'password' => 'somehashedpassword']);
        $Login_Controller->Database = $Mock_DB;
        $result = $Login_Controller->logUserIn('user', 'password');

        $this->assertArrayHasKey('jwt', $result);
        $this->assertSame('ok', $result['success']);
    }
     // ...
}

在上面的示例中,我们只是在模拟的数据库类中传递,并让其余的代码按照应有的方式运行。这使我们能够测试该行为中的所有代码都可以正常运行以产生所需的结果。由于它是应用程序的关键组成部分,因此数据库类可能会进行测试,因此我们可以自在地嘲笑它。现在,一旦为剩下的两个方案创建了测试,您很可能会涵盖所有用户身份验证过程中涉及的代码的全部或所有内容。

您可能需要为残疾或暂停帐户等添加更多方案,但这只是显示测试行为的力量的要点。最好的部分是,如果用于给定情况的任何代码更改,则可能根本不必更新测试。这是因为该行为是整体测试的,其起始状态会产生可重复的结果。如果结果已更改,则可能会发生错误或更改,例如重命名jwt键。与基于实现的方法相比,如果您更改了任何代码,则身份验证方案取决于您需要更新更改的每个测试。

结论

行为驱动的测试是开发人员腰带中的另一种工具,可以帮助您进行更少的测试,花费更少的时间重构测试,对测试的可靠性更有信心,并帮助定义有关应用程序功能的共同定义。