八、API 测试——闸门上的防护装置

在上一章中,我们修复了已确定的问题,并完成了 RESTfulWeb 服务中的其余部分。然而,为了确保质量,我们需要测试端点,而手动测试是不够的。在现实世界的项目中,我们不能重复测试每个端点,因为在现实世界中有更多的端点。因此,我们转向自动化测试。我们编写测试用例并以自动化的方式执行它们。事实上,首先编写测试用例,运行它们,然后编写代码以满足该测试的需求更为合理。这种开发方法称为TDD测试驱动开发

TDD 很好,确保我们完全按照测试用例工作。然而,在这本书中,我们没有使用 TDD,因为有很多东西需要理解,我们不想同时包含更多的东西。所以现在,当我们在 Lumen 中完成了概念、理解和编写 RESTfulWeb 服务(这对我们中的许多人来说也是新的),现在我们可以做这个缺失的事情,即测试。TDD 不是必需的,但测试是必需的。如果我们到目前为止还没有编写有利于理解其他内容的测试,那么我们应该现在就做。以下是我们将在本章中介绍的主题:

  • 自动化 API 测试的需要
  • 测试类型:
    • 单元测试
    • 集成测试
    • 功能测试
    • 验收测试
    • 我们将编写什么类型的测试?
    • 测试框架:
      • CodeCeption 简介
      • 设置和配置
      • 编写 API 测试
      • 摘要和更多资源

自动化测试的需要

正如我们前面讨论的,在现实世界中,我们不能在每个主要特性或更改之后重复测试每个端点。我们可以尝试,但我们是人,我们可能会错过这一点。更大的问题是,我们有时可能会认为我们测试了它,但却错过了它,因为没有我们测试的记录,我们无法知道。如果我们有一个单独的质量保证团队或人员,他们很可能会进行测试并保存记录。然而,对于 RESTful web 服务,这将花费更多的时间,或者 QA 人员可能会测试整个最终产品,而不是 RESTful web 服务。

就像 RESTfulWeb 服务作为产品的一个组件或一面工作一样,RESTfulWeb 服务也有更多的底层组件。不仅仅是端点,这些端点还依赖于更低级的代码。因此,为了使调试更容易,我们也为这些低级组件编写测试。此外,通过这种方式,我们可以确保这些低级别组件正常工作,并按其预期执行。在出现任何问题的情况下,我们可以运行测试,并确切地知道哪些工作不正常。

虽然开始编写测试需要时间,但从长远来看,它是好的。首先,它节省了每次更改后重复测试相同端点的时间。然后,在重构某些东西时,它会有很大帮助。它让我们知道涟漪效应在哪里,以及由于重构而受到的影响。虽然,最初编写测试需要时间,但如果我们打算做一些保留下来的东西,这是值得的。事实上,软件开发成本低于维护成本。这是因为它将被开发一次,但要维护和进行更改,它将消耗更多的时间。因此,如果我们编写了自动化测试,它将确保所有工作都完全按照要求进行,因为维护代码的人可能不是第一次编写代码的人。

没有一种类型的测试可以提供所有这些好处,但是有不同类型的测试,每种类型都有自己的好处。既然我们知道了自动化测试和编写测试用例的重要性,那么让我们了解一下不同类型的测试。

测试类型

在不同的环境中有不同类型的测试。在本例中,我们将讨论四种主要类型的自动化测试。根据我们测试的方式和内容,这些是不同的类型:

  • 单元测试
  • 集成测试
  • 功能测试
  • 验收测试

单元测试

在单元测试中,我们分别测试不同的单元。所谓单元,我们指的是非常小的独立组件。显然,组件彼此依赖,但是我们考虑一个非常小的单元。在我们的例子中,小单元是类。类是一个单元,应该有一个单独的职责,它应该是从其他类抽象出来的,并且依赖于其他类或组件的最小数量。无论如何,我们可以说在单元测试中,我们通过创建该类的对象来测试该类,而不管它是否满足所需的行为。

需要理解的一件重要事情是,在单元测试期间,我们的测试不应该触及被测试的类/代码以外的代码。如果此代码在单元测试期间与其他对象或外部对象交互,我们应该模拟这些对象,而不是与实际对象交互,以便其他对象的方法的结果不会影响我们正在测试的单元/类的结果。你可能想知道我们所说的嘲弄是什么意思?模仿是指提供一个假的对象,并按照所需的行为进行设置。

