编写一个简单的测试

测试PHP代码的主要手段是使用PHPUnit,它是基于一种叫做单元测试的方法论。单元测试背后的理念非常简单:将代码分解成尽可能小的逻辑单元。然后对每个单元进行隔离测试,以确认它的性能符合预期。这些预期被编纂成一系列的断言。如果所有的断言都返回 “true”,那么这个单元就通过了测试。

{% hint style=”info” %} 在程序化PHP中,单元是一个函数。对于OOP PHP来说,单元是一个类中的方法。 {% endhint %}

如何做…

1.首先要做的是直接将 PHPUnit 安装到你的开发服务器上,或者下载源码,源码以单个 phar(PHP 档案)文件的形式存在。快速访问PHPUnit的官方网站(https://phpunit.de/\),我们可以直接从主页下载。

  1. 然而,最好的做法是使用一个包管理器来安装和维护 PHPUnit。为此,我们将使用一个名为 Composer 的软件包管理程序。要安装 Composer,请访问主网站 https://getcomposer.org/,并按照下载页面的说明进行安装。目前的程序,在写这篇文章的时候,如下所示。注意,你需要用当前版本的哈希值代替``。
  1. php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
  2. php -r "if (hash_file('SHA384', 'composer-setup.php') === '<hash>') {
  3. echo 'Installer verified';
  4. } else {
  5. echo 'Installer corrupt'; unlink('composer-setup.php');
  6. } echo PHP_EOL;"
  7. php composer-setup.php
  8. php -r "unlink('composer-setup.php');"

{% hint style=”info” %} 最佳实践

使用Composer这样的软件包管理程序的好处是,它不仅可以安装,还可以用来更新你的应用程序所使用的任何外部软件(如PHPUnit)。 {% endhint %}

  1. 接下来,我们使用Composer来安装PHPUnit。这是通过创建一个composer.json文件来完成的,该文件包含一系列概述项目参数和依赖关系的指令。对这些指令的完整描述超出了本书的范围;然而,为了本实例的目的,我们使用关键参数 require 创建了一组最小的指令。你还会注意到,文件的内容是JavaScript对象符号(JSON)格式。
  1. {
  2. "require-dev": {
  3. "phpunit/phpunit": "*"
  4. }
  5. }
  1. 要从命令行进行安装,我们运行以下命令。后面的输出就会显示出来。
  1. php composer.phar install

编写一个简单的测试 - 图1

  1. PHPUnit 和它的依赖项被放置在一个 vendor 文件夹中,如果该文件夹不存在,Composer 将会创建它。然后,调用 PHPUnit 的主要命令被符号化地链接到 vendor/bin 文件夹中。如果你把这个文件夹放在你的 PATH 中,你所需要做的就是运行这个命令,它将检查版本并顺便确认安装。
  1. phpunit --version

运行简单的测试

