Behat:编写接受测试的简单有效方法
#php #bdd #symfony #behat

行为驱动的开发(BDD)是一种软件开发方法,重点是在具体示例中定义应用程序的所需行为。这是基于这样的想法:软件系统的行为可以用技术和非技术利益相关者均可访问的语言描述。这种方法有助于确保软件系统与业务需求和期望保持一致。

如果您有兴趣在PHP项目中采用行为驱动的开发,则Behat和Gherkin为实施它提供了强大的工具。他们允许开发人员专注于应用程序的行为,而不是实施细节。在此博客文章中,我将提供有关如何使用Gherkin语言在Symfony应用程序中编写接受测试的逐步指南。通过遵循本文中概述的说明,您将能够为您的Symfony应用程序设置Behat并开始编写有效,有效的接受测试。

Behat

Behat是PHP的开源BDD框架,它允许开发人员用称为Gherkin的人类可读语言编写和执行测试。 Behat测试可以用简单的非技术语言编写,利益相关者(例如产品所有者或业务分析师)易于理解。 Behat允许开发人员专注于应用程序的行为,而不是实现细节。

小黄瓜

用小黄瓜编写的测试以场景大纲格式构成,由描述应用程序行为的一系列步骤组成。这些步骤以一种简单的自然语言编写,并在描述测试的先决条件,行动和预期结果时将其组织为给定的条款。这些方案可以进一步组织到功能文件中,将小组相关的方案在一起,并清晰简洁地概述正在测试的行为。

安装依赖项

要设置我们的测试环境,我们必须安装一些外部依赖项。

让我们从Behat开始:

composer require behat/behat --dev

另一个依赖性是MinkExtension-一个Behat扩展名,可在Behat和Mink之间进行集成(PHP的Web测试框架)。它允许开发人员编写与网页交互并测试Web应用程序功能的Behat方案。 MinkExtension提供了一组预定义的步骤,可用于执行常见的Web交互,例如单击链接,填写表单和验证页面内容。

composer require friends-of-behat/mink-extension --dev

BehatChromeExtension是一个Behat扩展程序,可在Behat和Google Chrome浏览器之间进行集成。它允许开发人员使用Chrome浏览器作为测试环境执行Behat测试。使用BehatChromeExtension,开发人员可以在现实情况下模拟用户交互并在现实世界中测试其行为。该扩展名提供了更现实的测试环境,可以帮助发现在无头浏览器环境中可能看不到的问题。

composer require dmore/behat-chrome-extension --dev

FriendsOfBehat/SymfonyExtension是提供Behat和Symfony框架之间集成的扩展。它允许开发人员编写与Symfony应用程序相互作用并测试其功能的Behat方案。扩展程序提供了一组预定义的步骤,可用于执行常见的符号特定相互作用,例如使用服务容器和测试路线进行与学说的互动。该扩展程序还提供了对Behat依赖注入和配置管理功能的支持,从而更容易在Symfony Projects中管理和维护Behat测试。

composer require friends-of-behat/symfony-extension --dev

安装上方的扩展名后,将创建以下文件结构:

|config/
| - services_test.yaml
|features/
| - demo.feature
|tests/
| - Behat/
|   - DemoContext.php
|behat.yml.dist

默认情况下,SymfonyNextension尝试加载bootstrap.php文件,该文件尚不存在在Symfony应用程序中。但是,我们可以使用用于phpunit的文件。

要运行将单击我们应用程序的测试,将需要浏览器。在这种情况下,将使用Google Chrome。在下面,您可以从安装浏览器的Dockerfile中看到代码段。

# Install chromedriver
RUN apt-get -y install gnupg
RUN wget -qO - https://dl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/googlechrome-linux-keyring.gpg
RUN echo "deb [arch=amd64 signed-by=/usr/share/keyrings/googlechrome-linux-keyring.gpg] http://dl.google.com/linux/chrome/deb/ stable main" | tee /etc/apt/sources.list.d/google-chrome.list
RUN apt-get -y update && apt-get -y install google-chrome-stable

Behat配置

Behat在behat.yml文件中配置。就我而言,文件看起来像这样:

default:
  suites:
    ui_posts:
      contexts:
          - App\Behat\Context\PostsContext  
          - App\Behat\Context\FixturesContext
      filters:
        tags: "@posts"
#      paths: ['%paths.base%/features/posts']
  extensions:
    FriendsOfBehat\SymfonyExtension:
      bootstrap: tests/bootstrap.php
      kernel:
        class: 'App\Kernel'
        environment: test
        debug: false      

    DMore\ChromeExtension\Behat\ServiceContainer\ChromeExtension: ~

    Behat\MinkExtension:
      browser_name: chrome
      base_url: http://localhost
      sessions:
        default:
          chrome:
            api_url: "http://localhost:9222"