例如,如果我们使用User类进行测试,并且它依赖于Role类,那么Role类中的问题不应该让User类测试失败。为此,我们将模拟Role对象并注入User类对象,然后为User类正在使用的Role类的方法设置固定的返回值。因此接下来,它将实际调用Role类,而不依赖于它。

单元测试的好处:

  • 它会让我们知道,如果一个类没有做它打算做的事情。一段时间后,当项目处于维护阶段时,另一个开发人员将能够理解这个类打算做什么。其方法的目的是什么。所以它就像是一本由开发人员编写的类手册,开发人员知道他们为什么要编写这个类。
  • 此外,正如我们刚才讨论的,我们应该模拟对象,测试中的类依赖于这些对象,我们应该能够将模拟对象注入测试中的类的对象中。如果我们能够做到这一点,并且能够在不调用外部对象的情况下进行管理,那么只有这样,我们才能调用我们的代码,一个可测试的代码。如果我们的代码是不可测试的,那么我们就不能为它编写单元测试。因此,单元测试帮助我们使代码可测试,这实际上是更松散耦合的代码。因此,即使我们不编写测试,拥有可测试代码也是一个优势(因为它是松散耦合的,并且更易于维护)。
  • 如果我们有任何问题,它将让我们调试问题的确切位置。
  • 由于单元测试不与外部对象交互,因此它们比其他类型的测试更快。

Developers who write unit tests are considered to be better developers, as code with tests is consider cleaner code because the developer has ensured that unit level components are not tightly coupled. And units tests can be used as a manual to what a class provides and how to use it.

验收测试

验收测试与单元测试完全相反。单元测试在最低级别进行,而验收测试在最高级别进行。验收测试是最终用户如何看待产品以及最终用户如何与产品交互。对于网站,在验收测试中,我们编写从外部点击 URL 的测试。测试工具模拟浏览器或外部客户端点击 URL。事实上,一些测试工具还提供了使用 web 浏览器的选项,如 Chrome 或 Firefox。

Most of the time, these tests are written by a QA team. It is because they are the people who are there to ensure that system is working for the end user, exactly how it is intended to. Also, these tests execution is slow. And for user interfaces, sometimes there is a lot of detail to test so a separate QA team does this type of testing. However, it is just a common practice, so there can be an exception to that depending on the situation.

验收测试的好处:

  • 验收测试让您了解最终用户将如何从外部看到您的软件并与之交互
  • 它还可以让您捕获任何特定 web 浏览器中都会出现的问题,因为它使用真实的 web 浏览器来执行测试
  • 由于验收测试是为了从外部执行而编写的,所以测试哪个系统以及使用什么技术或框架来编写系统并不重要

例如,如果您使用 PHP 编写的工具编写测试用例,那么您也可以将其用于其他语言编写的系统。所以,不管开发语言是 PHP、Python、Golang 还是.Net。这是因为验收测试是在没有任何系统内部知识的情况下从外部对系统进行的。这是这四种测试中唯一一种测试系统而不考虑任何内部细节的测试。

Acceptance tests are very useful as they interact with your system with a real browser. So if some thing will not work in the specific browser, then these things can be identified. But keep in mind that with a real browser, these tests take time to execute. It is also slow if the browser simulation is used but still the browser simulation will take less time than the real browser. Note that acceptance testing is considered the slowest and most time consuming among these four types of tests.

功能测试

功能测试类似于验收测试;然而,这是从另一个角度。功能测试是关于测试功能需求的。它测试功能需求和系统外部的测试。但是,它在内部具有可见性,并且可以在测试用例中执行系统的一些代码。

与验收测试类似,它会点击 URL;但是,即使要点击 URL,它也会执行浏览器或外部客户端将在特定 URL 上执行的代码。然而,它实际上并没有从外部命中 URL。实际上,测试并没有命中 URL,它们只是模拟 URL。这是因为与验收测试不同,我们对最终用户将如何与之交互不感兴趣,相反,如果代码是从该 URL 执行的,那么我们想知道响应。

我们更感兴趣的是我们的功能需求是否得到满足,如果没有得到满足,那么问题在哪里?

功能测试的好处:

  • 通过功能测试,测试工具可以访问系统,因此它显示出比验收测试更好的错误细节。
  • 功能测试实际上不会打开浏览器或外部客户端,因此速度更快。
  • 在功能测试中,我们还可以通过测试工具直接执行系统代码,因此在某些情况下,我们可以这样做以节省测试用例编写时间,或者加快测试用例执行速度。有许多测试工具可用于此。我们将很快使用其中一个名为 CodeCeption 的。