1.为了便于说明,我们假设我们有一个包含add()函数的chap_13_unit_test_simple.php文件。

  1. <?php
  2. function add($a = NULL, $b = NULL)
  3. {
  4. return $a + $b;
  5. }
  1. 然后将测试写成扩展PHPUnit\Framework\TestCase的类。如果你要测试一个函数库,在测试类的开头,包括包含函数定义的文件。然后,你会写出以test开头的方法,通常后面是你要测试的函数的名称,可能还有一些额外的CamelCase词来进一步描述测试。在本示例中,我们将定义一个SimpleTest测试类。
  1. <?php
  2. use PHPUnit\Framework\TestCase;
  3. require_once __DIR__ . '/chap_13_unit_test_simple.php';
  4. class SimpleTest extends TestCase
  5. {
  6. // testXXX() methods go here
  7. }
  1. 断言是任何测试集的核心。一个断言是一个PHPUnit方法,它将一个已知的值和你想测试的值进行比较。一个例子是 assertEquals(),它检查第一个参数是否等于第二个参数。下面的例子测试了一个名为 add() 的方法,并确认 2 是 add(1,1) 的返回值。
  1. public function testAdd()
  2. {
  3. $this->assertEquals(2, add(1,1));
  4. }
  1. 你也可以测试一下某件事情是否不真实。这个例子断言1+1不等于3。
  1. $this->assertNotEquals(3, add(1,1));
  1. assertRegExp()是一个在测试字符串时非常有用的断言。在这个例子中,假设我们正在测试一个从一个多维数组中生成一个HTML表格的函数。
  1. function table(array $a)
  2. {
  3. $table = '<table>';
  4. foreach ($a as $row) {
  5. $table .= '<tr><td>';
  6. $table .= implode('</td><td>', $row);
  7. $table .= '</td></tr>';
  8. }
  9. $table .= '</table>';
  10. return $table;
  11. }
  1. 我们可以构造一个简单的测试,以确认输出包含<table>,一个或多个字符,然后是</table>。此外,我们希望确认元素<td>B</td>存在。在编写测试时,我们建立一个由三个子数组组成的测试数组,其中包含字母A-C、D-F和G-I。然后我们将测试数组传递给函数,并针对结果运行断言。
  1. public function testTable()
  2. {
  3. $a = [range('A', 'C'),range('D', 'F'),range('G','I')];
  4. $table = table($a);
  5. $this->assertRegExp('!^<table>.+</table>$!', $table);
  6. $this->assertRegExp('!<td>B</td>!', $table);
  7. }
  1. 要测试一个类,不需要包含一个函数库,只需要包含定义要测试的类的文件。为了便于说明,让我们把前面显示的函数库移到Demo类中。
  1. <?php
  2. class Demo
  3. {
  4. public function add($a, $b)
  5. {
  6. return $a + $b;
  7. }
  8. public function sub($a, $b)
  9. {
  10. return $a - $b;
  11. }
  12. // etc.
  13. }
  1. 在我们的SimpleClassTest测试类中,我们不包含库文件,而是包含代表Demo类的文件。为了运行测试,我们需要一个Demo的实例。为此,我们使用了一个专门设计的setup()方法,它在每次测试之前都会运行。另外,你会注意到一个 teardown()方法,它是在每次测试后立即运行的。
  1. <?php
  2. use PHPUnit\Framework\TestCase;
  3. require_once __DIR__ . '/Demo.php';
  4. class SimpleClassTest extends TestCase
  5. {
  6. protected $demo;
  7. public function setup()
  8. {
  9. $this->demo = new Demo();
  10. }
  11. public function teardown()
  12. {
  13. unset($this->demo);
  14. }
  15. public function testAdd()
  16. {
  17. $this->assertEquals(2, $this->demo->add(1,1));
  18. }
  19. public function testSub()
  20. {
  21. $this->assertEquals(0, $this->demo->sub(1,1));
  22. }
  23. // etc.
  24. }

{% hint style=”info” %} 之所以在每次测试前后运行setup()trapdown(),是为了保证一个新鲜的测试环境。这样,一个测试的结果就不会影响另一个测试的结果。 {% endhint %}

测试数据库模型类

