PHP测试

正如在上一篇指南中所解释的,Matomo(以前是Piwik)的测试套件包含PHP测试和用户界面测试.PHP测试套件是用它编写和运行的PHPUnit)

如果你正在创建一个新的插件,你可能会发现参与其中是有益的测试驱动开发或者至少通过测试来验证代码的正确性。通过测试,您将能够确保您的代码能够工作,并且您将能够确保您所做的更改不会导致倒退。

如何区分单元测试、集成测试和系统测试?

这有时很难决定,经常导致讨论。当测试只测试单个方法或类时,我们将测试视为单元测试。有时两个或三个类仍然可以被认为是一个单元,例如,如果你必须传递一个虚拟类或类似的东西,但它实际上应该只测试一个类或方法。如果它依赖于文件系统、web、配置、数据库或其他插件,那么它就不是单元测试,而是集成测试。如果测试很慢,它很可能不是单元测试,而是集成测试。“慢”当然是非常主观的,也取决于服务器,但如果你的测试没有任何依赖,你的测试就会非常快。

如果你对加载的插件、文件系统、web、配置、数据库或类似的东西有任何依赖,这就是一个集成测试。如果您在一个测试中测试多个类,那么它就是一个集成测试。

例如,如果您通过HTTP或CLI调用Matomo本身,那么整个系统都在测试,那么这就是一个系统测试。

为什么我们要把测试分成单元、集成、系统和ui文件夹?

因为它们失败的原因不同,测试执行的持续时间也不同。这允许我们执行所有单元测试并快速得到结果。单元测试不应该在不同的系统上失败,而应该在任何地方运行,例如,无论您是否使用NFS。一旦单元测试是绿色的,通常就会执行所有的集成测试,看看下一阶段是否正常工作。它们需要更长的时间,因为它们依赖于数据库和文件系统。系统和ui测试花费的时间最多,因为它们总是贯穿整个代码。

单独运行测试的另一个好处是我们得到了更准确的代码覆盖率。例如,当运行单元测试时,我们将得到真正的代码覆盖率,因为它们总是只测试一个类或方法。集成测试通常会运行大量代码,但实际上通常只测试一个方法。尽管许多方法没有经过测试,但在运行集成测试时,它们仍然被标记为已测试。

需求

在你开始之前,确保你有设置Matomo,并确保已启用开发模式:

$ ./console development:enable

要安装PHPUnit,在Matomo根目录中运行下面的命令(取决于您如何安装PHPUnit)安装的作曲家你可能不需要php命令):

php的作曲家。phar安装

如果你的开发不使用Matomo本地主机作为主机名(或者如果您的web服务器使用自定义端口号),然后编辑您的配置/ config.ini.php文件及以下(测试)部分,添加http_host和/或港口设置:

[tests] http_host = localhost port = 8777

request_uri需要配置以运行测试。如果您的开发Matomo是在子目录中设置的,例如athttp://localhost/dev/matomo,那么你的设置应该是这样的:

[测试]request_uri = "/dev/matomo"

如果你不使用任何子目录,你可以像这样简单地设置它:

[测试]request_uri = "/"

在运行测试之前(至少是第一次,但可以随时重新运行),运行此命令迁移测试数据库。

$ ./控制台测试:setup-fixture OmniFixture

编写单元测试

单元测试只测试单个方法或类,不使用文件系统、web、配置、数据库或任何其他插件等依赖项。

要创建一个新的单元测试,使用控制台:

$ ./console生成:test——测试类型单元

该命令将询问您插件的名称和测试的名称(通常是您想测试的类的名称)。它将创建一个文件编写MyPlugin /测试/单位/插件/ WidgetsTest.php其中包含一个例子让你开始:

/** * @group MyPlugin * @group WidgetsTest * @group Plugins */ class WidgetsTest extends UnitTestCase{公共函数testsimpleadd () {$this->assertEquals(2,1 +1);}}

我们不想讨论应该如何编写单元测试。这完全取决于你。如果您没有编写单元测试的经验,我们建议您阅读有关该主题的文章或书籍,观看视频或其他任何有助于您学习的东西。