集成测试

集成测试与单元测试非常相似,在这两种类型的测试中,我们通过让类的对象调用它们的方法来测试类。但是他们在我们测试课程的方式上是不同的。在单元测试的情况下,我们不接触被测类与之交互的其他对象。但是在集成测试中,我们想看看它们是如何协同工作的。我们让他们彼此互动。

有时,根据单元测试,一切都正常工作,但在更高级别的测试(功能测试或验收测试)中,我们通过点击 URL 根据需求进行测试。因此,在某些情况下,更高级别的测试失败了,单元测试通过了,所以为了缩小问题的范围,集成测试非常有用。因此,您可以考虑集成测试位于功能测试和单元测试之间。功能测试是关于测试功能需求,而单元测试是关于测试单个单元。因此,集成测试处于中间,测试单个单元如何协同工作;然而,它通过测试代码中的小组件进行测试,但也允许它们彼此交互。

Some developers write integration tests and call it a unit test. Actually, integration tests just let code under the test to interact with other objects, so it can test how those classes work when they interact with system components. So, some people write integration tests if the code under test is very simple that needs to be tested while interacting with the system. However, it is not necessary to only write one unit test or integration test, you can write both if you have time.

集成测试的好处:

  • 当单元测试不足以捕获 bug,并且高级测试不断告诉您有问题时,集成测试非常有用。因此,集成测试有助于调试问题。
  • 由于集成测试的性质,它在重构的情况下非常有帮助,同时准确地告诉您新更改会影响什么。

我们将进行什么类型的测试?

每种类型的测试都有其自身的重要性,尤其是单元测试。然而,我们将主要进行 API 测试,测试我们的 RESTfulWeb 服务端点。这并不意味着单元测试不那么重要,只是我们在本章主要关注 API 测试,因为这本书关注的是 RESTfulWeb 服务。事实上,测试是一个大话题,您将能够看到关于测试和 TDD 的完整书籍。

Nowadays, BDD (Behavior-driven Development) is a more popular term. It is not completely different than TDD. It is just a different way of stating test cases. In fact, in BDD, there are no test cases instead there are specs. They serve the same purpose but BDD has a more friendly way to address the problem that is by stating specs and implementing them and that's how TDD works. So TDD and BDD are not different, just a different way to address the same problem.

我们可以通过功能测试和验收测试两种方式执行 API 测试。然而,将 API 测试编写为功能测试更有意义。因为功能测试速度很快,而且能够洞察我们的代码库。这也更有意义,因为验收测试是针对最终用户的,而最终用户不使用 API。最终用户使用用户界面。

测试框架

就像我们有编写软件的框架一样,我们也有编写测试用例的框架。由于我们是 PHP 开发人员,我们将使用一个用 PHP 编写的测试框架,或者我们可以用 PHP 编写测试用例,这样我们就可以轻松地使用它。

首先,我们可以在 PHP 中使用不同的测试框架,无论我们在应用程序开发中使用哪个开发框架。然而,也有测试工具,来与 Laravel 和流明。我们也可以使用它们来编写测试用例。事实上,为 Lumen 编写测试用例会更容易,但它将是 Lumen 和 Laravel 特定的。在这里,我们将使用一个框架,您可以在 Lumen 和 Laravel 生态系统之外以及任何 PHP 项目中使用该框架,无论您使用哪个开发框架来编写代码。

PHP 中有许多不同的测试框架,那么我们将如何决定使用哪一个呢?我们将使用一个框架,它的级别不会太低,我们需要自己编写所有的东西,因为我们不准备编写单元测试,而是编写功能测试,所以我们选择了一个高级框架。一个著名的单元测试框架是 PHPUnit:https://phpunit.de/