1.当测试一个有数据库访问权限的类(如Model类)时,其他的考虑因素也在发挥作用。主要的考虑是,你应该针对测试数据库,而不是生产中使用的真实数据库来运行测试。最后一点是,通过使用测试数据库,你可以事先用适当的、受控的数据填充它,setup()teardown()也可以用来添加或删除测试数据。

  1. 作为一个使用数据库的类的例子,我们将定义一个类 VisitorOps。这个新类将包括添加、删除和查找访客的方法。请注意,我们还添加了一个方法来返回最新执行的SQL语句。
  1. <?php
  2. require __DIR__ . '/../Application/Database/Connection.php';
  3. use Application\Database\Connection;
  4. class VisitorOps
  5. {
  6. const TABLE_NAME = 'visitors';
  7. protected $connection;
  8. protected $sql;
  9. public function __construct(array $config)
  10. {
  11. $this->connection = new Connection($config);
  12. }
  13. public function getSql()
  14. {
  15. return $this->sql;
  16. }
  17. public function findAll()
  18. {
  19. $sql = 'SELECT * FROM ' . self::TABLE_NAME;
  20. $stmt = $this->runSql($sql);
  21. while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
  22. yield $row;
  23. }
  24. }
  25. public function findById($id)
  26. {
  27. $sql = 'SELECT * FROM ' . self::TABLE_NAME;
  28. $sql .= ' WHERE id = ?';
  29. $stmt = $this->runSql($sql, [$id]);
  30. return $stmt->fetch(PDO::FETCH_ASSOC);
  31. }
  32. public function removeById($id)
  33. {
  34. $sql = 'DELETE FROM ' . self::TABLE_NAME;
  35. $sql .= ' WHERE id = ?';
  36. return $this->runSql($sql, [$id]);
  37. }
  38. public function addVisitor($data)
  39. {
  40. $sql = 'INSERT INTO ' . self::TABLE_NAME;
  41. $sql .= ' (' . implode(',',array_keys($data)) . ') ';
  42. $sql .= ' VALUES ';
  43. $sql .= ' ( :' . implode(',:',array_keys($data)) . ') ';
  44. $this->runSql($sql, $data);
  45. return $this->connection->pdo->lastInsertId();
  46. }
  47. public function runSql($sql, $params = NULL)
  48. {
  49. $this->sql = $sql;
  50. try {
  51. $stmt = $this->connection->pdo->prepare($sql);
  52. $result = $stmt->execute($params);
  53. } catch (Throwable $e) {
  54. error_log(__METHOD__ . ':' . $e->getMessage());
  55. return FALSE;
  56. }
  57. return $stmt;
  58. }
  59. }
  1. 对于涉及数据库的测试,建议使用测试数据库而不是实时生产数据库。相应地,你将需要一组额外的数据库连接参数,可以用来在setup()方法中建立数据库连接。

  2. 有可能你希望建立一个一致的样本数据块。这可以在setup()方法中插入到测试数据库中。

  3. 最后,你可能希望在每次测试后重置测试数据库,这在 teardown() 方法中完成。

使用MOCK类

1.在某些情况下,测试将访问需要外部资源的复杂组件。一个例子是需要访问数据库的服务类。在测试套件中尽量减少数据库访问是一个最佳实践。另一个考虑因素是,我们不是在测试数据库访问;我们只是在测试一个特定类的功能。因此,有时有必要定义模拟类,模仿其父类的行为,但限制对外部资源的访问。

{% hint style=”info” %} 最佳实践