编写集成测试

如果您的测试需要访问测试Matomo数据库、文件系统或任何其他依赖项,请创建一个集成测试:

$ ./console generate:test——测试类型集成

该命令还会询问插件的名称和测试的名称。它将创建一个文件插件/编写MyPlugin /测试/集成/ WidgetsTest.php其中包含一个示例来帮助您入门。

IntegrationTestCase基类提供设置()方法创建一个测试Matomo数据库和一个tearDown ()方法删除它。在集成测试期间,将加载所有插件,允许您编写实际的集成测试。

IntegrationTestCase基类还提供了两个可以重写的额外方法:

  • beforeTableDataCached ()IntegrationTestCase将在运行任何测试之前初始化一个fixture。该fixture可以添加实体、跟踪访问、归档它们或以另一种方式影响数据库。作为一种优化,IntegrationTestCase在设置fixture之后,是否将所有这些数据缓存到内存中,并简单地将它们重新插入到测试中的数据库表中设置()方法。这允许我们对每个测试都有一个干净的记录,而不必每次都破坏数据库并重新运行fixture(这将比仅仅恢复表状态慢得多)。控件中的fixture外部添加数据设置()方法。这会降低测试的速度,并扩展在travis-ci.com上已经运行了很长时间的构建。如果该数据对于每个测试都是静态的,那么可以将其添加到一个已覆盖的beforeTableDataCached ()方法。然后它将与夹具数据一起缓存。您可以在这里添加的一些东西的例子是:通过SitesManager API添加许多网站或通过UsersManager API添加许多用户。使用API比仅仅将数据插入截断的表要慢得多。
  • configureFixture ():为了处理Matomo环境的创建/销毁,集成测试使用一个空白fixture(它可以被覆盖,就像在系统测试中使用一些数据引导测试一样)。该夹具的配置可能不是每个测试都理想,因此可以使用这种方法以不同的方式配置夹具。例如,默认情况下,我们不会在系统中创建一个真正的超级用户(例如,将一行添加到用户表和其他所有东西),我们不加载任何翻译。有些测试可能不需要这个。

故障排除集成测试

在编写集成测试时可能会出现一些常见问题。下面列出了它们的适当解决方案:

  • 在我的测试中需要加载翻译,但没有:默认情况下,集成测试不加载翻译(而系统测试加载翻译)。如果你需要真正的转换,你可以在集成测试中添加以下代码来覆盖这个行为:受保护的静态函数configureFixture($fixture){父::configureFixture($fixture);$fixture->extraTestEnvVars['loadRealTranslations'] = true;}
  • 我想在集成测试中跟踪请求,但是它们没有跟踪并且给出空响应。:有很多原因可能会发生这种情况,但最常见的是没有创建超级用户,跟踪器无法验证。默认情况下,集成测试在运行测试之前不会创建真正的超级用户。您可以使用添加到集成测试中的以下代码覆盖此行为:受保护的静态函数configureFixture($fixture){父::configureFixture($fixture);$fixture->createSuperUser = true;}

测试的自定义行为

只要有可能,你就应该使用依赖注入改变测试中的行为。例如,如果您不想从远程服务获取数据,而是使用本地fixture,那么您可以在插件/ MyPluginName / config / test.php其中返回一个常规数组并覆盖任何DI依赖项。

如果DI不可用或不容易使用,则可以通过使用检查当前是否正在执行测试$isTestMode = defined('PIWIK_TEST_MODE') && PIWIK_TEST_MODE;

运行测试

要运行测试,使用该命令运行测试:允许您执行测试套件、特定文件、文件夹内的所有文件或一组测试。

为了验证创建的测试是否有效,我们将运行它,如下所示:

$ ./控制台测试:运行WidgetsTest

这将运行具有该组的所有测试WidgetsTest.由于其他测试可以使用相同的组,您可能希望将路径传递给您的测试文件:

$ ./console tests:运行plugins/Insights/tests/Unit/Widgets.php

如果你想在你的插件中运行所有的测试,将插件的名称作为参数传递:

$ ./控制台测试:运行洞察

