hackpack / hackunit
Hack 的 xUnit 测试框架
Requires
- hhvm: ^3.11
- facebook/definition-finder: ^1.0
README
Hack 单元测试框架,用于和编写在 Hack 语言中的 Hack。
但是为什么呢?!
已经有许多测试框架可用,例如 PHPUnit 和 behat。 为什么你应该使用这个呢?
因为你喜欢 Hack 特定的功能!
使用 HackUnit,你可以使用内置的 async
关键字轻松地使用协作异步运行测试。
使用 HackUnit,你可以使用 yield
关键字轻松地以异步方式遍历测试数据。
使用 HackUnit,你可以使用 注解 指示测试方法。
HackUnit 的原始目标是使用 Hack 的严格模式编写测试框架。随着更多功能的添加,项目将继续保持这一目标。
安装
使用 Composer 安装 HackUnit
composer require --dev hackpack/hackunit
用法
HackUnit 可以通过包含的可执行脚本 bin/hackunit
从命令行运行。默认情况下,它将被链接到您的 vendor/bin
目录。
因此,调用 HackUnit 的最常见方法是
vendor/bin/hackunit path1 [path2] ...
其中 path1
、path2
等... 是每个要扫描测试套件的基路径/文件。如果指定的任何路径是目录,则将递归扫描目录。
某些命令行选项可以改变 HackUnit 的行为
--exclude="path/to/exclude"
: 不扫描提供的路径或任何路径下的文件。此选项可以多次给出以排除多个路径/文件。
测试套件
要定义测试套件,创建一个类并注释适当的 方法。
您可以检查 HackUnit 的测试文件以获取具体示例。
测试
使用 <<Test>>
属性 定义单个测试方法。测试的执行顺序没有保证。
每个测试方法必须接受正好 1 个参数,类型提示为 HackPack\HackUnit\Contract\Assert
。如果您将方法标记为测试但签名不匹配,则测试将不会运行。
测试方法可以是实例方法,也可以是类(静态)方法。
namespace My\Namespace\Test; use HackPack\HackUnit\Contract\Assert; class MySuite { <<Test>> public function testSomething(Assert $assert) : void { // Do some testing here! $assert->int(2)->not()->eq(3); $assert ->whenCalled(() ==> {throw new \Exception(‘bad error)}) ->willThrowClassWithMessage(\Exception::class, ‘bad error’) ; } }
异步
只需将异步关键字添加到测试方法中即可异步运行测试。
namespace My\Namespace\Test; use HackPack\HackUnit\Contract\Assert; class MyAsyncSuite { <<Test>> public async function testSomething(Assert $assert) : Awaitable<void> { // Make some async DB calls here as part of your test! $user = await get_user(); // Or maybe an async curl call $result = await get_external_user($user->id, 'api password'); $assert->string($result['user_name'])->is('expected username'); } }
所有这样的 async
测试都使用协作多任务处理(请参阅 async 文档),如果您的测试执行真正的 I/O 操作(数据库调用、网络调用等),则可以更快地运行整个测试套件。
设置
您可以让 HackUnit 在每个单个测试方法运行之前和/或在每个测试套件的任何测试方法运行之前运行一些方法。为此,用 <<Setup>>
属性 标记适当的方法。可以声明多个设置方法,但执行顺序没有保证。
每个设置方法(套件和测试)必须正好要求 0 个参数。如果您将方法标记为设置且它需要参数,则它将不会执行,并在报告中显示解析错误。
class MySuite { <<Setup(‘suite’)>> public function setUpSuite() : void { // Suite level Setup methods must be class (static) methods // Perform tasks before any tests in this suite are run } <<Setup(‘test’)>> public function setUpTest() : void { // Perform tasks just before each test in this suite is run } <<Setup>> public function setUpTestAgain() : void { // Multiple set up methods may be defined // If there are no parameters to the setup attribute, the method is treated like a test setup } }
套件设置方法仅在类中的任何测试方法运行之前运行一次。
测试设置方法在每个测试方法运行之前运行(因此可能多次运行)。
清理
您可以让HackUnit在每个测试方法运行后以及套件中的所有测试方法运行后运行一些方法。要这样做,请使用<<TearDown>>
属性标记适当的方法。可以声明多个清理方法,但执行顺序没有保证。
每个清理方法(套件和测试)都必须恰好接受0个参数。如果您标记一个方法为清理方法,而它需要参数,则不会执行该方法,并在报告中显示解析错误。
class MySuite { <<TearDown(‘suite’)>> public static function cleanUpAfterSuite() : void { // Suite level TearDown methods must be class (static) methods // Perform tasks after all tests in this suite are run } <<TearDown(‘test’)>> public function cleanUpAfterTest() : void { // Perform tasks just after each test in this suite is run } <<TearDown>> public function cleanUpMoarStuff() : void { // This is also a ‘test’ teardown method } }
套件清理方法仅在类中的所有测试方法运行之后运行一次。
测试清理方法在每个测试方法运行之后运行(因此可能多次运行)。
套件提供者
您的测试套件可能需要将参数传递给构造函数。为了告诉HackUnit如何构造您的测试套件,您必须定义至少一个套件提供者。套件提供者使用<<SuiteProvider>>
属性标记。
您可以为单个测试套件定义多个套件提供者。要这样做,您必须通过传递一个字符串参数给属性来为每个提供者命名(即<<SuiteProvider('提供者名称')>>
)。提供者名称没有限制,除了每个提供者名称必须是唯一的。
要为特定的测试使用特定的套件提供者,您必须将套件提供者的名称传递给测试属性。
class SuiteWithProviders { <<SuiteProvider('One')>> public static function() : this { $someDependency = new TestDoubleOne(); return new static($someDependency); } <<SuiteProvider('Two')>> public static function() : this { $someDependency = new TestDoubleTwo(); return new static($someDependency); } <<Test('One')>> public function testOne(Assert $assert) : void { // Do some assertions using TestDoubleOne } <<Test('Two')>> public function testTwo(Assert $assert) : void { // Do some assertions using TestDoubleTwo } }
断言
所有测试方法必须接受恰好一个类型为HackPack\HackUnit\Contract\Assert
的参数,该参数用于进行可测试的断言。此对象用于构建HackUnit将进行检查和报告的断言。
在所有下面的示例中,$assert
包含一个HackPack\HackUnit\Contract\Assert
的实例。
布尔断言
要断言关于bool
类型变量的内容,请调用$assert->bool($myBool)->is($expected)
。
数字断言
要断言关于int
和float
类型变量的内容,分别调用$assert->int($myInt)
和$assert->float($myFloat)
。生成的对象包含以下方法来执行适当的断言。
$assert->int($myInt)->eq($expected);
: 断言$myInt
与$expected
相同$assert->int($myInt)->gt($expected);
: 断言$myInt
大于$expected
$assert->int($myInt)->lt($expected);
: 断言$myInt
小于$expected
$assert->int($myInt)->gte($expected);
: 断言$myInt
大于或等于$expected
$assert->int($myInt)->lte($expected);
: 断言$myInt
小于或等于$expected
所有这些都可以通过在断言之前调用not()
来修改,以否定断言的含义。例如
$assert->int($myInt)->not()->eq($expected);
注意:此库只允许断言比较相同类型的数字。$assert->int(1)->eq(1.0);
会产生类型错误。
字符串断言
要断言关于string
类型变量的内容,请调用$assert->string($myString)
。生成的对象包含以下方法来执行适当的断言。
$assert->string($myString)->is($expected)
: 断言$myString === $expected
$assert->string($myString)->hasLength($int)
: 断言字符串长度为$int
$assert->string($myString)->matches($pattern)
: 断言正则表达式$pattern
与字符串匹配$assert->string($myString)->contains($subString)
: 断言$subString
是$myString
的子串$assert->string($myString)->containedBy($superString)
: 断言$myString
是$superString
的子串
以上所有断言都可以通过在断言之前调用 not()
来取反。例如
$assert->string($myString)->not()->containedBy($superString);
集合断言
要断言集合和数组,请调用 $assert->container($context)
。生成的对象包含以下方法以执行断言。
$assert->container($context)->isEmpty();
: 断言上下文中没有元素$assert->container($context)->contains($value);
: 断言上下文包含给定的值$assert->container($context)->containsAny($list);
: 断言上下文包含列表中提供的至少一个元素$assert->container($context)->containsAll($list);
: 断言上下文包含列表中提供的所有元素$assert->container($context)->containsOnly($list);
: 断言上下文包含列表中提供的所有元素且没有更多
上述所有 contains*
断言都接受一个可选的第二个参数,该参数必须是一个可调用函数。该可调用函数将用于比较上下文中的元素与提供的元素。如果传递给可调用函数的元素应被视为等价,则该可调用函数应返回 true
,否则应返回 false
。
键值集合
如果集合的键对于断言很重要,则应使用 $assert->keyedContainer($context)
。生成的对象包含以下方法以执行断言。
$assert->container($context)->contains($key, $value);
: 断言上下文在提供的键中包含的值与提供的值匹配$assert->container($context)->containsKey($key);
: 断言上下文包含提供的键$assert->container($context)->containsAny($list);
: 断言上下文包含列表中提供的至少一个元素,其中键和值都必须视为等价$assert->container($context)->containsAll($list);
: 断言上下文包含列表中提供的所有元素,其中键和值都必须视为等价$assert->container($context)->containsOnly($list);
: 断言上下文包含列表中提供的所有元素且没有更多,其中键和值都必须视为等价
上述所有断言都接受一个可选的第二个(在 contains
的情况下为第三个)参数,该参数必须是一个可调用函数。该可调用函数将用于比较上下文中的元素值与提供的元素。如果传递给可调用函数的值应被视为等价,则该可调用函数应返回 true
,否则应返回 false
。
混合断言
要针对任何类型的变量进行通用断言,请调用 $assert->mixed($context)
。生成的对象包含以下方法以执行适当的断言。
$assert->mixed($context)->isNull();
: 断言$context === null
$assert->mixed($context)->isBool();
: 断言$context
是类型bool
$assert->mixed($context)->isInt();
: 断言$context
是类型int
$assert->mixed($context)->isFloat();
: 断言$context
是类型float
$assert->mixed($context)->isString();
: 断言$context
是类型string
$assert->mixed($context)->isArray();
: 断言$context
是类型array
$assert->mixed($context)->isObject();
: 断言$context
是类型object
$assert->mixed($context)->isTypeOf($className)
: 断言$context instanceof $className
$assert->mixed($context)->looselyEquals($expected)
: 断言$context == $expected
注意松散比较$assert->mixed($context)->identicalTo($expected)
: 断言$context === $expected
注意严格比较
跳过测试
跳过特定测试方法的执行有两种方法
- 将属性
<<Skip>>
添加到测试方法或测试套件中。如果将<<Skip>>
属性添加到套件中,该类中的所有测试都将被跳过。 - 调用传递给测试方法的
Assert
对象的skip()
方法。
use \HackPack\HackUnit\Contract\Assert; <<Skip>> class SkippedSuite { // All methods here would be skipped } class MySuite { <<Test, Skip>> public function skippedTest(Assertion $assert) : void { // This will not be run and the test will be marked skip in the report. } <<Test>> public function skipFromMiddleOfTest(Assert $assert) : void { // This will be run $assert->skip(); // This will not be run and the test will be marked skip in the report. } }
数据提供者
您可以将某些方法标记为提供要迭代并传递给测试方法的列表。为此,使用 <<DataProvider('name')>>
属性标记数据提供方法。您必须按照所示命名数据提供者,以便 HackUnit 能够知道每个消耗数据的测试应使用哪个数据提供者。
class TestThatUsesData { <<DataProvider('csv values')>> public static function loadCsvValues(): AsyncIterator<array<string>> { $asynCsvLoader = new AsyncCsvLoader('/path/to/data.csv'); foreach($asyncCsvLoader await as $line) { yield $line; } } <<Test, Data('csv values')>> public function testCsvValues(Assert $assert, array<string> $line): void { // Do assertions using the data from the csv file. } <<DataProvider('simple count')>> public static function count(): Traversable<int> { return Vector{1, 2, 3, 4, 5}; } <<Test, Data('simple count')>> public function countingTest(Assert $assert, int $count): void { // This test will be run five times, once for each of the values in the vector above. } }
上面的示例演示了定义同步和非同步数据提供者。请注意返回值。
- 异步数据提供者 必须 返回
AsyncIterator<dataType>
。 - 非同步数据提供者 必须 返回
Traversable<dataType>
。 - 数据提供者的消费者 必须 将
dataType
作为第二个参数接受。
在上面的示例中,如果 csv 消费者函数的签名是 public function testCsvValues(Assert $assert, Traversable<string>): void
,则会出现 MalformedSuite
错误。尽管 array<string>
是 Traversable<string>
类型的一种,但 HackUnit 解析器比较类型的字符串表示,以确保永远不会将无效数据传递给测试方法。
HackUnit 加载测试的方法
命令行中指定的基本路径(s)内的所有文件都将使用 Fred Emmott 的 Definition Finder 库扫描以查找类定义。然后,将加载这些文件,并使用反射来确定哪些类是测试套件,以及套件中的每个任务由哪些方法执行。
谢谢 Fred!
严格模式下的所有文件!
嗯...并不完全是这样。
顶层代码必须使用 // partial
模式,因此 bin/hackunit
文件不在严格模式下。项目的其余部分也在,只有一个例外。必须在扫描测试套件之后动态加载测试套件文件。我看到的执行这种动态包含的唯一方法是在类方法中使用 include_once
,这在严格模式中是不允许的。这个唯一的例外用 /* HH_FIXME */
注释标记,该注释禁用了该行的类型检查器。
运行 HackUnit 的测试
HackUnit 使用 HackUnit 进行测试。从项目目录运行
hhvm /path/to/composer.phar test