Skip to main content

Write Tests

Unitary tests are structured around groups.

A group() defines what is being tested and acts as a container for one or more tests. Inside a group, you create individual tests using expect() or check().

Each test describes a specific behavior and can be completed by calling validate() or assert().

Read more

A test file consists of one or more group() blocks. Each group focuses on a single area of logic and should contain multiple independent tests.

Think of it like this:

  • group() → defines what is being tested
  • expect() / check() → defines a single test
  • validate() / assert() → executes and reports the test

Each group runs in isolation, allowing you to safely include setup logic without leaking side effects between tests. Tests created with check() also run in isolation, ensuring that conditional logic does not affect other tests.


Writing Tests in Unitary

A Unitary test file can include one or more group(), each serving as a container for tests that validate a specific subject. Each group runs within its own isolated scope, allowing you to safely add business logic while keeping side effects and dependencies contained. You can add multiple tests within each group.

use MaplePHP\Unitary\{Expect,TestCase};

group("HTTP Request", function(TestCase $case) {

$request = new Request("GET", "https://example.com/?id=1&slug=hello");

// Test 1
$case->expect($request->getPort())
->isEqualTo(80)
->validate("Default HTTP port must be resolved correctly.");

// Test 2
$case->expect($request->getMethod())
->isRequestMethod()
->validate();

// Test 3
$case->expect($request->getUri()->getQuery())
->hasQueryParam("id", 1)
->hasQueryParam("slug", "hello")
->validate();
});

Execute

php vendor/bin/unitary

Response

Unitary CLI response


Descriptions

Descriptions are optional and useful when the purpose of a validation isn’t immediately obvious.

use MaplePHP\Unitary\{Expect,TestCase};

group("Validating API Response", function(TestCase $case) {

$json = '{"response":{"status":404,"message":"ok"}}';

$case->expect($json)
->isJson()
->hasJsonValueAt("response.status", 200)
->describe("Response status must be 200")
->validate("API response status");
});
  • describe(): Adds an optional description to clarify each validation. You may chain multiple descriptions.
  • validate(): Optionally defines a heading for the test.

Response

Unitary CLI response


Check against

Use check() when you want to isolate a test or introduce conditional logic.

use MaplePHP\Unitary\{TestCase,Expect};

group("Check against example", function(TestCase $case) {

$case->check(function (Expect $expect) {

$json = $apiClient->get("/users");

$expect->against($json)
->isJson()
->hasJsonValueAt("response.status", 200);

}, "Validate API response");

});

Early exit in tests

Use check() when a test consists of multiple dependent steps in isolation.

A check() block is reported as a single test, even if it contains several validations.

When an assert() fails, it triggers a soft stop, immediately halting execution within the block. This prevents cascading failures and highlights the root cause.

use MaplePHP\Unitary\{TestCase, Expect};

group("API response validation", function (TestCase $case) {

// ONE test
$case->check(function (Expect $expect) use ($apiClient) {

$response = $apiClient->get("/users/123");

// Step 1
$expect->against($response->getStatusCode())
->isEqualTo(200)
->assert("API must return 200 before validating response body");

$data = json_decode($response->getBody(), true);

// Step 2 (only runs if step 1 passed)
$expect->against($data)
->isArray()
->assert("Response body must be valid JSON");

// Step 3 (only runs if step 2 passed)
$expect->against($data)
->hasKey("id")
->hasKey("email")
->assert();

}, "Validate API response");

});

Abort test group on failure

A hard stop aborts the current group immediately. No further validations or logic inside that group will run. Execution will continue with the next group in the same test file, if it exists.

You may chain assert() as the final step instead of validate(). If the validation fails, it triggers a hard stop, immediately aborting the group.

A check() block counts as a single test. You may perform multiple validations inside it, but they all belong to the same isolated test flow.

use MaplePHP\Unitary\{Expect,TestCase};

group("Validating API Response", function(TestCase $case) {

$json = $apiClient->get("/users");

// Test 1
$case->expect($json)
->isJson()
->hasJsonValueAt("response.status", 200)
->assert("API response status");

// Test 2
$case->check(function(Expect $expect) {
$request = request();

$case->against($request->getUri()->getQuery())
->hasQueryParam("id", 1)
->hasQueryParam("slug", "hello");

})->assert("Request query params must be valid");

});

Response

Unitary CLI response


Debug output

Unitary is built to make testing feel natural. You can use print_r(), var_dump(), or echo, and Unitary will capture and display the output neatly in the CLI.

use MaplePHP\Unitary\{Expect,TestCase};

group("Debug example", function(TestCase $case) {
print_r(['milk', 'cheese', 'bread']);
});

Note: Output generated outside of a group() may not appear in the CLI stream. If needed, call die() or exit() after printing.

Response

Unitary CLI response


Quick custom validations

When a test requires logic not covered by built-in expectations, you can define custom validations directly.

A native assert() inside a group creates a hard stop. Inside a validation callback, it becomes a soft stop and the validation is still reported.

use MaplePHP\Unitary\{Expect,TestCase};

group("Preconditions", function(TestCase $case) {

assert(1 === 1, "Setup is invalid 1");

$case->check($userData, function(Expect $expect) {
assert(1 === 1, "Setup is invalid 2");
});

$case->check(function(Expect $expect) {
return $expect->val() === 1;
}, "Test json validation");
});

You now know how to create and structure Unitary tests, from simple validations to advanced grouped assertions. For a complete list of validation methods, see the Validation API.