在你的测试中,将实际的数据库访问限制在Model(或同等的)类中。否则,运行整套测试所需的时间可能会过长。 {% endhint %}

  1. 在这种情况下,为了说明问题,定义一个服务类VisitorService,它使用了前面讨论的VisitorOps类。
  1. <?php
  2. require_once __DIR__ . '/VisitorOps.php';
  3. require_once __DIR__ . '/../Application/Database/Connection.php';
  4. use Application\Database\Connection;
  5. class VisitorService
  6. {
  7. protected $visitorOps;
  8. public function __construct(array $config)
  9. {
  10. $this->visitorOps = new VisitorOps($config);
  11. }
  12. public function showAllVisitors()
  13. {
  14. $table = '<table>';
  15. foreach ($this->visitorOps->findAll() as $row) {
  16. $table .= '<tr><td>';
  17. $table .= implode('</td><td>', $row);
  18. $table .= '</td></tr>';
  19. }
  20. $table .= '</table>';
  21. return $table;
  22. }
  1. 为了测试的目的,我们为$visitorOps属性添加一个getter和setter。这允许我们插入一个模拟类来代替真正的VisitorOps类。
  1. public function getVisitorOps()
  2. {
  3. return $this->visitorOps;
  4. }
  5. public function setVisitorOps(VisitorOps $visitorOps)
  6. {
  7. $this->visitorOps = $visitorOps;
  8. }
  9. } // closing brace for VisitorService
  1. 接下来,我们定义一个VisitorOpsMock模拟类,模仿其父类的功能。类的常量和属性都是继承的。然后我们添加模拟测试数据,以及一个getter,以备以后需要访问测试数据时使用。
  1. <?php
  2. require_once __DIR__ . '/VisitorOps.php';
  3. class VisitorOpsMock extends VisitorOps
  4. {
  5. protected $testData;
  6. public function __construct()
  7. {
  8. $data = array();
  9. for ($x = 1; $x <= 3; $x++) {
  10. $data[$x]['id'] = $x;
  11. $data[$x]['email'] = $x . 'test@unlikelysource.com';
  12. $data[$x]['visit_date'] =
  13. '2000-0' . $x . '-0' . $x . ' 00:00:00';
  14. $data[$x]['comments'] = 'TEST ' . $x;
  15. $data[$x]['name'] = 'TEST ' . $x;
  16. }
  17. $this->testData = $data;
  18. }
  19. public function getTestData()
  20. {
  21. return $this->testData;
  22. }
  1. 接下来,我们重写findAll()来使用yield返回测试数据,就像在父类中一样。请注意,我们仍然构建SQL字符串,因为这是父类的工作。
  1. public function findAll()
  2. {
  3. $sql = 'SELECT * FROM ' . self::TABLE_NAME;
  4. foreach ($this->testData as $row) {
  5. yield $row;
  6. }
  7. }
  1. 为了模拟findById(),我们简单地从$this->testData返回数组键。对于removeById(),我们取消设置从$this->testData中提供的数组键作为参数。
  1. public function findById($id)
  2. {
  3. $sql = 'SELECT * FROM ' . self::TABLE_NAME;
  4. $sql .= ' WHERE id = ?';
  5. return $this->testData[$id] ?? FALSE;
  6. }
  7. public function removeById($id)
  8. {
  9. $sql = 'DELETE FROM ' . self::TABLE_NAME;
  10. $sql .= ' WHERE id = ?';
  11. if (empty($this->testData[$id])) {
  12. return 0;
  13. } else {
  14. unset($this->testData[$id]);
  15. return 1;
  16. }
  17. }
  1. 添加数据稍微复杂一些,因为我们需要模拟一个事实,即id参数可能没有被提供,因为数据库通常会自动为我们生成这个参数。为了解决这个问题,我们检查id参数。如果没有设置,我们找到最大的数组键,然后递增。
  1. public function addVisitor($data)
  2. {
  3. $sql = 'INSERT INTO ' . self::TABLE_NAME;
  4. $sql .= ' (' . implode(',',array_keys($data)) . ') ';
  5. $sql .= ' VALUES ';
  6. $sql .= ' ( :' . implode(',:',array_keys($data)) . ') ';
  7. if (!empty($data['id'])) {
  8. $id = $data['id'];
  9. } else {
  10. $keys = array_keys($this->testData);
  11. sort($keys);
  12. $id = end($keys) + 1;
  13. $data['id'] = $id;
  14. }
  15. $this->testData[$id] = $data;
  16. return 1;
  17. }
  18. } // ending brace for the class VisitorOpsMock

使用匿名类作为模拟对象