BDD行为驱动开发风格)中还有另一个单元测试框架,名为 PHPSpec:http://www.phpspec.net 如果你想学习或编写单元测试,PHPSpec 是很棒的。然而,这里我们将使用一个对功能测试和单元测试都很好的框架。虽然,我们不是在编写单元测试,但是我们想考虑一个框架,您也可以稍后使用它来进行单元测试。我选择的框架是 CodeCeption:http://codeception.com/ 因为它似乎非常擅长 API 测试。另一个 BDD 风格的替代品可能是 Behat:http://behat.org/en/latest/ 。这是一个高级测试框架,但如果我们正在进行验收测试,或者如果我们有一个单独的 QA 团队,他们将用小黄瓜语法(编写许多测试用例,这会更好 https://github.com/cucumber/cucumber/wiki/Gherkin 非常接近自然语言。然而,对于 PHP 开发人员来说,Behat 和 Gherkin 可能有更多的学习曲线,而 CodeCeption 只是 PHP(尽管它可以在需要时使用 Gherkin),因此,由于许多读者对编写测试用例还不熟悉,我将保持简单和接近 PHP。然而,这里有一个详细的比较,我在 2 年前写的,用于 API 测试的框架,它是旧的,但对大多数东西仍然有效。如果您感兴趣,那么您可以看看http://haafiz.me/programming/api-testing-selecting-testing-framework

联合概念介绍

CodeCeption 是用 PHP 编写的,由 PHPUnit 提供支持。CodeCeption 声称CodeCeption 使用 PHPUnit 作为运行测试的后端。因此,任何 PHPUnit 测试都可以添加到 CodeCeption 测试套件中,然后执行

验收测试以外的测试需要一个测试框架,该框架与测试下的代码有一些洞察力或联系。如果我们使用的是开发框架,那么测试框架应该为该框架提供某种模块或插件。CodeCeption 擅长于此。它为不同的框架和 CMS 提供模块,如 Symfony、Joomla、Laravel、Lumen、Yii2、WordPress 和 Zend 框架。只是想让你知道,这些只是一些框架。CodeCeption 支持许多其他模块,这些模块在不同情况下可能会有所帮助。

设置和理解结构

安装 CodeCeption 有不同的方法,但我更喜欢 composer,它是将不同的 PHP 工具作为 PHP 包安装的标准方法。让我们安装它:

composer require "codeception/codeception" --dev

如您所见,我们正在使用--dev标志,以便在composer.json文件的require-dev块中添加 CodeCeption。因此,在生产环境中,当您运行composer install --no-dev时不会安装它,它不会在require-dev块中安装依赖项。如果出现混淆,请检查与作曲家相关的章节,即第 5 章加载并与作曲家解决,一个进化的

安装之后,我们需要将其设置为编写测试用例,并使其成为我们项目的一部分。安装只意味着它现在在vendors目录中,现在我们可以通过 composer 执行 CodeCeption 命令。

要进行设置,我们需要运行 CodeCeption 引导命令:

composer exec codecept bootstrap

codecept是 CodeCeption 在vendor/bin目录中的可执行文件,所以我们通过 composer 执行它,并给它一个参数来运行bootstrap命令。因此,执行此命令后,将在项目中添加一些文件和目录。

以下是它们的列表:

codeception.yml

tests/_data/
tests/_output/

tests/acceptance/
tests/acceptance.suite.yml
tests/_support/AcceptanceTester.php
tests/_support/Helper/Acceptance.php
tests/_support/_generated/AcceptanceTesterActions.php

tests/functional/
tests/functional.suite.yml
tests/_support/FunctionalTester.php
tests/_support/Helper/Functional.php
tests/_support/_generated/FunctionalTesterActions.php

tests/unit/
tests/unit.suite.yml
tests/_support/UnitTester.php
tests/_support/Helper/Unit.php
tests/_support/_generated/UnitTesterActions.php

如果您查看上面提到的文件列表,您会注意到我们在根目录下有一个文件,codeception.yml,它包含 CodeCeption 测试的基本配置。它告诉我们有关路径和基本设置的信息。如果您阅读此文件,您将能够轻松理解它。如果你不明白什么,现在就忽略它。除此文件外,所有内容都在tests/目录中。这些东西对我们来说更重要。

首先,tests/目录中有两个空目录。_output包含失败情况下的测试用例输出,_data包含数据库查询,以防我们要在运行测试之前和之后设置默认数据库进行测试。

除此之外,您可以看到有三组文件具有相似的文件,但测试类型不同。在 CodeCeption 中,我们将这些组称为测试套件。因此,默认情况下,CodeCeption 附带三个测试套件。验收、功能和单元套件。所有这三个套件都包含四个文件和一个空目录。那么,让我们看看这些文件和目录的用途。

测试/{suite name}/

此处,{suite-name}将由套房的实际名称替换;与单元套房一样,它将是tests/unit/