当然,你也可以定义多个参数:

$ ./控制台测试:运行insights WidgetsTest

这将在具有WidgetsTest组的insights插件中执行所有测试。如果你只想在你的插件中运行单元测试,你可以这样做:

$ ./控制台测试:运行洞察单元

要运行所有单元、集成或系统测试,请使用以下参数之一:

$ ./控制台测试:运行单元$ ./控制台测试:运行集成$ ./控制台测试:运行系统

仅运行特定的测试用例以节省时间

在开发或调试测试时,不需要总是在一个文件或组中执行所有测试。相反,通过添加筛选器选项,您只能执行特定的测试或一组测试。

$ ./console测试:运行路径/file.php——filter="test_mymethod"

这将只运行以特定方法名开始的测试用例test_mymethod.这将使此测试的故障排除更快,因为您不需要等待所有其他测试用例完成。

特殊的测试

Matomo中的大多数单元和集成测试都测试单个类,或者最多测试一个Matomo子系统。然而,有一个测试很特别,因为它们不测试Matomo的行为,而是测试Matomo是否准备好被释放。这个测试叫做ReleaseCheckListTest并执行以下类型的测试:

  • 当开发人员将一个方法标记为@deprecated时,我们有时会希望确保在特定的时间内删除它,在我们给插件开发人员停止使用它的机会之后。这可以是在特定的时间,也可以是在Matomo的主要版本发布时。
  • 检查资源文件是否处于最新版本或是否准备发布
  • 测试调试代码不会意外地留在某些代码中
  • 还有很多其他的事情。

插件有时会定义自己的测试版本。

最佳实践

使用正确的断言

  • 如果可能,最好使用assertSameassertequal所以它做了一个精确的比较(包括类型)
  • 知道其他的方法,比如$ this - > assertSame(计数(数组)美元)使用$ this - > assertCount(数组)美元
  • 看到可用断言的完整列表

比较整个变量

而不是例如

$this->assertEquals(1, count($missingTables));$this->assertEquals('foobar', $missingTables[0]);

使用起来要好得多$this->assertSame(['foobar'], $missingTables);

这样你只需要一个assertequal$this->assertEquals(1, count($missingTables));可以删除。更重要的是,当有一个测试失败时,它会显示整个的输出/差值missingTables美元与当前的实现相比,你只会看到类似的东西期望1,实际2这对了解问题出在哪里并没有什么帮助。使用建议的断言,它将确切地告诉您哪里出了问题,并通过比较整个变量,始终确保没有遗漏任何内容。

每个检查有一个测试用例

在理想的情况下,每个测试用例只有一个断言,或者只测试一个特定的用例。而不是例如:

公共函数test_multiply() {$this->assertSame(false, $this->report->multiply(0,false));$this->assertSame(1, $this->report->multiply(1,1));}

将它们分成两种不同的方法:

公共函数test_multiply_shouldReturnFalse_whenOneInputIsNotNumeric() {$this->assertSame(false, $this->report->multiply(0,false));}公共函数test_multiply_shouldReturnTheResult_whenTwoNumbersAreGiven() {$this->assertSame(1, $this->report->multiply(1,1));}

这样,当测试用例失败时,测试输出将更加详细,并且更清楚用例试图测试什么。

不要捕捉异常

我们不应该捕捉任何意外的异常,否则测试会在我们没有注意到它们开始失败的情况下成功。相反,我们可以简单地删除try/catch块。当将来出现任何异常时,测试将失败(这很好),我们将得到PHPUnit报告的异常消息。

公共函数test_multiply() {try {$this->assertSame(false, $this->report->multiply(0,false));} catch(异常$e) {Log:: Log('测试失败:' . Log)。$ e - > getMessage ());}}

测试边缘用例

不要只测试方法可能使用的预期方式。还可以传递意外值,例如等。

不要断言。

假设你有一个这样的断言:$this->assertNotContains("_paq.push(['requireCookieConsent']);", $trackingCode);

那么简单地使用它可能会更好$this->assertNotContains('requireCookieConsent', $trackingCode);