1.关于mock对象的一个很好的变化是使用新的PHP 7匿名类来代替创建一个定义mock功能的正式类。使用匿名类的好处是可以扩展一个现有的类,这使得对象看起来合法。如果你只需要覆盖一两个方法,这种方法特别有用。

  1. 在这个例子中,我们将修改之前介绍的VisitorServiceTest.php,将其称为VisitorServiceTestAnonClass.php
  1. <?php
  2. use PHPUnit\Framework\TestCase;
  3. require_once __DIR__ . '/VisitorService.php';
  4. require_once __DIR__ . '/VisitorOps.php';
  5. class VisitorServiceTestAnonClass extends TestCase
  6. {
  7. protected $visitorService;
  8. protected $dbConfig = [
  9. 'driver' => 'mysql',
  10. 'host' => 'localhost',
  11. 'dbname' => 'php7cookbook_test',
  12. 'user' => 'cook',
  13. 'password' => 'book',
  14. 'errmode' => PDO::ERRMODE_EXCEPTION,
  15. ];
  16. protected $testData;
  1. 您会注意到,在setup()中,我们定义了一个匿名类,该类扩展了VisitorOps。我们只需要重写findAll()方法。
  1. public function setup()
  2. {
  3. $data = array();
  4. for ($x = 1; $x <= 3; $x++) {
  5. $data[$x]['id'] = $x;
  6. $data[$x]['email'] = $x . 'test@unlikelysource.com';
  7. $data[$x]['visit_date'] =
  8. '2000-0' . $x . '-0' . $x . ' 00:00:00';
  9. $data[$x]['comments'] = 'TEST ' . $x;
  10. $data[$x]['name'] = 'TEST ' . $x;
  11. }
  12. $this->testData = $data;
  13. $this->visitorService =
  14. new VisitorService($this->dbConfig);
  15. $opsMock =
  16. new class ($this->testData) extends VisitorOps {
  17. protected $testData;
  18. public function __construct($testData)
  19. {
  20. $this->testData = $testData;
  21. }
  22. public function findAll()
  23. {
  24. return $this->testData;
  25. }
  26. };
  27. $this->visitorService->setVisitorOps($opsMock);
  28. }
  1. 请注意,在testShowAllVisitors()中,当$this->visitorService->showAllVisitors()被执行时,匿名类会被访问者服务调用,而访问者服务又会调用重写的findAll()
  1. public function teardown()
  2. {
  3. unset($this->visitorService);
  4. }
  5. public function testShowAllVisitors()
  6. {
  7. $result = $this->visitorService->showAllVisitors();
  8. $this->assertRegExp('!^<table>.+</table>$!', $result);
  9. foreach ($this->testData as $key => $value) {
  10. $dataWeWant = '!<td>' . $key . '</td>!';
  11. $this->assertRegExp($dataWeWant, $result);
  12. }
  13. }
  14. }

使用MOCK BUILDER

1.另一种技术是使用getMockBuilder()。虽然这种方法不允许对产生的mock对象进行大量的有限控制,但在你只需要确认返回某个类的对象,并且当运行指定的方法时,这个方法会返回一些预期的值的情况下,这种方法是非常有用的。

  1. 在下面的示例中,我们复制了VisitorServiceTestAnonClass;唯一的区别在于如何在setup()中提供VisitorOps的实例,在本例中,使用getMockBuilder()。请注意,虽然我们在这个例子中没有使用with(),但它是用来将受控参数馈送到模拟方法的。
  1. <?php
  2. use PHPUnit\Framework\TestCase;
  3. require_once __DIR__ . '/VisitorService.php';
  4. require_once __DIR__ . '/VisitorOps.php';
  5. class VisitorServiceTestAnonMockBuilder extends TestCase
  6. {
  7. // code is identical to VisitorServiceTestAnon
  8. public function setup()
  9. {
  10. $data = array();
  11. for ($x = 1; $x <= 3; $x++) {
  12. $data[$x]['id'] = $x;
  13. $data[$x]['email'] = $x . 'test@unlikelysource.com';
  14. $data[$x]['visit_date'] =
  15. '2000-0' . $x . '-0' . $x . ' 00:00:00';
  16. $data[$x]['comments'] = 'TEST ' . $x;
  17. $data[$x]['name'] = 'TEST ' . $x;
  18. }
  19. $this->testData = $data;
  20. $this->visitorService =
  21. new VisitorService($this->dbConfig);
  22. $opsMock = $this->getMockBuilder(VisitorOps::class)
  23. ->setMethods(['findAll'])
  24. ->disableOriginalConstructor()
  25. ->getMock();
  26. $opsMock->expects($this->once())
  27. ->method('findAll')
  28. ->with()
  29. ->will($this->returnValue($this->testData));
  30. $this->visitorService->setVisitorOps($opsMock);
  31. }
  32. // remaining code is the same
  33. }

{% hint style=”info” %} 我们已经展示了如何创建简单的一次性测试。然而,在大多数情况下,你会有许多需要测试的类,最好是一次全部测试。这可以通过开发一个测试套件来实现,在下一个事例中会有更详细的讨论。 {% endhint %}

如何运行…

首先,你需要安装 PHPUnit,如步骤 1 至 5 所述。 确保在 PATH 中包含 vendor/bin,这样你就可以从命令行运行 PHPUnit。