无论如何,这个目录将用于保存我们将要编写的测试用例。

tests/{suite name}.suite.yml

此文件是特定于该套件的配置文件。它包含此特定套件的ActorName。Actor 只是一个具有特定设置和功能的角色。根据参与者设置,它以不同的方式运行测试。设置包括模块的配置和启用模块。

tests/-support/-generated/{suite name}TesterActions.php

此文件是根据tests/{suite-name}.suite.yml中的设置自动生成的文件。

tests/_support/{suite name}Tester.php

此文件使用_generated目录中生成的文件,如果需要,开发人员可以对其进行更多自定义。但是,通常不需要。

tests/_support/Helper/{suite name}.php

套件中的此文件是帮助文件。在这里,您可以在类中添加更多方法,并在测试用例中使用这些方法。就像其他代码具有库和帮助器一样,测试用例代码也可以在该套件的帮助器类中具有帮助器方法。

请注意,如果需要不同的帮助器类,可以添加更多文件。

创建 API 套件

在我们的例子中,我们需要单元测试和 API 测试。尽管我们可以使用 functional tests suite 进行 API 测试,因为这些测试处于功能测试级别,但为了清晰和理解,我们可以通过以下命令创建单独的 API 套件:

composer exec codecept g:suite api

这里,在这个命令中,g是 generate 的缩写,它将生成一个 API 套件。api只是另一个套件的名称,此命令创建了以下文件和目录:

tests/api/
tests/api.suite.yml
tests/_support/ApiTester.php
tests/_support/Helper/Api.php
tests/_support/_generated/ApiTesterActions.php

api.suite.yml文件将具有基本设置,没有太多细节。这是因为api.suite.yml文件将有基本设置,没有太多细节。因为api这里只是一个名字。你甚至可以说:

composer exec codecept g:suite abc

它应该创建具有相同文件结构和设置的abc套件。因此,我们的 API 套件只是另一个测试套件,为了清晰和理解,我们单独创建了它。

配置 API 套件

API 需要 REST 客户端来获取 RESTful web 服务端点。除此之外,它取决于流明。我们之所以说 Lumen,是因为它将与我们的代码集成,因为我们正在编写功能级测试,而不是验收测试。因此,我们的测试框架应该具有洞察力并与流明交互。我们在配置中还需要什么?我们需要设置测试.env文件。因此,我们的配置文件如下所示:

class_name: ApiTester
modules:
    enabled:
        - REST:
            url: /api/v1
            depends: Lumen
        - \Helper\Api
    config:
        - Lumen:
            environment_file: .env.testing

在继续之前,请注意,我们在这里的config/Lumen下指定了一个不同的环境文件选项,即environment_file: .env.testing。所以,如果我们在这里指定了.env.testing,那么我们应该有一个.env.testing。没什么大不了的,只是复制粘贴你的.env文件。从命令行执行以下操作:

cp .env .env.testing

更改数据库凭据,使其指向不同的数据库,并具有当前数据库架构和数据的副本,您要基于该副本编写测试用例。虽然,我们在 Laravel/Lumen 中进行测试时所做的与数据库相关的工作将被回滚,并且不会影响我们的实际数据库,因此相同的数据库在开发中也可以使用。但是,事实上,不建议在登台时禁止使用相同的数据库进行测试;因此,最好从一开始就保留不同的数据库和配置。

We don't run tests in production. We don't even install tests related tool in production, as you can see we installed CodeCeption with the --dev flag. So, when our code is in production and we want to deploy a new feature, our test cases are run on a different server and then the code is deployed on the production server. There are several CI (Continuous Integration) tools available for that.

编写测试用例

现在,是编写测试用例的时候了。第一件事是我们如何决定我们应该测试什么。我们应该先测试每个端点,然后测试每个类吗?

首先要了解的是,我们应该只测试我们编写的代码。我所说的我们,是指我们团队中的某个人。我们不打算测试第三方代码、框架代码或包。此外,我们不想测试每个类和每个方法。在理想情况下,我们可以测试每个小函数的细节,但它也有缺点。首先,在现实世界中,我们没有时间这么做。我们打算测试大部分部件,但不是所有部件。另一个原因是,我们正在编写的所有测试也是一种负担。我们还需要长期维护这些测试。所以,我们对有意义的部分进行单元测试;它实际上是在做一些复杂的事情。如果你有一个简单的函数,它调用另一个函数并返回结果,那么我不认为这段代码应该有自己的测试。