我配置了一个测试套件来测试简单的CRUD博客应用程序。如果我们有多个测试套件,我们可以将其标记(在这种情况下为@posts),以确保仅在该套件的上下文中运行该特定标签的测试。另外,我们可以将路径设置为特征目录。否则,所有测试套件都将运行。

extensions部分中,我们可以配置其他附加组件,例如SymfonyExtensionMinkExtensionChromeExtension具有默认配置。

编写第一个测试

正如我之前提到的,我们在Gherkin中编写了测试场景,这是一种对业务可理解的语言。如果我们简单的CRUD,我们想测试显示,添加,编辑和删除帖子的功能。为此,我们需要在features目录中创建一个posts.feature文件。

在BDD中,没有关于如何编写用户故事的正式要求,但是有一个广泛使用的标准。首先,我们添加一个功能部分,其中我们将其命名。然后,我们描述其提供的功能及其业务价值。我们可以用三行进行此操作:“为了……”,“作为...”,“我想...”。

@posts
Feature: Managing posts
  In order to manage posts
  As a writer
  I want to add, edit, delete and display blog posts

在本节之后,我们可以添加测试方案。对我来说,第一个将能够从列表页面导航到添加帖子页面。

  Scenario: Writer goes to create post page
    Given I am on "/post/"
    When I follow "Create new"
    Then I should be on "/post/new"

正如我们所看到的,我们的场景是用人类可以理解的自然语言编写的。但是,我们如何使Behat理解我们要测试的内容呢?这是上下文所在的地方。在前面显示的配置文件中,您可能会注意到对App\Behat\Context\PostsContext类的引用。上下文用于将自然语言转化为一种可以理解的语言。

在此测试中,PostsContext扩展了MinkContext,在上述方案以及其他许多方面都具有每个步骤的实现,这使我们能够重复使用基本步骤而无需从scratch中写下它们。

use Behat\MinkExtension\Context\MinkContext;

class PostsContext extends MinkContext
{
}

MinkContext类中,我们可以找到一种方法:

/**
 * @Given /^(?:|I )am on "(?P<page>[^"]+)"$/
 * @When /^(?:|I )go to "(?P<page>[^"]+)"$/
 */
public function visit($page)
{
    $this->visitPath($page);
}

当我们在场景中的posts.feature文件中编写Given I am on "/post/"时,Behat将根据PostsContext类中包含的注释找到并调用此方法。

现在,让我们编写另一种添加帖子并验证是否显示在列表上的方案。

  Scenario: Writer wants to add a new post and see it on the posts page
    Given I am on "/post/new"
    When I fill in "Title" with "An example post"
    And I fill in "Content" with "Lorem ipsum"
    And I press "Save"
    Then I should be on "/post/"
    And On the posts list I can see post with title "An example post" 1 time

在这种情况下,我们通过填写表单,保存并检查它是否出现在列表页面上来添加新帖子。最后一步检查了页面上的标题多少次,无法在Minkextension中找到,我们必须自己写。看起来像这样:

/**
 * @Then /^On the posts list I can see post with title "([^"]*)" (\d+) times$/
 * @Then /^On the posts list I can see post with title "([^"]*)" (\d+) time$/
 * @throws ExpectationException
 */
public function onThePostsListICanSeeTitleNTimes(string $title, int $count): void
{
    $element = $this->getSession()->getPage();

    $result = $element->findAll('xpath', "//*[contains(text(), '$title')]");

    $resultCount = \count($result);

    if ($resultCount == $count && str_contains(reset($result)->getText(), $title)) {
        return;
    }

    throw new ExpectationException(sprintf('"%s" was expected to appear %d times, got %d',
        $title,
        $count,
        $resultCount
    ), $this->getSession());
}

有时可能有必要将几个步骤分为一个步骤,例如,在访问我们的应用程序时需要身份验证,这需要进入登录页面,输入用户名和密码并提交表单。我们可以通过以下方式执行此操作:

/**
 * @Given I am logged in as admin
 */
public function iAmLoggedInAsAdmin(): void
{
    $this->visit('/login');
    $this->fillField('email', 'admin@admin');
    $this->fillField('password', 'admin1');
    $this->pressButton('Sign in');
}

我们可以在执行每个测试方案之前添加上述步骤。为此,请在第一个方案之前添加posts.feature文件中的背景部分。

  Background:
    Given I am logged in as admin