为什么?

  • 它降低了由于输入错误而导致测试意外通过的几率。如果实际代码在某处有空格(例如_paq。推动([' requireCookieConsent ']);),那么测试仍然会通过,即使它包含requireCookieConsent调用。
  • 它是未来的证明。如果有人将实现更改为使用双引号而不是单引号(_paq.push ([" requireCookieConsent "]);),那么测试仍然可以正确地工作,并且仍然可以正确地检测到任何失败,如果由于某种原因存在bug和requireCookieConsent电话突然出现trackingCode美元

注意这并不适用于egassertContains你想要具体到哪里,以确保我们得到预期的结果。你可以在这里写$this->assertContains("_paq.push(['requireCookieConsent']);", $trackingCode);

在适用的情况下使用数据提供者

不要写这样的测试:

公共函数test_me() {$this->assertSame(1, $this->square(1));$ this - > assertSame (4, $ this - >平方(2));$ this - > assertSame (9 $ this - >平方(3));}

像这样使用数据提供程序:

/** * @dataProvider getMyDataProvider */公共函数test_me($expected, $number) {$this->assertSame($expected, $this->square($number));}公共函数getMyDataProvider() {yield '当数字为1时,它应该返回1' => [1,1];Yield '当数字为2时,它应该返回4' => [4,2];Yield '当数字为3时,它应该返回9' => [9,3];}

这确保了测试执行不会在其中一个失败时中途停止,并使测试代码更具可读性。

修复损坏的系统测试构建

当构建在本地失败时

的目录加工过的而且预期正在执行的测试的“测试”目录。例如插件/ YourPluginName /测试/系统/测试/ PHPUnit) /系统/

然后您可以比较这两个目录的更改。如果您正在使用PHPStorm,那么只需选择两者加工过的而且预期目录,然后右键单击并选择比较目录.在那里,您可以看到每个文件的更改,并在需要时更新任何已处理的文件。如果您不使用PHPStorm,那么检查您的IDE是否提供类似的功能或使用linux命令Diff处理预期

一旦您更新了所有期望的文件,那么您就需要git添加而且git提交而且git推这些变化。

当崔维斯的构建失败时

Matomo中的系统PHP测试通常执行一个API方法,并将API方法的整个XML输出与预期的XML输出进行比较。

如果您正在对Matomo进行更改,那么这样一个API方法的结果可能会更改并破坏构建。这是一个检查代码的机会,作为Matomo开发人员,您应该确保输出中的任何更改都是实际预期的。

如果它们不是预期的,确定更改的原因,并在新的提交中修复它。如果更改是预期的,那么您应该相应地更新预期的系统文件。要比较和更新预期的系统文件,请遵循以下步骤:

  • 通过打开Travis运行您的拉请求来找出Travis构建号。构建号通常是5位或6位数字,并具有前导哈希字符。例如,当构建是# 45511,然后45511是版本号。
  • 执行该命令并替换{buildnumber}使用实际的版本号。./控制台开发:sync-system-test-processed {buildnumber}
    • 要更新预期的文件,直接追加该选项——预期.然后,在提交和推动这些更改之前,您需要确保每个更改都是实际预期的。
    • 或者如果你只想更新一些文件,或者如果你不使用git的可视化工具,那么你可以在没有预期选项的情况下执行该命令,在这种情况下,系统文件将在加工过的目录中。例如测试/ PHPUnit) /系统/处理而且插件/目标/测试/系统/处理.如果您正在使用PHPStorm,那么您可以同时选择已处理目录和预期目录,然后右击->比较目录.这允许您查看添加,更改和删除文件的每个更改,并让您单独更新每个预期文件。
  • 然后git添加而且git提交而且git推触发另一个构建运行的更改。
  • 如果某些测试仍然失败,您可能需要重复此过程,因为有时您可能忘记更新某些测试。

调试测试

作为编写测试的软件开发人员,能够在运行测试时设置断点和调试是很有用的。如果你使用Phpstorm阅读下面的答案学习使用Composer中的PHPUnit配置Phpstorm。

了解更多

  • 了解更多你可以用PHPUnit做什么读PHPUnit)的用户文档
Baidu