另一件事是,如果我们在做单元测试和 API 测试,那么我们应该从哪里开始编写测试呢?我们应该先为所有端点编写测试,然后为所有类编写测试,或者我们将做相反的事情,所以我们将首先测试所有类,然后测试所有端点?我们将如何做到这一点?我们显然打算测试我们的端点。我们还打算在这些端点下测试代码。这是不同的人可以做的不同的事情,但我和许多其他人,我已经看到,开始同时编写 API 测试和单元测试。我更喜欢编写 API 测试,并继续为一个资源编写它们。之后,我们将进行控制器的单元测试。在我们的例子中,除了从雄辩或关系中继承的东西之外,模型中没有太多东西。在资源的 API 测试期间,如果我们需要更多细节来修复 bug,那么我们可以开始为该类编写单元测试。但没有硬性规定。这只是偏好的问题。

post 资源的 API 测试

我们可以用结构化的方法编写测试用例,也可以在类中编写。这两种方法都很好,我推荐这门课,所以你们可以利用你们的 OOP 概念,若你们愿意的话。因此,让我们为其创建一个文件:

composer exec codecept generate:cest api CreatePost

这将在tests/api/CreatePostCest.php中创建一个类,其内容类似于:

<?php

class CreatePostCest
{
    public function _before(ApiTester $I)
    {
    }

    public function _after(ApiTester $I)
    {
    }

    // tests
    public function tryToTest(ApiTester $I)
    {
    }
}

这里有_before()方法,您可以编写任何代码,在测试用例之前执行这些代码,_after()方法在测试用例之后执行。下一个方法就是我们要修改的一个例子。

在这里,我们将编写两种类型的测试。一个是在尝试创建未登录的帖子时返回未经授权的错误,另一个是在登录后创建帖子,应该成功。

在写这篇文章之前,让我们先建立数据库工厂来获取文章的随机内容,以便在测试期间使用它。我们修改一下app/database/factories/ModelFactory.php,看起来是这样的:

<?php

/*
|--------------------------------------------------------------------------
| Model Factories
|--------------------------------------------------------------------------
|
| Here you may define all of your model factories. Model factories give
| you a convenient way to create models for testing and seeding your
| database. Just tell the factory how a default model should look.
|
*/

$factory->define(App\User::class, function (Faker\Generator $faker) {
    return [
        'name' => $faker->name,
        'email' => $faker->email,
    ];
});

$factory->define(App\Post::class, function (Faker\Generator $faker) {
    return [
        'title' => $faker->name,
        'content' => $faker->text(),
        'status' => $faker->randomElement(['draft', 'published']),
    ]; });

突出显示的代码是我刚刚添加的。因此,我们告诉它返回通过Faker\Generator类对象生成的参数、标题、内容和状态数组。因此,根据我们在这里定义不同字段的方式,我们可以通过ModelFactory为 Post 用户生成随机内容,这样数据将是随机的、动态的,而不是静态的内容。在测试过程中,最好在测试用例中使用随机数据来正确地测试它。

好的,现在让我们在CreatePostCest.php文件中编写测试用例,下面是我们要编写的函数:

// tests if it let unauthorized user to create post
public function tryToCreatePostWithoutLogin(ApiTester $I)
{
    //This will be in console like a comment but effect nothing
    $I->wantTo("Send sending Post Data to create Post to test if it let it created without login?");

    //get random data generated through ModelFactory
    $postData = factory(App\Post::class, 1)->make();

    //Send Post data through Post method
    $I->sendPost("/posts", $postData);

    //This one will also be like a comment in console
    $I->expect("To receive a unauthorized error resposne");

    //Response code of unauthorized request should be 401
    $I->seeResponseCodeIs(401);
}

正如您所看到的,注释解释了那里的一切,因此没有必要明确地告诉任何事情,除了我们使用了sendPost()方法来发送 Post 请求,对于不同的 HTTP 方法,我们也可以说sendGet()sendPut(),等等。所以,现在我们需要运行这个测试。

我们可以通过:

composer exec codecept run api

它不会在控制台上为我们提供清晰的输出。我们可以添加-v-vv-vvv使输出越来越详细,但在这个命令中,它会使 composer exec 相关信息越来越详细。因此,让我们将其执行为:

vendor/bin/codecept run api

请随意添加-v最多三次,以获得越来越详细的输出:

