nette-tester
Use this skill whenever writing, modifying, or running .phpt test files with Nette Tester. Invoke for any task involving Tester\Assert methods (Assert::same, Assert::match, Assert::exception, etc.), test bootstrap setup, vendor/bin/tester commands, or debugging failing test output (.expected/.actual files). Also use when the user needs to write tests for a Nette project and asks about test structure, the test() function, testException(), or assertion methods. This skill is specifically for Nette Tester – do not use for PHPUnit, Pest, Jest, Vitest, or other testing frameworks, and do not use for general PHP testing without Nette context.
What this skill does
## Testing with Nette Tester
Nette Tester is a testing framework for PHP. Test files use the `.phpt` extension.
```shell
composer require nette/tester --dev
```
### Bootstrap File
The bootstrap file should set up the Tester environment and enable helper functions:
```php
<?php declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
Tester\Environment::setup();
Tester\Environment::setupFunctions(); // enables test(), testException(), testNoError(), setUp()
```
### Basic Test Structure
```php
<?php declare(strict_types=1);
use Tester\Assert;
require __DIR__ . '/../bootstrap.php';
test('Calculator adds numbers correctly', function () {
$calc = new App\Model\Calculator;
Assert::same(5, $calc->add(2, 3));
});
test('Calculator throws on division by zero', function () {
$calc = new App\Model\Calculator;
Assert::exception(
fn() => $calc->divide(10, 0),
\DivisionByZeroError::class,
);
});
```
Key points:
- Use the `test()` function for each test case
- The first parameter of `test()` should be a clear description of what is being tested
- Do not add comments before `test()` calls - the description parameter serves this purpose
- Group related tests in the same file
- Test file naming: `{ClassName}.phpt` or `{ClassName}.{feature}.phpt`
### Assertions Overview
- `Assert::same($expected, $actual)` - strict identity (`===`)
- `Assert::notSame($expected, $actual)` - not strictly equal
- `Assert::equal($expected, $actual)` - loose comparison (ignores object identity, array key order, float epsilon)
- `Assert::notEqual($expected, $actual)`
- `Assert::true($actual)`, `Assert::false($actual)`, `Assert::null($actual)`, `Assert::notNull($actual)`
- `Assert::truthy($actual)`, `Assert::falsey($actual)`
- `Assert::contains($needle, $haystack)` - checks substring or array element; **avoid for testing output** (see warning below)
- `Assert::notContains($needle, $haystack)`
- `Assert::hasKey($key, $array)`, `Assert::hasNotKey($key, $array)`
- `Assert::count($count, $value)`
- `Assert::type($type, $value)` - class/interface or built-in type (`'string'`, `'int'`, `'list'`, etc.)
- `Assert::match($pattern, $actual)` - pattern matching with placeholders (see below)
- `Assert::matchFile($file, $actual)` - pattern loaded from file
- `Assert::exception($fn, $class, $message, $code)` - asserts exception is thrown
- `Assert::error($fn, $type, $message)` - asserts PHP error/warning/deprecation is generated
- `Assert::noError($fn)` - asserts no errors or exceptions
**Warning about Assert::contains:** Do not use `Assert::contains()` for testing generated output (HTML, text, etc.). It only checks for a substring - the test will pass even if the output contains errors or is completely broken, as long as the needle appears somewhere. Use `Assert::match()` or `Assert::matchFile()` instead, which verify the entire structure of the output.
### Pattern Matching with Assert::match
`Assert::match($pattern, $actual)` compares a string against a pattern with placeholders. `Assert::matchFile($file, $actual)` works the same way but loads the pattern from a file.
Available placeholders:
| Pattern | Meaning |
|---------|---------|
| `%a%` | one or more of anything except end of line |
| `%a?%` | zero or more of anything except end of line |
| `%A%` | one or more of anything including end of line |
| `%A?%` | zero or more of anything including end of line |
| `%s%` / `%s?%` | one or more / zero or more whitespace (except EOL) |
| `%S%` / `%S?%` | one or more / zero or more non-whitespace |
| `%c%` | a single character (except end of line) |
| `%d%` / `%d?%` | one or more / zero or more digits |
| `%i%` | signed integer |
| `%f%` | floating point number |
| `%h%` | one or more HEX digits |
| `%w%` | one or more alphanumeric characters |
| `%ds%` | directory separator (`/` or `\`) |
| `%%` | literal `%` character |
Important behavior:
- **Ungreedy matching** - placeholders match as little text as possible, so `%a%` captures the shortest possible string
- **Line endings are normalized** - `\r\n` and `\n` are treated as equivalent, so tests work cross-platform
- **Trailing whitespace is ignored**
- Patterns can also be raw regexps delimited by `~` or `#`
```php
Assert::match('<div class="item">%a%</div>', $html);
// For larger patterns, use NOWDOC syntax
Assert::match(<<<'XX'
<html>
<body>%A%</body>
</html>
XX, $html);
// Or load the pattern from a file (supports the same placeholders)
Assert::matchFile(__DIR__ . '/expected/output.html', $actual);
```
When `Assert::matchFile()` fails, the expected and actual output are written to the test output directory as `.expected` and `.actual` files.
### Testing Exceptions
For simple single-call exceptions, use the concise `fn()` style:
```php
Assert::exception(
fn() => Arrays::pick($arr, 'undefined'),
Nette\InvalidArgumentException::class,
"Missing item '%s%'.",
);
```
The `Assert::exception()` method:
1. First parameter: A closure that should throw the exception
2. Second parameter: Expected exception class
3. Third parameter (optional): Expected exception message, can contain match placeholders (`%a%`, `%s%`, etc.)
For testing PHP errors and deprecations:
```php
Assert::error(
fn() => $object->deprecatedMethod(),
E_USER_DEPRECATED,
'This method is deprecated',
);
```
If the entire `test()` block is to end with an exception, you can use `testException()`:
```php
testException('throws exception for invalid input', function () {
$mapper = new FilesystemMapper(__DIR__ . '/fixtures');
$mapper->getAsset('missing.txt');
}, AssetNotFoundException::class, "Asset file 'missing.txt' not found at path: %a%");
```
### setUp() for Shared Setup
Use `setUp()` to run common initialization before each `test()` block:
```php
$db = null;
setUp(function () use (&$db) {
$db = new TestDatabase;
});
test('insert works', function () use (&$db) {
$db->table('user')->insert(['name' => 'John']);
Assert::count(1, $db->table('user')->fetchAll());
});
test('each test gets fresh setup', function () use (&$db) {
// setUp() runs again before this test
Assert::type(TestDatabase::class, $db);
});
```
### Essential Commands
```bash
# Run all tests with output
vendor/bin/tester tests/ -s
# Run tests in specific directory
vendor/bin/tester tests/filters/ -s
# Run in parallel (8 threads)
vendor/bin/tester tests/ -j 8
# Run specific test file directly
php tests/common/Engine.phpt
# Run with code coverage (requires Xdebug or PCOV)
vendor/bin/tester tests/ --coverage coverage.html --coverage-src app/
```
### Test Output Directory
When a test fails, Nette Tester writes the expected and actual output into an `output` directory next to the test files (e.g. `tests/Tracy/output/`). For each failing test `Foo.phpt`, you will find:
- `Foo.expected` - what the test expected to see
- `Foo.actual` - what was actually produced
**Always look at these files first** when investigating test failures. Comparing `.expected` vs `.actual` shows the exact difference and is much more informative than the short failure message printed by the runner.
### Online Documentation
For detailed information, use WebFetch on these URLs:
- [Nette Tester](https://tester.nette.org) – complete testing guide
- [Assertions](https://tester.nette.org/en/assertions) – all Assert methods
- [Test Annotations](https://tester.nette.org/en/test-annotations) – @testCase, @dataProvider, @skip
Related in Productivity
gitea-workflow
IncludedOrchestrate agile development workflows for Gitea repositories using the tea CLI. Use when working with Gitea-hosted repos and asking to 'run the workflow', 'continue working', 'what's next', 'complete the task cycle', 'start my day', 'end the sprint', 'implement the next task', or wanting guided step-by-step development assistance. Keywords: workflow, orchestrate, agile, task cycle, sprint, daily, implement, review, PR, standup, retrospective, gitea, tea.
microsoft-graph-gateway
IncludedRoute Microsoft Graph work in this workspace. Use when users want to read or write Outlook mail, calendar events, contacts, OneDrive or SharePoint files, Teams, Planner, To Do, users, groups, directory data, or arbitrary Microsoft Graph endpoints from VS Code. Prefer WorkIQ for common read scenarios. Use Microsoft Graph for write actions and gap-read scenarios that need exact Graph properties, filters, permissions, or endpoints.
copilotkit
IncludedUse when building with CopilotKit — setup, development, integrations, debugging, upgrading, or contributing. Routes to the appropriate specialized skill based on the task.
wordly-wisdom
IncludedProvides calibrated decision analysis using Charlie Munger-style multiple mental models, inversion, incentive mapping, circle-of-competence checks, misjudgment audits, second-order effects, and forecast updates. Use when the user asks for an oracle take, a hard call, a decision memo, a premortem, an outside view, a red-team, a sanity-check, what am I missing, think this through, or wants a strategy, hire, investment, plan, product, partnership, or major life choice analysed. Avoid for simple factual lookups or time-sensitive legal, medical, or market questions without fresh evidence.
swain-session
IncludedSession management and project status dashboard. Owns the full session lifecycle (start/work/close/resume), focus lane, bookmarks, worktree detection, and tab naming. Also serves as the project status dashboard — shows active epics, progress, actionable next steps, blocked items, tasks, GitHub issues, and recommendations. Worktree creation is deferred to swain-do task dispatch (SPEC-195). Triggers on: 'session', 'status', 'what's next', 'dashboard', 'overview', 'where are we', 'what should I work on', 'show me priorities', 'bookmark', 'focus on', 'session info'.
gandi
IncludedComprehensive Gandi domain registrar integration for domain and DNS management. Register and manage domains, create/update/delete DNS records (A, AAAA, CNAME, MX, TXT, SRV, and more), configure email forwarding and aliases, check SSL certificate status, create DNS snapshots for safe rollback, bulk update zone files, and monitor domain expiration. Supports multi-domain management, zone file import/export, and automated DNS backups. Includes both read-only and destructive operations with safety controls.