原文:
zh.annas-archive.org/md5/0741f77c4686cccb7feaca7feda46f8b
译者:飞龙
第八章:API 测试-守卫在大门上
在上一章中,我们解决了我们识别出的问题,并完成了 RESTful web 服务中剩下的事情。然而,为了确保质量,我们需要测试我们的端点,手动测试是不够的。在现实世界的项目中,我们无法重复测试每个端点,因为在现实世界中有更多的端点。因此,我们转向自动化测试。我们编写测试用例并以自动化的方式执行它们。事实上,首先编写测试用例,运行它们,然后编写代码来满足该测试的要求更有意义。这种开发方法称为 TDD(测试驱动开发)。
TDD 是好的,并确保我们按照我们的测试用例进行工作。然而,在这本书中,我们没有使用 TDD,因为有很多东西要理解,我们不想同时包括一件事。所以现在,当我们完成了概念、理解和在 Lumen 中编写 RESTful web 服务(对我们许多人来说也是新的)时,现在我们可以做这个缺失的事情,也就是测试。TDD 并非必不可少,但测试是。如果我们迄今为止没有为了理解其他东西而编写测试,那么现在我们应该这样做。以下是本章将涵盖的主题:
-
自动化 API 测试的需求
-
测试类型:
-
单元测试
-
集成测试
-
功能测试
-
验收测试
-
我们将编写什么类型的测试?
-
测试框架:
-
介绍 CodeCeption
-
设置和配置
-
编写 API 测试
-
总结和更多资源
自动化测试的需求
正如我们之前讨论过的,在现实世界中,我们无法在每个主要功能或更改后重复测试每个端点。我们可以尝试,但我们是人类,我们可能会错过。更大的问题是,我们有时可能会认为我们已经测试过了,但却错过了,因为没有记录我们测试过什么,我们无法知道。如果我们有一个单独的质量保证团队或人员,他们很可能会测试并记录下来。然而,在 RESTful web 服务的情况下,这将占用更多的时间,或者可能的情况是 QA 人员将作为一个整体测试最终产品,而不是 RESTful web 服务。
就像 RESTful web 服务作为产品的一个组件或一个方面一样,RESTful web 服务还有更多低级组件。不仅仅是端点,而是这些端点依赖于更低级的代码。因此,为了使我们的调试更容易,我们也为这些低级组件编写测试。此外,这样我们可以确保这些低级组件运行良好,并且按照其意图进行操作。在出现任何问题的情况下,我们可以运行测试,并确切地知道哪些地方出了问题。
尽管一开始编写测试需要时间,但从长远来看是有好处的。首先,它节省了在每次更改后重复测试相同端点的时间。然后,在重构某些东西时,它在很大程度上有所帮助。它让我们知道涟漪效应在哪里,以及由于我们的重构受到了什么影响。尽管一开始编写测试需要时间,但如果我们打算做一些长期存在的东西,那么它是值得的。事实上,软件开发成本比维护成本要低。这是因为它只会开发一次,但要维护和做更改,将会消耗更多的时间。因此,如果我们编写了自动化测试,它将确保一切都按照要求正常工作,因为维护代码的人很可能不是第一次编写代码的人。
没有一种测试可以提供所有这些好处,但有不同类型的测试,每种测试都有其自己的好处。既然我们知道了自动化测试和编写测试用例的重要性,让我们了解一下不同类型的测试。
测试类型
在不同的上下文中有不同类型的测试。在我们的情况下,我们将讨论四种主要类型的自动化测试。这些是不同类型的测试,基于我们测试的方式和内容:
-
单元测试
-
集成测试
-
功能测试
-
验收测试
单元测试
在单元测试中,我们分别测试不同的单元。所谓的单元,指的是非常小的独立组件。显然,组件彼此依赖,但我们考虑的是一个非常小的单元。在我们的情况下,这个小单元是类。这个类是一个应该具有单一职责的单元,它应该与其他类或组件抽象,并且依赖于最少数量的其他类或组件。无论如何,我们可以说在单元测试中,我们通过创建该类的对象来测试类,而不管它是否满足所需的行为。
一个重要的事情要理解的是,在单元测试期间,我们的测试不应该触及除了测试类/代码之外的代码。如果在单元测试期间,这段代码与其他对象或外部内容进行交互,我们应该模拟这些对象,而不是与实际对象进行交互,以便其他对象的方法的结果不会影响我们正在测试的单元/类的结果。你可能想知道我们所说的模拟是什么意思?模拟意味着提供一个虚假对象,并根据所需的行为设置它。
例如,如果我们正在使用User
类进行测试,并且它依赖于Role
类,那么Role
类中的问题不应该导致User
类的测试失败。为了实现这一点,我们将模拟Role
对象并将其注入User
类对象中,然后为User
类使用的Role
类的方法设置固定的返回值。因此,接下来,它将实际调用Role
类,并且不会依赖于它。
单元测试的好处:
-
它将让我们知道一个类是否没有达到其意图。一段时间后,当项目处于维护阶段时,另一个开发人员将能够理解这个类的意图。它的方法的意图是什么。因此,它将像是由知道为什么编写了该类的开发人员编写的类的手册。
-
另外,正如我们刚刚讨论的,我们应该模拟对象,测试类依赖的对象,我们应该能够将模拟对象注入到测试类的对象中。如果我们能够做到这一点,并且能够在不调用外部对象的情况下进行管理,那么我们才能称我们的代码为可测试代码。如果我们的代码不可测试,那么我们就无法为其编写单元测试。因此,单元测试帮助我们使我们的代码可测试,这实际上是更松散耦合的代码。因此,具有可测试代码是一种优势(因为它是松散耦合的,更易于维护),即使我们不编写测试。
-
如果我们遇到任何问题,它将让我们调试出问题所在。
-
由于单元测试不与外部对象交互,因此它们比其他一些测试类型更快。
编写单元测试的开发人员被认为是更好的开发人员,因为带有测试的代码被认为是更干净的代码,因为开发人员已经确保了单元级组件不是紧密耦合的。单元测试可以作为类提供的手册以及如何使用它。
验收测试
验收测试是单元测试的完全相反。单元测试是在最低级别进行的,而验收测试是在最高级别进行的。验收测试是最终用户将如何看待产品以及最终用户将如何与产品进行交互的方式。在网站的情况下,在验收测试中,我们编写测试以从外部访问 URL。测试工具模拟浏览器或外部客户端来访问 URL。事实上,一些测试工具还提供使用 Web 浏览器(如 Chrome 或 Firefox)的选项。
大多数情况下,这些测试是由 QA 团队编写的。这是因为他们是确保系统对最终用户正常工作的人,正如预期的那样。此外,这些测试的执行速度很慢。对于用户界面,有时需要测试很多细节,因此需要一个单独的 QA 团队来进行此类测试。但这只是一个常见的做法,因此根据情况可能会有例外。
验收测试的好处:
-
验收测试让您看到最终用户如何从外部看到和与您的软件交互
-
它还可以让您捕捉将在任何特定的 Web 浏览器中发生的问题,因为它使用真实的 Web 浏览器来执行测试
-
由于验收测试是为了从外部执行而编写的,所以无论您要测试哪个系统以及用于编写系统的技术或框架是什么都无关紧要
例如,如果您正在使用 PHP 编写测试用例的工具,那么您也可以将其用于其他语言编写的系统。因此,开发语言是 PHP、Python、Golang 还是.Net 都无关紧要。这是因为验收测试从外部击中系统,而不需要了解系统的任何内部知识。它是这四种测试中唯一一个在不考虑任何内部细节的情况下测试系统的测试类型。
验收测试非常有用,因为它们与您的系统使用真实浏览器进行交互。因此,如果某些内容在特定浏览器中无法正常工作,那么这些问题可以被识别出来。但请记住,使用真实浏览器,这些测试需要时间来执行。如果使用浏览器模拟,速度也会很慢,但仍然比真实浏览器快。请注意,验收测试被认为是这四种测试中最慢和最耗时的。
功能测试
功能测试与验收测试类似;但是,它是从不同的角度。功能测试是关于测试功能需求。它测试功能需求并从系统外部进行测试。但是,它具有内部可见性,并且可以在测试用例中执行系统的一些代码。
与验收测试类似,它击中 URL;然而,即使要击中 URL,它也会执行浏览器或外部客户端将在特定 URL 上执行的代码。但实际上并不是从外部击中 URL。测试实际上并没有击中 URL,它只是模拟了它。这是因为与验收测试不同,我们对最终用户如何与之交互并不感兴趣,而是如果代码从该 URL 执行,我们想要知道响应。
我们更感兴趣的是我们的功能需求是否得到满足,如果没有得到满足,问题出在哪里?
功能测试的好处:
-
通过功能测试,测试工具可以访问系统,因此显示的错误细节比验收测试更好。
-
功能测试实际上不会打开浏览器或外部客户端,因此速度更快。
-
在功能测试中,我们还可以通过测试工具直接执行系统代码,因此在某些情况下,我们可以这样做来节省测试用例编写时间,或者使测试用例执行更快。有许多可用的测试工具。我们将很快使用其中一个名为 CodeCeption。
集成测试
集成测试在某种程度上与单元测试非常相似,因为在这两种测试中,我们通过使它们的对象调用它们的方法来测试类。但它们在测试类的方式上有所不同。在单元测试的情况下,我们不会触及我们要测试的类与之交互的其他对象。但在集成测试中,我们想要看到它们如何一起工作。我们让它们相互交互。
有时,一切都按照单元测试的要求正常运行,但在更高级别的测试(功能测试或验收测试)中却不正常,我们会根据需求通过访问 URL 进行测试。因此,在某些情况下,高级别测试失败,而单元测试通过,为了缩小问题的范围,集成测试非常有用。因此,可以认为集成测试处于功能测试和单元测试之间。功能测试是关于测试功能需求,而单元测试是关于测试单个单元。因此,集成测试处于两者之间,它测试这些单个单元如何一起工作;然而,它通过测试代码中的小组件进行测试,同时也让它们相互交互。
一些开发人员编写集成测试并将其称为单元测试。实际上,集成测试只是让测试中的代码与其他对象进行交互,因此它可以测试这些类在与系统组件交互时的工作方式。因此,如果测试中的代码非常简单且需要与系统交互进行测试,有些人会编写集成测试。然而,并不一定只编写一个单元测试或集成测试,如果有时间,可以同时编写两者。
集成测试的好处:
-
当单元测试不足以捕捉错误,高级别测试不断告诉您有问题时,集成测试非常有用,可以帮助调试问题。
-
由于集成测试的性质,在重构时非常有帮助,可以告诉您新更改受到了什么影响。
我们将进行哪种类型的测试?
每种类型的测试都有其重要性,尤其是单元测试。然而,我们主要进行 API 测试,将测试我们的 RESTful Web 服务端点。这并不意味着单元测试不重要,只是我们在本章主要关注 API 测试,因为本书侧重于 RESTful Web 服务。实际上,测试是一个大课题,你将能够看到关于测试和 TDD 的完整书籍。
如今,“BDD”(行为驱动开发)是一个更流行的术语。它与 TDD 并没有完全不同。它只是陈述测试用例的一种不同方式。实际上,在 BDD 中,没有测试用例,而是规范。它们具有相同的目的,但 BDD 以更友好的方式解决问题,即通过陈述规范并实现它们,这就是 TDD 的工作方式。因此,TDD 和 BDD 并没有不同,只是解决同一个问题的不同方式。
我们可以以功能测试和验收测试的方式进行 API 测试。然而,将 API 测试编写为功能测试更有意义。因为功能测试将更快,并且对我们的代码库有洞察力。这也更有意义,因为验收测试是为最终用户而设计的,而最终用户不使用 API。最终用户使用用户界面。
测试框架
就像我们有用于编写软件的框架一样,我们也有用于编写测试用例的框架。由于我们是 PHP 开发人员,我们将使用一个用 PHP 编写的测试框架,或者我们可以在其中用 PHP 编写测试用例,以便我们可以轻松使用它。
首先,无论我们用于应用开发的开发框架是什么,我们都可以在 PHP 中使用不同的测试框架。然而,Laravel 和 Lumen 也带有测试工具。我们也可以使用它们来编写测试用例。实际上,为 Lumen 编写测试用例会更容易,但它将是特定于 Lumen 和 Laravel 的。在这里,我们将使用一个框架,您将能够在 Lumen 和 Laravel 生态系统之外以及任何 PHP 项目中使用它,无论您使用哪个开发框架来编写代码。
PHP 中有许多不同的测试框架,那么我们如何决定使用哪一个?我们将使用一个不太低级的框架,因为我们不打算编写单元测试,而是功能测试,所以我们选择了一个稍微高级的框架。一个著名的单元测试框架是 PHPUnit:phpunit.de/
。
还有另一个以BDD(行为驱动开发)风格命名的单元测试框架,名为 PHPSpec:www.phpspec.net
,如果您想学习或编写单元测试,PHPSpec 也很棒。然而,在这里,我们将使用一个既适用于功能测试又适用于单元测试的框架。尽管我们不写单元测试,但我们希望考虑一个稍后也可以用于单元测试的框架。我选择的框架是 CodeCeption:codeception.com/
,因为它在 API 测试方面似乎非常出色。另一个 BDD 风格的选择可能是 Behat:behat.org/en/latest/
。这是一个高级测试框架,但如果我们进行验收测试,甚至更好的是如果我们有一个专门的 QA 团队,他们将用 Gherkin 语法(github.com/cucumber/cucumber/wiki/Gherkin
)编写许多测试用例,这非常接近自然语言。然而,对于 PHP 开发人员来说,Behat 和 Gherkin 可能有更多的学习曲线,而 CodeCeption 只是简单的 PHP(尽管如果需要,它也可以使用 Gherkin),因此许多读者将是新手编写测试用例,我将保持简单并贴近 PHP。然而,这是我两年前写的关于选择 API 测试框架的详细比较,虽然有些过时,但对于大部分内容仍然有效。如果您感兴趣,可以看一下haafiz.me/programming/api-testing-selecting-testing-framework
。
CodeCeption 简介
CodeCeption 是用 PHP 编写的,并由 PHPUnit 支持。CodeCeption 声称CodeCeption 使用 PHPUnit 作为运行其测试的后端。因此,任何 PHPUnit 测试都可以添加到 CodeCeption 测试套件中,然后执行。
除了验收测试之外的其他测试需要一个具有对测试代码的洞察或连接的测试框架。如果我们使用的是开发框架,那么测试框架应该具有某种针对该框架的模块或插件。CodeCeption 在这方面做得很好。它为不同的框架和 CMS 提供了模块,例如 Symfony、Joomla、Laravel、Lumen、Yii2、WordPress 和 Zend 框架。只是让您知道,这些只是一些框架。CodeCeption 还支持许多其他模块,可以在不同情况下提供帮助。
设置和理解结构
安装 CodeCeption 有不同的方法,但我更喜欢 composer,这是安装不同 PHP 工具的标准方式。所以让我们安装它:
composer require "codeception/codeception" --dev
正如您所看到的,我们正在使用--dev
标志,这样它就会将 CodeCeption 添加到composer.json
文件中的require-dev
块中。因此,在生产环境中,当您运行composer install --no-dev
时,它将不会安装在require-dev
块中的依赖项。如果有疑惑,请查看与 composer 相关的章节,即第五章,使用 Composer 加载和解决问题,一个进化。
安装完成后,我们需要设置它以编写测试用例,并使其成为我们项目的一部分。安装只意味着它现在在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 带有三个测试套件。接受、功能和单元套件。所有这三个套件都包含四个文件和一个空目录。所以,让我们来看看每个文件和目录的目的。
tests/{suite-name}/
在这里,{suite-name}
将被套件的实际名称替换;比如说单元套件,它将是tests/unit/
。
无论如何,这个目录将用于保存我们将要编写的测试用例。
tests/{suite-name}.suite.yml
这个文件是特定套件的配置文件。它包含了这个特定套件的ActorName
。Actor 实际上就是具有特定设置和能力的人。根据 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 测试。虽然我们可以使用功能测试套件进行 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 网络服务端点。除此之外,它还依赖于 Lumen。我们说 Lumen,因为它将与我们的代码集成,我们正在编写功能级别的测试而不是接受测试。因此,我们的测试框架应该对 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 中进行测试时与数据库相关的内容将会回滚,并且不会影响我们的实际数据库,因此在开发中使用相同的数据库也是可以的。但是,在暂存环境中不建议,事实上是禁止使用相同的数据库进行测试;因此,最好从一开始就保持不同的数据库和配置。
我们不会在生产环境中运行测试。我们甚至不会在生产环境中安装与测试相关的工具,正如您所看到的,我们使用--dev
标志安装了 CodeCeption。因此,当我们的代码在生产环境中,并且我们想要部署一个新功能时,我们的测试用例会在不同的服务器上运行,然后将代码部署到生产服务器上。有几种CI(持续集成)工具可用于此。
编写测试用例
现在,是时候编写测试用例了。首先要了解的是,我们如何决定应该测试什么。我们应该先测试每个端点,然后再测试每个类吗?
首先要理解的是,我们应该只测试我们自己编写的代码。我所说的我们,是指我们团队的某个人。我们不打算测试第三方代码、框架代码或包代码。此外,我们也不想测试每个类和每个方法。在理想情况下,我们可以测试每个细微功能的细节,但这也有其缺点。首先,在现实世界中,我们没有时间这样做。我们打算测试大部分但不是全部部分。另一个原因是,我们编写的所有测试也是一种负担。随着时间的推移,我们还需要维护这些测试。因此,我们只对有意义的部分进行单元测试;只有在实际上执行一些复杂操作的地方才进行测试。如果您有一个函数,它的功能就是调用另一个函数并返回结果,那么我认为这样的代码片段不应该有自己的测试。
另一件事是,如果我们既要进行单元测试又要进行 API 测试,那么我们应该从哪里开始编写测试呢?我们应该先为所有端点编写测试,然后再为所有类编写测试,还是相反,先测试所有类,然后再测试所有端点?我们该如何做呢?我们显然打算测试我们的端点。我们也打算测试这些端点下的代码。这是不同的人可以以不同方式做的事情,但我和许多其他人,我见过的人,都是同时编写 API 测试和单元测试。我更喜欢编写 API 测试并继续为一个资源编写测试。之后,我们将转向控制器的单元测试。在我们的情况下,模型中除了从 Eloquent 或关系继承的内容之外,没有太多东西。在对资源进行 API 测试时,如果我们需要更多细节来修复错误,那么我们可以开始为该类编写单元测试。但这并没有硬性规定。这只是一种偏好问题。
针对 post 资源的 API 测试
我们可以以结构化方法编写测试用例,也可以以类的方式编写。两种方式都可以,我建议使用类,这样您可以在某个时候利用面向对象的概念。因此,让我们为此创建一个文件:
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
为帖子用户生成随机内容,这样数据将是随机和动态的,而不是静态内容。在测试期间,最好在测试用例中使用随机数据来进行测试。
好的,现在让我们在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 请求之外,没有必要明确说明任何事情,我们也可以说sendGet()
或sendPut()
来使用不同的 HTTP 方法,等等。所以,现在我们需要运行这个测试。
我们可以通过以下方式运行它:
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
在我们的情况下,我们的第一个测试应该已经通过了。现在,我们应该编写我们的第二个测试用例,也就是在登录后创建一个帖子。这涉及到更多我们需要理解的东西。所以让我们先看一下这个测试用例的代码,然后再进行审查:
// 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
文件中配置的 Lumen 模块。然后,我们使用 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 路径的断言,文档在这里:codeception.com/docs/modules/REST#seeResponseJsonMatchesJsonPath
。
你会像这样使用它:
$I->seeResponseJsonMatchesJsonPath('$.data[*].title');
这也是你在响应中看到的,但甚至有一个方法可以测试该记录是否现在也存在于数据库中。你应该自己尝试一下。你可以在这里找到它的文档:codeception.com/docs/modules/Db#seeInDatabase
。
其他测试用例
还有很多与其他帖子操作(端点)相关的测试用例。然而,编写测试用例的方式将保持不变。所以,我会跳过这部分,这样你就可以自己编写这些测试用例。不过,作为提示,以下是一些你应该练习编写的测试用例:
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.
因此,我强烈建议你编写这些测试用例。如果你需要更多示例,或者想要查看与身份验证相关的端点测试示例,那么你可以在这里找到一些示例,以便更好地理解:github.com/Haafiz/REST-API-for-basic-RPG/tree/master/tests/api
。
有关 CodeCeption 的更多信息,请参阅 CodeCeption 文档:codeception.com/
。
总结
在本章中,我们学习了测试类型,自动化测试的重要性,并为我们的 RESTful Web 服务端点编写了 API 测试。我再次想说的一件事是,我们只编写了 API 测试,以保持专注在我们的主题上,但单元测试同样重要。然而,测试是一个庞大的主题,单元测试有其自身的复杂性,所以无法在这一章中讨论。
更多资源
如果你想了解更多关于 PHP 自动化测试的信息,那么这里有一些重要的资源。
《Test Driven Laravel》(Adam Wathan 的视频课程)adamwathan.me/test-driven-Laravel/
,然而这主要是关注于 Laravel 的。但是,这也会教给你一些重要的东西。
同样地,Jeffrey Way 的旧书《Laravel Testing Decoded》可以在leanpub.com/Laravel-testing-decoded
找到。
再次强调,这是一本专门针对 Laravel 的书,但在一般情况下也会教给你很多东西。Jeffrey Way 即将推出的新书是关于 PHP 测试的,名为《Testing PHP》:leanpub.com/testingphp
前面提到的关于 PHP 的书还没有完成,所以你可以从 Jeffrey Way 的精彩的视频测试中学习:laracasts.com/skills/testing
。事实上,Laracasts 不仅适用于测试,还适用于全面学习 PHP 和 Laravel。
无论你选择哪个来源,重要的是你要练习。这对于开发和测试都是如此。事实上,如果你以前没有进行过测试,那么练习测试就更加重要。起初,你可能会感到有些不知所措,但这是值得的。
第九章:微服务
尽管我们在本书中讨论了不同的方面,但我们所做的是为一个简单的博客创建了一个 RESTful web 服务。我们选择了这个例子,以便在业务逻辑方面保持简单,这样我们可以更详细地专注于我们的实际主题。这是有帮助的,但在现实世界中,事情并不那么简单和小。有着不同部分的大型系统很难维护。这些部分也很难调试和扩展。扩展性与仅仅维护和优化以获得更好的性能是不同的。在扩展性方面,代码和部署环境的优化都很重要。可扩展性、可维护性和性能一直是我们面临的挑战。
为了解决这个问题,我们有一种被称为微服务的架构风格。因此,在本章中,我们将讨论这个问题。微服务并不是必须使用的东西。然而,它们解决了我们在为更大的系统创建 RESTful web 服务时经常面临的一些挑战。因此,我们将看到微服务如何解决这些问题,以及微服务架构带来的挑战。
以下是本章将讨论的主题:
-
介绍微服务
-
基于微服务架构的动机
-
它与 SOA(面向服务的架构)有何不同
-
团队结构
-
微服务的挑战
-
微服务实现
介绍微服务
首先让我们定义微服务架构,然后深入了解微服务的细节。微服务架构成为一个热门术语,但并没有任何正式的定义。事实上,迄今为止,关于其属性或定义,还没有官方共识。然而,不同的人尝试过定义它。我在 Martin Fowler 的博客上找到了一个定义,非常令人信服。他和 James Lewis 这样定义它:
微服务架构风格是一种将单个应用程序开发为一组小服务的方法,每个服务在自己的进程中运行,并使用轻量级机制进行通信,通常是 HTTP 资源 API。这些服务围绕业务能力构建,并且可以通过完全自动化的部署机制独立部署。这些服务的集中管理最少,可能使用不同的编程语言和不同的数据存储技术。-- James Lewis 和 Martin Fowler
这似乎很正式,所以让我们深入了解这个定义,并尝试理解微服务架构。
首先,你应该知道,在我们为博客创建的 RESTful web 服务的例子中,是一个单体式的 web 服务。这意味着一切都在同一个 web 服务中。一切都在一起,因此需要一起部署为一个代码库。我们也可以对更大的应用程序使用相同的单体式方法,但该应用程序将变得越来越复杂,可扩展性将会减弱。
与此相反,微服务由许多小服务组成。每个小服务被称为微服务,或者我们可以简单地称之为服务。这些服务实现了一个应用程序的目的,但它们是独立的,非常松散耦合的。因此,每个微服务都有一个单独的代码库和一个单独的数据库或存储。由于每个微服务都是独立的,即使我们想要在同一台服务器上或不同的服务器上部署,它也可以独立部署。这意味着所有服务可能是相同的语言或框架,也可能不是。如果一个服务是 PHP,另一个可能是 Node.js,另一个可能是 Python。
如何将应用程序划分为微服务?
因此,问题是,“如果我们有一个庞大的应用程序,那么我们如何决定如何将其分成不同的微服务?”在理解如何将一个大系统分成微服务时,我们将考虑不同的因素。这些因素基于马丁·福勒所谓的“微服务的特征”。您可以在martinfowler.com/articles/microservices.html
上查看马丁·福勒关于微服务特征的完整文章。
因此,在将一个大系统分成小的微服务时,需要考虑以下因素:
-
每个微服务应该独立于其他微服务。如果不是完全独立的(因为这些服务是一个应用程序的一部分,所以它们可能会相互交互),那么依赖关系应该是最小的。
-
我们将应用程序分成不同的组件。所谓组件,是指一个可以独立替换和升级的软件单元。这意味着替换或升级一个组件不应该对应用程序产生任何(或者最小的)影响。一个微服务将基于这样一个单一组件。
-
一个服务应该有一个单一的责任。
-
将应用程序或系统分成几个微系统,可以从业务需求入手。根据业务能力制作组件是一个好主意。事实上,我们的团队应该根据业务能力而不是技术来划分。
-
同时,确保服务不要过于细粒度也很重要。过于细粒度的服务可能会导致开发工作量增加,同时由于相互交互的事物太多而导致性能不佳,因为它们实际上是相互依赖的。
在理想情况下,这些服务总是彼此独立的。然而,这并不总是可能的。有时,一个服务需要另一个服务的某些东西,有时,两个或更多服务有一些共同的逻辑。因此,依赖服务主要通过 HTTP 调用相互交互,共同的逻辑可以在不同服务之间的共享代码库中。然而,这仅在这些服务中使用相同技术时才可能。实际上,这意味着两个或更多服务依赖于共同的代码库。因此,根据前述定义,从理论上讲,这违反了微服务架构,但由于没有正式的理论或官方规范,所以我们考虑任何在现实世界中发生的事情。
对微服务的动机
有几个动机支持微服务。然而,我想要开始的是,当我们将其分成具有单一责任的组件时,我们遵守SRP(单一责任原则)。单一责任实际上是面向对象原则中的前五个之一,也被称为 SOLID(en.wikipedia.org/wiki/SOLID_(object-oriented_design)
)。这个单一责任原则,无论是在架构层面还是低层面,都使事情变得简单和容易。在微服务的情况下,它将不同的组件分离开来。因此,修改一个组件的原因将与一个单一功能相关。系统的其他组件和功能将像以前一样工作。这就是微服务作为独立的组件和功能使它们更容易修改而不影响其他组件的方式。
以下是必须分开微服务的其他原因。
维护和调试
告诉大家模块化的代码总是更容易维护并且可以轻松调试,这并不是什么新鲜事。您可以轻松调试它,而且还有什么比不仅是模块化而且还部署为独立模块的组件更模块化的呢?因此,我们从微服务中获得了许多优势,这些优势是我们从模块化代码中获得的。
然而,有一点需要理解。如果我们从一开始就使用微服务架构,应用程序将是模块化的,因为我们正在分开开发服务。然而,如果我们没有从一开始就使用微服务,而是后来想要将其转换为微服务,那么首先,我们需要有模块化的代码,然后我们才能使用微服务架构,因为如果我们没有模块和松散耦合的代码,我们就无法将它们拆分成独立的组件。
总之,微服务的动机很简单,我们可以轻松地调试模块化的代码和组件。在维护的情况下,如果代码在独立的组件中,并且其他服务得到了它们所需的东西,而不用担心修改组件的内部逻辑,那么就不会出现连锁反应。
实际上,这还不是全部;在维护阶段一个非常重要的因素是生产力。在更大的代码库中,随着时间的推移,生产力可能会降低,因为开发人员需要担心整个应用程序。然而,在微服务中,一个团队中的开发人员进行的特定更改不需要担心整个应用程序,而只需要关注那个特定服务内的代码,因为对于那个特定的更改和正在处理它的开发人员来说,这一个微服务就是整个应用程序,其责任远远小于整个应用程序。因此,在维护期间,微服务的生产力可能比单片应用程序要好得多。
可扩展性
当系统扩展并且您想要为更多客户提供良好的性能时,经过一段时间,当您也进行了优化后,您需要更好、更强大的服务器。您可以通过向服务器添加更多资源来使服务器更强大。这被称为垂直扩展。垂直扩展有其局限性。毕竟,这是一个服务器。如果我们想要更多的可扩展性呢?实际上,还有另一种扩展方式,即水平扩展。在水平扩展中,您添加更多的小型服务器或服务器实例,而不是将所有资源添加到一个服务器中。在这种情况下,一个单片应用程序将如何部署在多个服务器上?我们可能需要在多个服务器上部署完整的应用程序,然后通过负载均衡器来管理通过多个服务器的流量。
然而,将整个应用程序部署在多个服务器上并不划算。如果我们可以让应用程序的一部分从一个服务器提供,另一部分从另一个服务器提供呢?这怎么可能?我们只有一个应用程序。这就是微服务架构的优势所在。它的好处不仅仅是可扩展性。其关键好处之一是系统中松散耦合的组件。
技术多样性
正如我们所见,在微服务中,每个代码库都与其他代码库分开。因此,不同的团队可以使用不同的技术和不同的存储来开发不同的服务。事实上,这些团队完全不需要在不同的服务之间使用相同的技术,除非它们提供的其他服务需要相互交互。然而,如果我们想要使用共享代码的选项来避免在不同技术中重复编写相同的逻辑,那么为了拥有共享的代码库,我们可能需要使用相同的技术。
弹性
在微服务中,弹性也是其中一个关键的好处。由于每个服务都是一个独立的组件,如果系统的一个组件因某种原因失败,那么问题可以与系统的其余部分隔离开来。
然而,我们需要确保系统在发生故障时能够正确降级。如果一个服务出现故障,我们可以尝试将其最小化,但可能会再次出现故障。然而,为了最小化其影响,我们应该小心处理,以便最小化其对其他服务和我们应用程序用户的影响。
可替换性
如果要替换系统的一部分,那么在单片架构中并不那么简单,因为一切都在同一个代码库中。然而,在微服务中,更容易替换系统的一个组件,因为你所需要做的就是有另一个服务并用它替换现有的服务。显然,你仍然需要有一个替代服务,但不像在同一个代码库中用其他代码替换整个组件那样。
并行化
通常,客户希望他们的软件能够早期开发并尽快上市,以便他们可以测试他们的想法或占领更多市场。因此,他们希望有更多的开发人员并行工作在他们的应用程序上。不幸的是,在单片应用程序中,我们可以进行有限的并行工作。实际上,如果我们有非常模块化的代码,我们也可以在单片应用程序中进行并行工作。然而,它仍然不能像基于微服务的应用程序那样独立和模块化。
每个服务都是独立开发和部署的。虽然这些服务彼此通信,但开发可以独立进行,在大多数情况下,我们可以保持几个服务的独立开发。因此,许多开发人员,实际上是开发团队,可以并行工作,这意味着软件可以早期开发。如果多个模块需要解决问题或需要另一个功能,则可以并行进行。
与 SOA 的不同之处
SOA 代表面向服务的架构。从名称上看,这种架构依赖于服务,就像微服务一样。服务定位是计算机软件中的一种服务设计范式。其原则强调关注点的分离(与 SRP 相同)。到目前为止,它似乎与微服务相似。在了解差异之前,我们需要知道什么是 SOA。尽管没有一个清晰的官方定义 SOA。所以让我们从维基百科中获取这个基本定义:
面向服务的架构(SOA)是一种软件设计风格,应用组件通过网络上的通信协议向其他组件提供服务。服务导向架构的基本原则与供应商、产品和技术无关。
如果你看这个定义,你会发现 SOA 与微服务非常相似,但它的定义并不那么简洁和清晰。一个原因可能是 SOA 本身是一个广义的架构。或者我们可以更好地说,SOA 是微服务的广义形式。
如 Oracle 的帖子所述:
“过去十年我们一直在谈论的就是微服务的 SOA。”-- Torsten Winterberg, Oracle ACE Director.
因此,微服务遵循相同的原则,但它更加专业化,专注于拥有多个独立的服务,其中一个服务是完全不同的组件,独立于其他服务存在。
团队结构
根据康威定律:
“设计系统的组织…受限于产生与这些组织的通信结构相同的设计。”
因此,为了基于微服务架构制定设计并获得其好处,我们还需要相应地组织工作的结构化团队。
通常,在单片应用程序中,我们有以下团队:
-
Dev-ops 团队
-
后端开发团队
-
数据库管理员团队
-
移动应用程序开发团队
然而,在分布式架构的情况下,例如微服务(如果我们正在开发电子商务应用程序),我们将有以下团队:
-
产品目录
-
库存
-
订单
-
优惠券
-
愿望清单
所有这些团队都将有成员,包括 Dev-ops、后端开发人员、数据库管理员和移动应用开发人员。因此,在微服务的情况下,我们将为每个服务设立一个团队。
团队规模:
没有硬性规定,但建议团队规模应符合杰夫·贝佐斯的“2 披萨规则”:如果一个团队不能靠两块披萨养活,那就太大了。
原因是,如果团队变得更大,那么沟通可能会变得糟糕。
微服务的挑战
没有免费的午餐。一切都有其不利之处,或者至少有一些需要应对的挑战。如果我们选择微服务,它也有自己的挑战。因此,让我们来看看它们,并讨论如果有权衡的话,如何将它们最小化。
基础设施维护
尽管你不必每天更新你的基础设施,但它仍然需要维护,需要更多的努力。微服务带来了技术自由,但并非没有任何代价。你必须使用不同的技术来维护不同的服务器实例。这将需要更好的基础设施和有更多技术经验的人。
实际上,你并不总是需要更好的基础设施和对所有这些不同技术都有了解的人。通常,每个负责不同服务的团队都会有自己的基础设施或与 Dev-ops 相关的人员。然而,在这种情况下,你需要更多的人,因为现在,你不再在不同团队之间共享 Dev-ops 或基础设施相关的人员。事实上,这就是微服务团队的组成方式。团队至少不应该有共享资源。否则,你就无法因为独立服务而获得并行工作的优势。
然而,基础设施不仅意味着服务器设置,还包括部署、监控和日志记录。因此,为了达到这个目的,你不能只使用一种技术来解决问题,而牺牲了你的技术选择。然而,限制你的技术选择也可以让 Dev-ops 变得更容易一些。
另一件事是你需要在持续集成服务器上进行自动部署。它运行你的测试用例,然后,如果一切顺利,就会部署到你的实际服务器上。为此,你需要有 Dev-ops 人员编写脚本来自动化你的部署。有几种方法可以做到这一点。
性能
实际上,微服务可以更快地运行的原因是,客户端使用了一个完全独立的微服务。一个明显的原因是,一个请求在一个小的微服务中需要经过的步骤比在一个大型单体应用中要少。
然而,这是一个理想情况,不是所有的微服务都完全独立于彼此。它们相互作用并且相互依赖。因此,如果一个服务需要从另一个服务获取某些东西,它很可能需要进行网络调用,而网络调用是昂贵的。这会导致性能问题。然而,如果服务以最小的依赖方式创建,这种情况可以最小化。如果依赖不是最小的,那就意味着服务不是独立的,在这种情况下,我们可以合并这样的服务并创建一个独立的服务。
另一个选择可以是共享代码;这段代码将被用于不同的服务之间。如果两个或更多服务使用相同的功能,那么我们可以将其作为不同服务依赖的另一个服务,而是将其作为不同服务代码库的一部分共享代码。我们不会重复自己,会尝试将其制作成不同服务可以使用的模块或包。然而,有些人认为这是不好的做法,因为我们会在不同服务之间共享一些代码,这意味着它不会松散耦合。
调试和故障排除
正如你所看到的,我们说在微服务中调试和维护会更容易。然而,当这些服务之间进行通信并且一个服务的输出影响另一个服务时,这也会成为一个挑战。
当我们有不同的服务时,我们需要一种服务之间相互通信的方式。服务之间的通信有两种方式:通过 HTTP 调用或通过消息。这里,通过消息我们指的是使用某种消息队列,比如 RabbitMQ 等。在消息传递的情况下,如果出现错误或者发生了一些意外情况,那么这可能会非常困难。因为不只有一个服务,每个服务都是基于前一个服务的输出工作的,所以很难知道问题出在哪里。
因此,解决这个问题的一种方法是彻底编写测试。因为如果确保每个服务的测试用例都被编写并测试它们是否正常工作,那么在部署之前就可以发现问题。
然而,情况并非总是如此。这是因为不只有一个服务。许多服务正在交互,有时,实时环境中会出现问题,你希望进行调试和修复。出于这个目的,日志非常重要。然而,再次强调,这是一个分布式环境。那么,我们能做些什么呢?以下是你需要确保在日志中做的一些事情。
日志应该是集中的
你需要在某个集中的地方收集日志。如果你的日志在一个集中的地方,那么查看它们就会更容易,而不是检查每个服务器实例的日志。
这也很重要,因为你应该在实例之外的地方有日志备份。原因是,如果你替换了一个实例,那么你可能希望保留日志的副本以便在调试时使用。这可以是任何地方,包括亚马逊 S3、你的数据库或者磁盘,但你希望它是持久的和可用的。如果你在 AWS 上,你也可以使用他们的监控服务 CloudWatch。
日志应该是可搜索的。
拥有日志是好的。但就像互联网上的很多信息一样,如果你不知道哪个链接对你来说有用,那它实际上并不有用。由于搜索引擎告诉我们哪些页面有更相关的内容,这变得更容易。同样,活动应用程序的日志,特别是当有很多服务的日志在一起时,将不会那么有用。会有很多日志。因此,为了使它们可用,你应该以一种可以搜索和在查看时容易理解的方式存储你的日志。
跟踪请求链
就像用户在网站上从一个页面转到另一个页面一样,用户的客户端发送请求来执行不同的任务。因此,知道用户在这之前发送了哪些请求是一个好主意,因为在某些情况下,之前的请求可能会影响其他请求。因此,为了跟踪这一点,你可以简单地传递一个标识符,第一次期望在所有其他请求中都能找到相同的标识符。
另一个优点是,它不仅会显示流程,而且如果有人要求你解释为什么出现了某个特定的问题,那么对你来说也会更容易。如果标识符在客户端,相关人员可以在错误报告中给你该标识符作为参考,这样你就可以理解要跟踪哪个请求流程。
动态日志级别
通常,对于日志记录,你会使用某种日志框架,典型的日志级别有警告、信息、调试和详细。通常,在生产环境中,会使用信息级别或其他信息,但如果你想要解决一些问题并进行调试,你应该能够动态地更改日志级别。
因此,如果需要的话,你应该能够动态地在运行时设置日志级别。这很重要,因为如果在生产环境中出现问题,你不希望它持续很长时间。
实施
由于本章只是微服务的简介,我们不会深入讨论实现的细节。然而,我们将概述如何在微服务中实现不同的事物。我们已经在本书中讨论了 RESTful Web 服务的实现。然而,微服务还有其他一些部分。因此,我们只会了解在实现这些部分时涉及了什么。
部署
我们将自动化部署。我们将使用持续交付工具。持续交付是一个过程,其中有短周期的频繁交付,并确保软件可以随时可靠地发布。它旨在更快地发布软件,并通过构建、测试和频繁发布软件的方法来最小化风险。
持续交付从源代码控制一直到生产的自动化。有各种工具或流程可以帮助实现持续交付过程。然而,在其中有两个重要的事情:
-
测试
-
CI(持续集成)
首先,在提交代码之前,开发人员应该在提交 CI 服务器上运行他们的测试(最重要的是单元测试),运行集成测试,并在通过测试时在 CI 服务器上集成。Travis CI 和 Jenkins CI 是流行的 CI 工具。除此之外,Circle CI 也很受欢迎。
在持续集成之后,构建会自动进行并自动部署。由于一图胜千言,为了进一步阐述,我在这里添加了维基百科的这张图片(这张图片来自维基媒体):
通过这个图表,我们将对 CI 有一些了解。有关持续交付的详细信息,您可以阅读维基百科文章en.wikipedia.org/wiki/Continuous_delivery
。
服务间通信
我们看到服务器之间的通信很重要。服务彼此依赖,有时一个服务的输入是另一个服务的输出,有时一个服务正在使用另一个服务。其中一个重要的事情是这些服务之间的通信。
因此,我们可以将服务间通信分为两种类型:
-
同步通信
-
异步通信
同步通信
在同步通信中,一个服务与另一个服务通信并等待结果。通常通过简单的 HTTP 调用来完成,使用与最终客户端相同的方法。因此,这些是简单的 HTTP 调用,得到一个响应(通常是 JSON)。一个服务向另一个服务发送 HTTP 请求,等待其响应,并在收到响应后继续。同步通信具有网络开销,并且必须等待响应,但实现简单,有时延迟不是问题。因此,在这种情况下,为了简单起见,我们可以使用同步通信。
异步通信
在异步通信中,一个服务不会等待另一个的响应。它基于发布-订阅模型。它使用消息代理向其他消费者/订阅者的服务发送消息。它使用轻量级消息传递工具,通过这些工具,一个服务向另一个服务发送消息。这样的消息传递工具包括但不限于 RabbitMQ、Apache Kafka 和 Akka。
如果您对了解更多关于微服务之间通信的内容感兴趣,那么howtocookmicroservices.com/communication/
上的文章可能会很有趣。
共享库或公共代码
正如我们讨论的,可能有一些代码在不同的服务之间是共通的。它既可以是第三方代码,也可以是由团队为同一个应用程序编写的代码。无论哪种情况,我们显然都希望使用这些共通的代码。为了做到这一点,我们不会在我们的应用程序中复制这些代码,因为这违反了 DRY(不要重复自己)原则。但是,请注意,如果我们使用不同的编程语言/技术,我们就无法使用共通的代码。
所以我们做的是,我们打包那些通用代码或共享库,然后上传到某个地方,在部署时可以从那里获取这个包。在 PHP 的情况下,我们会创建 composer 包并上传到 packagist。然后,在服务中,当我们需要那些通用代码时,我们只需安装 composer 的包并从 vendor 目录中使用那些通用代码。
像 composer 这样的包和包管理器不仅仅存在于 PHP 中。在 Node.js 中,有 NPM(Node Package Manager),你可以使用它来创建一个 Node 包来实现相同的目的。因此,在不同的技术中,有不同的方法来创建和使用这样的包。
总结
在这一章中,作为本书的最后一章,我试图介绍微服务,这是一种现在备受关注的架构风格。我们研究它是因为我们需要一种架构,可以使用 RESTful Web 服务在复杂和更大的系统中实现更好的性能和可伸缩性。
本书的重点是 PHP7 中的 RESTful Web 服务,我们还研究了与构建 RESTful Web 服务或 PHP7 相关的其他主题。我们详细研究了其中一些主题,而其他一些只是触及了一下。许多这些主题太广泛,无法包含在一个章节中。其中一些主题可以有一本专门的书来专门讨论。这就是为什么我提供了不同的 URL 以供学习材料或建议阅读,如果你感兴趣的话可以参考。
接下来
有两件重要的事情:
实践:
真正的学习是当你开始实践某些东西时才开始的。当你实践时,有时会遇到问题并学到更多,这是你在没有解决这些问题的情况下无法学到的。
查阅建议的材料:
无论我提供了什么建议阅读材料,都请停下来至少看一下建议的材料。如果你觉得有帮助,可以深入了解。谁知道,那些建议的材料可能会教会你比整本书更有价值的东西。毕竟,那些材料提供了比我们在本书中讨论的更多细节。