vendor/bin/codecept run api -vv

我们可以在控制台中创建路径vendor/bin/codecept的别名,在该会话期间,我们可以使用如下速记:

alias codecept=vendor/bin/codecept
codecept run api -vv

因此,执行它,您将在控制台中看到许多细节。根据需要使用-v-vv-vvv。现在,让我们将其执行为:

codecept run api -v

在我们的情况下,我们的第一次测试应该已经通过了。现在,我们应该编写第二个测试用例,即在登录后创建一个 post。它涉及到更多我们需要了解的事情。因此,让我们先看看这个测试用例的代码,然后我们将回顾它:

// tests if it let unauthorized user to create post
public function tryToCreatePostAfterLogin(ApiTester $I)
{
    //This will be in console like a comment but effect nothing
 $I->wantTo("Sending Post Data to create Post after login");
 $user = App\User::first();
    $token = JWTAuth::fromUser($user);

    //get random data generated through ModelFactory
    $postData = factory(App\Post::class, 1)->make()->first()->toArray();

    //Send Post data through Post method
 $I->amBearerAuthenticated($token);
    $I->sendPost("/posts", $postData);

    //This one will also be like a comment in console
    $I->expect("To receive a unauthorized error resposne");

    //Response code of unauthorized request should be 401
 $I->seeResponseCodeIs(200);

}

如果您查看这个测试用例,您会发现除了一些语句之外,它与前面的测试用例代码非常相似。因此,我用粗体强调了这些陈述。首先,由于测试用例不同,我们的wantTo()论点也不同。

然后,我们从数据库中获取第一个用户,并基于用户对象生成一个令牌。在这里,我们调用应用程序代码是因为我们使用的是api.suite.yml文件中配置的流明模块。然后,我们将 CodeCeption 的$I->amBearerAuthenticated($token)方法与我们生成的$token一起使用。这意味着我们正在发送一个有效的令牌,因此服务器会将其视为登录用户。这个时间响应代码是 200,所以说$I->seeResponseCodeIs(200),我们告诉它应该有 200 个响应代码,否则测试应该失败。这就是这些代码所做的。

实际上,可以有更多类似的测试用例,比如在请求不完整的情况下返回400 Bad Request响应的测试。

运行测试后,您将在控制台的末尾看到:

OK (2 tests, 2 assertions)

这表明我们主张两件事。断言意味着陈述我们希望真实的期望或事实。简单地说,这是我们正在检查的回应。比如,现在我们只测试响应代码。但是在现实世界中,我们用更多的测试用例来测试整个响应。CodeCeption 还为我们提供了测试这些东西的功能。因此,让我们用更多断言修改当前的两个测试用例。下面是我们将要测试的另外两件事情:

  • 将断言我们得到 JSON 作为响应。
  • 将断言我们根据输入得到正确的响应数据。

下面是我们的代码:

<?php

use Tymon\JWTAuth\Facades\JWTAuth;

class CreatePostCest
{
    public function _before(ApiTester $I)
    {
    }

    public function _after(ApiTester $I)
    {
    }

    // tests if it let unauthorized user to create post
    public function tryToCreatePostWithoutLogin(ApiTester $I)
    {
        //This will be in console like a comment but effect nothing
        $I->wantTo("Send sending Post Data to create Post to test if it let it created without login?");

        //get random data generated through ModelFactory
        $postData = factory(App\Post::class, 1)->make()->first()->toArray();

        //Send Post data through Post method
        $I->sendPost("/posts", $postData);

        //This one will also be like a comment in console
        $I->expect("To receive a unauthorized error resposne");

        //Response code of unauthorized request should be 401
        $I->seeResponseCodeIs(401);
     // Response should be in JSON format
        $I->seeResponseIsJson();
    }

    // tests if it let unauthorized user to create post
    public function tryToCreatePostAfterLogin(ApiTester $I)
    {
        //This will be in console like a comment but effect nothing
        $I->wantTo("Sending Post Data to create Post after login");

        $user = App\User::first();
        $token = JWTAuth::fromUser($user);

        //get random data generated through ModelFactory
        $postData = factory(App\Post::class, 1)->make()->first()->toArray();

        //Send Post data through Post method
        $I->amBearerAuthenticated($token);
        $I->sendPost("/posts", $postData);

        //This one will also be like a comment in console
        $I->expect("To receive a 200 response");

        //Response code of unauthorized request should be 200
        $I->seeResponseCodeIs(200);
        // Response should be in JSON format
 $I->seeResponseIsJson();        //Response should contain data that matches with request $I->seeResponseContainsJson($postData);
    }
}