运行简单的测试

接下来,定义一个chap_13_unit_test_simple.php程序文件,其中包含一系列简单的函数,如步骤1中讨论的add()sub()等。然后你可以定义一个简单的测试类,包含在SimpleTest.php中,如步骤2和步骤3中提到的。

假设phpunit在你的PATH中,从终端窗口,改变到包含为这个配方开发的代码的目录,并运行以下命令。

  1. phpunit SimpleTest SimpleTest.php

你应该看到以下输出。

编写一个简单的测试 - 图2

SimpleTest.php中进行修改,使测试失败(第4步)。

  1. public function testDiv()
  2. {
  3. $this->assertEquals(2, div(4, 2));
  4. $this->assertEquals(99, div(4, 0));
  5. }

这是修订后的产出。

编写一个简单的测试 - 图3

接下来,在chap_13_unit_test_simple.php中添加table()函数(步骤5),在SimpleTest.php中添加testTable()(步骤6)。重新运行单元测试,观察结果。

要测试一个类,将在chap_13_unit_test_simple.php中开发的函数复制到Demo类中(步骤7)。在对步骤8中建议的SimpleTest.php进行修改后,重新运行简单测试并观察结果。

测试数据库模型类

首先,创建一个要测试的示例类,VisitorOps,如本小节步骤2所示。现在你可以定义一个类,我们将调用SimpleDatabaseTest来测试VisitorOps。首先,使用require_once来加载要测试的类。(我们将在下一个实例中讨论如何加入自动加载!)然后定义关键属性,包括测试数据库配置和测试数据。你可以使用php7cookbook_test作为测试数据库。

  1. <?php
  2. use PHPUnit\Framework\TestCase;
  3. require_once __DIR__ . '/VisitorOps.php';
  4. class SimpleDatabaseTest extends TestCase
  5. {
  6. protected $visitorOps;
  7. protected $dbConfig = [
  8. 'driver' => 'mysql',
  9. 'host' => 'localhost',
  10. 'dbname' => 'php7cookbook_test',
  11. 'user' => 'cook',
  12. 'password' => 'book',
  13. 'errmode' => PDO::ERRMODE_EXCEPTION,
  14. ];
  15. protected $testData = [
  16. 'id' => 1,
  17. 'email' => 'test@unlikelysource.com',
  18. 'visit_date' => '2000-01-01 00:00:00',
  19. 'comments' => 'TEST',
  20. 'name' => 'TEST'
  21. ];
  22. }

接下来,定义setup(),插入测试数据,并确认最后一条SQL语句是INSERT。还应该检查返回值是否为正值。

  1. public function setup()
  2. {
  3. $this->visitorOps = new VisitorOps($this->dbConfig);
  4. $this->visitorOps->addVisitor($this->testData);
  5. $this->assertRegExp('/INSERT/', $this->visitorOps->getSql());
  6. }

之后,定义 teardown(),删除测试数据,并确认查询 id = 1 的结果为 FALSE

  1. public function teardown()
  2. {
  3. $result = $this->visitorOps->removeById(1);
  4. $result = $this->visitorOps->findById(1);
  5. $this->assertEquals(FALSE, $result);
  6. unset($this->visitorOps);
  7. }

