HackUnit
xUnit testing framework written in hacklang
Install / Use
/learn @HackPack/HackUnitREADME
HackUnit
Testing framework written in and for Hack.
But Why?!
There are already many testing frameworks available, such as PHPUnit and behat. Why should you use this one?
Because you like Hack specific features!
With HackUnit, you can easily run your tests using cooperative async with the built in async keyword.
With HackUnit, you can easily iterate through your test data in an async way using the yield keyword.
With HackUnit, you indicate test methods using annotations.
The original goal of HackUnit was to write a testing framework using Hack's strict mode. The project will stay consistent with this goal as more features are added.
Install
Install HackUnit using Composer:
composer require --dev hackpack/hackunit
Usage
HackUnit can be run from the command line using the included executable script bin/hackunit. By default, this will be symlinked in your vendor/bin directory.
Thus, the most common way to invoke HackUnit is:
vendor/bin/hackunit path1 [path2] ...
where path1, path2, etc... are each base paths/files to scan for test suites. If any specified path is a directory, the directory will be recursively scanned.
Some command line options exist to alter the behavior of HackUnit:
--exclude="path/to/exclude": Do not scan the file or any file under the path provided. This option may be given multiple times to exclude multiple paths/files.
Test Suites
To define a test suite, create a class and annotate the appropriate methods.
You may inspect HackUnit’s test files for concrete examples.
Tests
Individual test methods are defined using the
<<Test>> attribute.
Execution order of the tests is not guaranteed.
Each test method MUST accept exactly 1 parameter, with the type hint of HackPack\HackUnit\Contract\Assert.
If you mark a method as a test and the signature does not match, the test will not be run.
Test methods may be instance methods, or they may be class (static) methods.
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’)
;
}
}
Async
Running your tests async is as easy as adding the async keyword to your test method.
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');
}
}
All such async tests are run using cooperative multitasking (see the async documentation), allowing your entire test suite to run faster if your tests perform real I/O operations (DB calls, network calls, etc...).
Setup
You may have HackUnit run some methods before each individual test method is run and/or before any test method is run for the suite. To do so, mark the appropriate method with the <<Setup>> attribute. Multiple setup methods may be declared, but the execution order is not guaranteed.
Each setup method (both suite and test) MUST require exactly 0 parameters. If you mark a method as setup and it requires a parameter, it will not be executed and a parse error will be shown in the report.
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
}
}
Suite setup methods are run once, before any of the test methods in the class are run.
Test setup methods are run just before each test method is run (and thus are potentially run multiple times).
Teardown
You may have HackUnit run some methods after each individual test method is run and/or after all test methods are run for the suite. To do so, mark the appropriate method with the <<TearDown>> attribute. Multiple teardown methods may be declared, but the execution order is not guaranteed.
Each teardown method (both suite and test) MUST require exactly 0 parameters. If you mark a method as teardown and it requires a parameter, it will not be executed and a parse error will be shown in the report.
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
}
}
Suite tear down methods are run once, after all of the test methods in the class are run.
Test tear down methods are run just after each test method is run (and thus are potentially run multiple times).
Suite Providers
Your test suite may require parameters to be passed to the constructor. To tell HackUnit how to construct your test suite, you must define at least one Suite Provider. A Suite Provider is marked with the <<SuiteProvider>> attribute.
You may define multiple Suite Providers for a single test suite. To do so, you must label each one by passing in one string parameter to the attribute (i.e., <<SuiteProvider('name of provider')>>). There are no restrictions on the name of a provider except that each provider name must be unique.
To use a particular Suite Provider for a particular test, you must pass the name of the Suite Provider to the Test attribute.
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
}
}
Assertions
All test methods must accept exactly one parameter of type HackPack\HackUnit\Contract\Assert which should be used to make testable assertions. This object is used to build assertions that will be checked and reported by HackUnit.
In all examples below, $assert contains an instance of HackPack\HackUnit\Contract\Assert.
Bool Assertions
To make assertions about bool type variables, call $assert->bool($myBool)->is($expected).
Numeric Assertions
To make assertions about int and float type variables, call $assert->int($myInt) and $assert->float($myFloat) respectively. The resulting object contains the following methods to actually perform the appropriate assertion.
$assert->int($myInt)->eq($expected);: Assert that$myIntis identical to$expected$assert->int($myInt)->gt($expected);: Assert that$myIntis greater than$expected$assert->int($myInt)->lt($expected);: Assert that$myIntis less than$expected$assert->int($myInt)->gte($expected);: Assert that$myIntis greater than or equal to$expected$assert->int($myInt)->lte($expected);: Assert that$myIntis less than or equal to$expected
All of the above may be modified with a call to not() before the assertion to negate the meaning of the assertion. For example:
$assert->int($myInt)->not()->eq($expected);
Note: This library only allows assertions to compare identical numeric types. $assert->int(1)->eq(1.0); produces a type error.
String Assertions
To make assertions about string type variables, call $assert->string($myString). The resulting object contains the following methods to actually perform the appropriate assertion.
$assert->string($myString)->is($expected): Assert that$myString === $expected$assert->string($myString)->hasLength($int): Assert that the string has a length of$int$assert->string($myString)->matches($pattern): Assert that the regular expression contained in$patternmatches the string$assert->string($myString)->contains($subString): Assert that$subStringis a substring of$myString$assert->string($myString)->containedBy($superString): Assert that$myStringis a substring of$superString
All of the above assertions may be negated by calling `not
Related Skills
node-connect
349.2kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
109.5kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
349.2kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
349.2kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