正如您在前面的代码片段中看到的突出显示的文本一样,我们又添加了三个断言,您可以看到它是多么简单。实际上,当我们不知道一个对象可以有什么值时,检查响应中的某些内容可能会很棘手。例如,如果我们想请求并查看帖子列表,该怎么办。那么,当我们不知道这些值时,我们将如何断言?在这种情况下,您可以使用基于 JSON 路径的断言,记录在这里:http://codeception.com/docs/modules/REST#seeResponseJsonMatchesJsonPath 。 您可以这样使用它:

$I->seeResponseJsonMatchesJsonPath('$.data[*].title');

这也是您在响应中看到的,但甚至有一种方法可以测试数据库中是否也存在该记录。你应该自己试试。您可以在这里找到其文档:http://codeception.com/docs/modules/Db#seeInDatabase

其他测试用例

还有更多与其他 Post 操作(端点)相关的测试用例。然而,编写测试用例的方法将保持不变。因此,我跳过了测试,这样您就可以自己编写这些测试用例了。然而,作为一个提示,以下是一些您应该为练习编写的测试用例:

tryToDeletePostWithWrongId()  and it should return 404 response.

tryToDeletePostWithCorrectId() and it should return 200 with JSON we set there in PostController delete() method.

tryToDeletePostWithIdBelongsToOtherUserPost() it should return 403 Forbidden response because a user is only allowed to delete his/her own Post.

tryToDeletePostWithoutLogin() it should return 401 Unauthenticated because only a logged in user is allowed to delete his/her Post.

然后与更新帖子相关:

tryToUpdatePostWithWrongId()  and it should return 404 response.

tryToUpdatePostWithCorrectId() and it should return 200 with JSON having that Post data.

tryToUpdatePostWithIdBelongsToOtherUserPost() it should return 403 Forbidden response because a user is only allowed to update his/her own Post.
tryToUpdatePostWithoutLogin() and it should return 401 unauthorized.

然后与上市后相关:

tryToListPosts() and it should return 200 response code with Post list having data and meta indices in JSON.

然后与获得单个帖子相关:

tryToSeePostWithId() and it should return 200 response code with Post data in JSON.

tryToSeePostWithInvalidId() and it should return 404 Not Found error.

因此,我强烈建议您编写这些测试用例。如果您需要更多示例,或者希望看到测试认证相关端点的示例,那么您可以在这里找到一些示例,以便更好地理解:https://github.com/Haafiz/REST-API-for-basic-RPG/tree/master/tests/api

有关 CodeCeption 的更多信息,请参阅位于的 CodeCeption 文档 http://codeception.com/

总结

在本章中,我们学习了测试类型、自动化测试的重要性,并为 RESTfulWeb 服务端点编写了 API 测试。我在这里再次想说的一件事是,我们只编写 API 测试来保持我们对主题的关注,但单元测试的重要性同样重要。然而,测试是一个巨大的主题,单元测试有其自身的复杂性,所以不能在这一章中讨论。

更多资源

如果您想了解有关 PHP 自动测试的更多信息,请参阅以下一些重要资源:

测试驱动的 Laravel(Adam Wathan 的视频课程)https://adamwathan.me/test-driven-Laravel/ ,但这主要是针对拉雷维尔。但是,这仍然会教会你一些重要的事情。

类似地,在中也有被解码的拉威尔测试(Jeffrey Way 的一本旧书)https://leanpub.com/Laravel-testing-decoded

同样,这是一本专门针对拉威尔的书,但总的来说,它教会了你很多东西。杰弗里·韦(Jeffrey Way)即将出版的新书《测试 PHP:是一本关于 PHP 测试的书 https://leanpub.com/testingphp

前面提到的关于 PHP 的书还没有完成,因此您可以从 Jeffrey Way 的精彩的屏幕播放测试视频中学习:https://laracasts.com/skills/testing 事实上,Laracasts 不仅适用于测试,还适用于学习整个 PHP 和 Laravel。

无论你选择哪种来源,重要的是你要练习。开发和测试也是如此。事实上,如果你以前没有做过测试,那么练习测试就更重要了。首先,你会发现这有点让人难以承受,但这是值得的。