整个posts.feature文件现在看起来像这样:

@posts
Feature: Managing posts
  In order to manage posts
  As a writer
  I want to add, edit, delete and display blog posts

  Background:
    Given I am logged in as admin

  Scenario: I want to go to create post page
    Given I am on "/post/"
    When I follow "Create new"
    Then I should be on "/post/new"

  Scenario: Writer wants to add a new post and see it on the posts page
    Given I am on "/post/new"
    When I fill in "Title" with "An example post"
    And I fill in "Content" with "Lorem ipsum"
    And I press "Save"
    Then I should be on "/post/"
    And On the posts list I can see post with title "An example post" 1 time

PostsContext文件看起来像这样:

use Behat\Mink\Exception\ExpectationException;
use Behat\MinkExtension\Context\MinkContext;

class PostsContext extends MinkContext
{
    /**
     * @Given I am logged in as admin
     */
    public function iAmLoggedInAsAdmin(): void
    {
        $this->visit('/login');
        $this->fillField('email', 'admin@admin');
        $this->fillField('password', 'admin1');
        $this->pressButton('Sign in');
    }

    /**
     * @Then /^On the posts list I can see post with title "([^"]*)" (\d+) times$/
     * @Then /^On the posts list I can see post with title "([^"]*)" (\d+) time$/
     * @throws ExpectationException
     */
    public function onThePostsListICanSeeTitleNTimes(string $title, int $count): void
    {
        $element = $this->getSession()->getPage();

        $result = $element->findAll('xpath', "//*[contains(text(), '$title')]");

        $resultCount = \count($result);

        if ($resultCount == $count && \str_contains(\reset($result)->getText(), $title)) {
            return;
        }

        throw new ExpectationException(\sprintf('"%s" was expected to appear %d times, got %d',
            $title,
            $count,
            $resultCount
        ), $this->getSession());
    }
}

添加固定装置

在执行每种情况之前,我们要登录。但是,可能会发现数据库中没有用户。

此外,在每次测试运行后,将添加具有相同标题和内容的另一篇文章将添加到数据库中。这将导致测试在第二次运行中失败。

在运行每种情况之前,最好清洁数据库并添加测试数据(固定装置)。我写了有关在这篇文章中使用Symfony中使用固定装置的文章:Using Fixtures In Testing Symfony Application

要加载测试数据,让我们创建一个FixturesContext,这将负责此任务。就我而言,看起来像这样:

namespace App\Behat\Context;

use Behat\Behat\Context\Context;
use Liip\TestFixturesBundle\Services\DatabaseToolCollection;

class FixturesContext implements Context
{
    public function __construct(
        private readonly DatabaseToolCollection $databaseToolCollection,
    ) {}

    /**
     * @BeforeScenario @fixtures
     */
    public function loadFixtures(): void
    {
        $this->databaseToolCollection->get()->loadFixtures([]);
    }
}

上面的类包含一种负责加载固定装置的方法。它具有@BeforeScenario注释,这意味着它将在每种情况之前执行。我们只能通过添加特定标签(在这种情况下,@fixtures)将加载固定装置限制为需要它们的方案。现在,在使用此标签的每种情况下,将加载示例数据。

上面的示例不是很灵活,即使我们不需要所有这些示例,也会每次加载相同的固定装置。取而代之的是,我们可以在FixturesContext中编写另一个步骤,以在特定方案中加载我们需要的固定装置,而无需使用@fixtures注释标记它:

    /**
     * @Given the fixture :className is appended
     */
    public function theFixtureIsAppended(string $fixtureClass): void
    {
        $this->databaseToolCollection->get()->loadFixtures([
            $fixtureClass
        ], true);
    }

然后我们可以在测试方案中使用它:

And the fixture "Fixtures\PostsData" is appended

运行测试

要运行测试,我们需要执行两个步骤。第一个是启动浏览器。我们可以通过运行以下命令来做到这一点:

google-chrome-stable --disable-gpu --headless --remote-debugging-address=0.0.0.0 --remote-debugging-port=9222 --no-sandbox

当浏览器运行时,我们可以运行测试。我们通过执行以下命令(请记住在.env文件中设置APP_ENV=test)来做到这一点)

vendor/bin/behat

在终端中,我们应该看到测试的结果。它应该看起来像这样:

Image description

概括

behat是一种以对企业和开发人员可以理解的方式启用编写测试的工具。

要开始编写简单的测试,您需要安装一些软件包并创建测试方案。 MinKextension处理了许多步骤,这当然可以使BDD的第一种方法更容易。