首先测试的是findAll()。首先,确认结果的数据类型。你可以用current()取最上面的元素。我们确认有五个元素,其中有一个是name,并且其值与测试数据中的值相同。

  1. public function testFindAll()
  2. {
  3. $result = $this->visitorOps->findAll();
  4. $this->assertInstanceOf(Generator::class, $result);
  5. $top = $result->current();
  6. $this->assertCount(5, $top);
  7. $this->assertArrayHasKey('name', $top);
  8. $this->assertEquals($this->testData['name'], $top['name']);

下一个测试是针对findById()。它与testFindAll()几乎相同。

  1. public function testFindById()
  2. {
  3. $result = $this->visitorOps->findById(1);
  4. $this->assertCount(5, $result);
  5. $this->assertArrayHasKey('name', $result);
  6. $this->assertEquals($this->testData['name'], $result['name']);
  7. }

你不需要费心去测试 removeById(),因为这已经在 teardown()中完成了。同样,也不需要测试runSql(),因为这已经作为其他测试的一部分完成了。

使用MOCK类

首先,定义一个 VisitorService 服务类,如本小节步骤 2 和 3 所述。接下来,定义一个VisitorOpsMock模拟类,这将在步骤4至7中讨论。

您现在可以为服务类开发一个测试,即 VisitorServiceTest。请注意,您需要提供您自己的数据库配置,因为最好的做法是使用测试数据库而不是生产版本。

  1. <?php
  2. use PHPUnit\Framework\TestCase;
  3. require_once __DIR__ . '/VisitorService.php';
  4. require_once __DIR__ . '/VisitorOpsMock.php';
  5. class VisitorServiceTest extends TestCase
  6. {
  7. protected $visitorService;
  8. protected $dbConfig = [
  9. 'driver' => 'mysql',
  10. 'host' => 'localhost',
  11. 'dbname' => 'php7cookbook_test',
  12. 'user' => 'cook',
  13. 'password' => 'book',
  14. 'errmode' => PDO::ERRMODE_EXCEPTION,
  15. ];
  16. }

setup()中,创建一个服务的实例,并在原类的位置插入VisitorOpsMock

  1. public function setup()
  2. {
  3. $this->visitorService = new VisitorService($this->dbConfig);
  4. $this->visitorService->setVisitorOps(new VisitorOpsMock());
  5. }
  6. public function teardown()
  7. {
  8. unset($this->visitorService);
  9. }

在我们的测试中,从访问者列表中产生一个HTML表格,然后你可以寻找某些元素,提前知道会发生什么,因为你可以控制测试数据。

  1. public function testShowAllVisitors()
  2. {
  3. $result = $this->visitorService->showAllVisitors();
  4. $this->assertRegExp('!^<table>.+</table>$!', $result);
  5. $testData = $this->visitorService->getVisitorOps()->getTestData();
  6. foreach ($testData as $key => $value) {
  7. $dataWeWant = '!<td>' . $key . '</td>!';
  8. $this->assertRegExp($dataWeWant, $result);
  9. }
  10. }
  11. }

然后,你可能会希望尝试最后两个小节中建议的变化,使用匿名类作为Mock对象,以及使用Mock Builder。

更多…

其他断言测试对数字、字符串、数组、对象、文件、JSON和XML的操作,如下表所示。

分类 断言
General assertEquals(), assertFalse(), assertEmpty(), assertNull(), assertSame(), assertThat(), assertTrue()
Numeric assertGreaterThan(), assertGreaterThanOrEqual(), assertLessThan(), assertLessThanOrEqual(), assertNan(), assertInfinite()
String assertStringEndsWith(), assertStringEqualsFile(), assertStringStartsWith(), assertRegExp(), assertStringMatchesFormat(), assertStringMatchesFormatFile()
Array/iterator assertArrayHasKey(), assertArraySubset(), assertContains(), assertContainsOnly(), assertContainsOnlyInstancesOf(), assertCount()
File assertFileEquals(), assertFileExists()
Objects assertClassHasAttribute(), assertClassHasStaticAttribute(), assertInstanceOf(), assertInternalType(), assertObjectHasAttribute()
JSON assertJsonFileEqualsJsonFile(), assertJsonStringEqualsJsonFile(), assertJsonStringEqualsJsonString()
XML assertEqualXMLStructure(), assertXmlFileEqualsXmlFile(), assertXmlStringEqualsXmlFile(), assertXmlStringEqualsXmlString()

关于单元测试的精彩讨论,请看这里:https://en.wikipedia.org/wiki/Unit\_testing。

关于 composer.json 文件指令的更多信息,请看 https://getcomposer.org/doc/04-schema.md。

关于完整的断言列表,请看一下PHPUnit文档页:https://phpunit.de/manual/current/en/phpunit-book.html\#appendixes.assertions。

PHPUnit文档还在这里详细介绍了如何使用getMockBuilder():https://phpunit.de/manual/current/en/phpunit-book.html\#test-doubles.mock-objects。