Try Live
Add Docs
Rankings
Pricing
Docs
Install
Install
Docs
Pricing
More...
More...
Try Live
Rankings
Enterprise
Create API Key
Add Docs
Nette Tester
https://github.com/nette/tester
Admin
Nette Tester is a productive unit testing framework for PHP that provides assertion functions, code
...
Tokens:
24,226
Snippets:
155
Trust Score:
7.7
Update:
3 months ago
Context
Skills
Chat
Benchmark
84.4
Suggestions
Latest
Show doc for...
Code
Info
Show Results
Context Summary (auto-generated)
Raw
Copy
Link
# Nette Tester - PHP Testing Framework Nette Tester is a lightweight, standalone PHP testing framework designed for simplicity, speed, and process isolation. It provides a productive testing environment for PHP applications without external dependencies, running on PHP 8.0+. Each test executes in a completely isolated PHP process, preventing interference between tests and ensuring reliable parallel execution with 8 threads by default. The framework features annotation-driven test configuration, comprehensive assertion methods, and multi-engine code coverage support (Xdebug, PCOV, PHPDBG). It's self-hosting, using itself for testing, and offers multiple test organization styles from simple assertion scripts to xUnit-style TestCase classes. Tests can be run individually as PHP scripts or orchestrated through the test runner for parallel execution with sophisticated output formatting. ## Core Assertions ### Assert::same() - Strict Identity Comparison Validates that two values are identical using strict comparison (===). Checks type, value, and object identity. ```php <?php require __DIR__ . '/vendor/autoload.php'; use Tester\Assert; use Tester\Environment; Environment::setup(); // Basic type comparisons Assert::same(1, 1); // passes Assert::same('hello', 'hello'); // passes Assert::same(true, true); // passes // Object identity - must be same instance $obj = new stdClass; Assert::same($obj, $obj); // passes // Arrays must match exactly (keys, values, order) Assert::same(['a', 'b'], ['a', 'b']); // passes // These fail - different types or instances try { Assert::same(1, 1.0); // fails: int !== float } catch (Tester\AssertException $e) { echo $e->getMessage(); // "1.0 should be 1" } try { Assert::same(new stdClass, new stdClass); // fails: different instances } catch (Tester\AssertException $e) { echo $e->getMessage(); } // With custom description Assert::same(42, $calculatedValue, 'Calculation result mismatch'); ``` ### Assert::equal() - Loose Equality with Expectations Compares values with flexible rules: ignores object identity, array key order, and float precision differences. Supports Expect objects for complex assertions. ```php <?php use Tester\Assert; use Tester\Expect; use Tester\Environment; Environment::setup(); // Loose comparison - ignores minor differences Assert::equal(1.0, 1); // passes: type coercion Assert::equal(['a' => 1, 'b' => 2], ['b' => 2, 'a' => 1]); // passes: key order ignored // Object comparison - ignores instance identity $obj1 = new stdClass; $obj1->id = 5; $obj2 = new stdClass; $obj2->id = 5; Assert::equal($obj1, $obj2); // passes: same class and properties // Float precision - marginally different floats are equal Assert::equal(0.1 + 0.2, 0.3); // passes: handles floating point precision // Using Expect for complex structures $user = [ 'id' => 123, 'username' => 'john', 'email' => 'john@example.com', 'created_at' => new DateTime('2024-01-15'), 'roles' => ['user', 'editor'], ]; Assert::equal([ 'id' => Expect::type('int'), // type validation 'username' => 'john', // exact match 'email' => Expect::match('#.*@example\.com#'), // regex pattern 'created_at' => Expect::type(DateTime::class), // class validation 'roles' => Expect::type('array')->andCount(2), // chained expectations ], $user); // Match order and identity when needed Assert::equal([1, 2, 3], [3, 2, 1], matchOrder: false); // passes Assert::equal([1, 2, 3], [1, 2, 3], matchOrder: true); // passes Assert::equal([1, 2, 3], [3, 2, 1], matchOrder: true); // fails ``` ### Assert::exception() - Exception Testing Validates that a callable throws a specific exception type with optional message and code verification. ```php <?php use Tester\Assert; use Tester\Environment; Environment::setup(); class UserRepository { public function find(int $id) { if ($id <= 0) { throw new InvalidArgumentException('ID must be positive', 100); } if ($id === 999) { throw new RuntimeException('User not found'); } return ['id' => $id, 'name' => 'User ' . $id]; } } $repo = new UserRepository; // Basic exception type check Assert::exception( fn() => $repo->find(0), InvalidArgumentException::class ); // Check exception message (supports patterns) Assert::exception( fn() => $repo->find(-5), InvalidArgumentException::class, 'ID must be positive' ); // Pattern matching in message Assert::exception( fn() => $repo->find(-1), InvalidArgumentException::class, '%a%positive%a%' // wildcard pattern ); // Check exception code Assert::exception( fn() => $repo->find(0), InvalidArgumentException::class, 'ID must be positive', 100 ); // Capture exception for further inspection $e = Assert::exception( fn() => $repo->find(999), RuntimeException::class, 'User not found' ); // Now inspect the caught exception Assert::same('User not found', $e->getMessage()); Assert::type(RuntimeException::class, $e); ``` ### Assert::match() - Pattern Matching Matches strings against patterns with wildcards or regular expressions. Supports extensive wildcard syntax for flexible matching. ```php <?php use Tester\Assert; use Tester\Environment; Environment::setup(); // Wildcard patterns Assert::match('%a%', 'hello world'); // one or more chars Assert::match('Error: %a%', 'Error: File not found'); // partial match Assert::match('%d%', '12345'); // digits only Assert::match('%i%', '-42'); // signed integer Assert::match('%f%', '3.14159'); // floating point Assert::match('%h%', 'a1b2c3'); // hexadecimal // Complex patterns $error = "Fatal error: Uncaught Exception in file /path/to/file.php on line 42"; Assert::match('Fatal error: %a% in file %a% on line %d%', $error); // Optional wildcards (zero or more) Assert::match('Hello%s?%World', 'HelloWorld'); // %s?% matches zero spaces Assert::match('Hello%s?%World', 'Hello World'); // or multiple spaces // Multiline matching $output = "Line 1\nLine 2\nLine 3"; Assert::match('%A%Line 2%A%', $output); // %A% includes newlines // Regular expressions (must use ~ or # delimiters) Assert::match('~^[0-9]{4}-[0-9]{2}-[0-9]{2}$~', '2024-01-15'); // date format Assert::match('#^user_[a-z]+_\d+$#i', 'user_admin_123'); // case insensitive // Email validation Assert::match('#^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$#i', 'test@example.com'); // Combined wildcards $log = "2024-01-15 10:30:45 [ERROR] Database connection failed: timeout after 30s"; Assert::match('%d%-%d%-%d% %d%:%d%:%d% [%w%] %a%: %a%', $log); // File paths with directory separator Assert::match('path%ds%to%ds%file.php', 'path/to/file.php'); // Unix Assert::match('path%ds%to%ds%file.php', 'path\\to\\file.php'); // Windows ``` ### Assert::type() - Type Validation Validates that a value is of a specified built-in type, class, or interface. ```php <?php use Tester\Assert; use Tester\Environment; Environment::setup(); // Built-in types Assert::type('int', 42); Assert::type('integer', 42); // alias for int Assert::type('float', 3.14); Assert::type('string', 'hello'); Assert::type('bool', true); Assert::type('array', [1, 2, 3]); Assert::type('object', new stdClass); Assert::type('callable', fn() => true); Assert::type('resource', fopen('php://memory', 'r')); Assert::type('null', null); Assert::type('scalar', 'test'); // string, int, float, or bool // Special type: list (array with sequential integer keys starting from 0) Assert::type('list', [1, 2, 3]); // passes Assert::type('list', ['a' => 1, 'b' => 2]); // fails: associative array try { Assert::type('list', [1 => 'a', 2 => 'b']); // fails: doesn't start at 0 } catch (Tester\AssertException $e) { echo $e->getMessage(); } // Class and interface validation class User { public function __construct(public string $name) {} } interface Repository {} class UserRepository implements Repository {} $user = new User('John'); Assert::type(User::class, $user); Assert::type('object', $user); $repo = new UserRepository; Assert::type(UserRepository::class, $repo); Assert::type(Repository::class, $repo); // interface check // With DateTime $date = new DateTime; Assert::type(DateTime::class, $date); Assert::type(DateTimeInterface::class, $date); // Using in complex assertions $response = ['code' => 200, 'data' => ['user' => new User('Alice')]]; Assert::type('array', $response); Assert::type('int', $response['code']); Assert::type(User::class, $response['data']['user']); ``` ### Assert::contains() - Containment Validation Validates that a string contains a substring or an array contains a value (strict comparison). ```php <?php use Tester\Assert; use Tester\Environment; Environment::setup(); // String containment Assert::contains('world', 'hello world'); Assert::contains('PHP', 'I love PHP programming'); Assert::contains('', 'any string'); // empty string always contained // Array containment (strict comparison) Assert::contains(3, [1, 2, 3, 4, 5]); Assert::contains('apple', ['apple', 'banana', 'cherry']); // Strict type checking in arrays $values = [1, 2, 3, '4']; Assert::contains(3, $values); // passes try { Assert::contains('3', $values); // fails: '3' !== 3 (strict) } catch (Tester\AssertException $e) { echo "Strict comparison failed\n"; } // Object comparison in arrays $obj1 = new stdClass; $obj2 = new stdClass; $objects = [$obj1]; Assert::contains($obj1, $objects); // passes: same instance try { Assert::contains($obj2, $objects); // fails: different instance } catch (Tester\AssertException $e) { echo "Object identity check failed\n"; } // Practical examples $html = '<div class="container"><p>Welcome</p></div>'; Assert::contains('Welcome', $html); Assert::contains('class="container"', $html); $permissions = ['read', 'write', 'delete']; Assert::contains('write', $permissions); // With custom description Assert::contains('success', $apiResponse, 'API response should indicate success'); // Negation Assert::notContains('error', $apiResponse); Assert::notContains('password', $loggedData); ``` ### Assert::error() - Error and Warning Testing Validates that a callable generates specific PHP errors, warnings, or notices. ```php <?php use Tester\Assert; use Tester\Environment; Environment::setup(); // Single error expectation Assert::error( function () { trigger_error('Custom warning', E_USER_WARNING); }, E_USER_WARNING, 'Custom warning' ); // Test deprecated function call Assert::error( function () { trigger_error('Deprecated: Old API', E_USER_DEPRECATED); }, E_USER_DEPRECATED ); // Using constant name as string Assert::error( fn() => trigger_error('Notice message', E_USER_NOTICE), 'E_USER_NOTICE', 'Notice message' ); // Multiple errors in sequence Assert::error( function () { trigger_error('First warning', E_USER_WARNING); trigger_error('Second notice', E_USER_NOTICE); }, [ [E_USER_WARNING, 'First warning'], [E_USER_NOTICE, 'Second notice'], ] ); // Pattern matching in error messages Assert::error( fn() => trigger_error('Error in module X', E_USER_ERROR), E_USER_ERROR, '%a%module%a%' ); // Testing actual PHP warnings Assert::error( fn() => 1 / 0, // Division by zero in PHP 8+ E_WARNING ); // Assert no errors occurred Assert::noError(function () { $x = 1 + 1; echo $x; }); // Catching exception as error alternative Assert::error( fn() => throw new Exception('Test exception'), Exception::class, 'Test exception' ); ``` ### Assert::count() - Count Validation Validates the number of elements in an array or Countable object. ```php <?php use Tester\Assert; use Tester\Environment; Environment::setup(); // Array counting Assert::count(3, [1, 2, 3]); Assert::count(0, []); Assert::count(5, ['a', 'b', 'c', 'd', 'e']); // Associative arrays Assert::count(2, ['name' => 'John', 'age' => 30]); // Nested arrays $matrix = [ [1, 2, 3], [4, 5, 6], ]; Assert::count(2, $matrix); // 2 rows Assert::count(3, $matrix[0]); // 3 columns in first row // Countable objects class TaskList implements Countable { private array $tasks = []; public function add(string $task) { $this->tasks[] = $task; } public function count(): int { return count($this->tasks); } } $tasks = new TaskList; Assert::count(0, $tasks); $tasks->add('Task 1'); $tasks->add('Task 2'); Assert::count(2, $tasks); // With custom description Assert::count(10, $results, 'Expected exactly 10 search results'); // Practical usage in tests $users = fetchUsersFromDatabase(); Assert::count(5, $users, 'Should retrieve 5 users'); $filteredData = array_filter($data, fn($item) => $item['active']); Assert::count(3, $filteredData, 'Should have 3 active items'); ``` ## TestCase - xUnit Style Testing ### Basic TestCase Structure Organize tests using xUnit-style classes with setUp/tearDown lifecycle hooks and multiple test methods. ```php <?php /** * @testCase */ declare(strict_types=1); require __DIR__ . '/bootstrap.php'; use Tester\Assert; use Tester\TestCase; class UserManagerTest extends TestCase { private UserManager $manager; private PDO $db; protected function setUp() { // Runs before each test method $this->db = new PDO('sqlite::memory:'); $this->db->exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)'); $this->manager = new UserManager($this->db); } protected function tearDown() { // Runs after each test method $this->db = null; $this->manager = null; } public function testCreateUser() { $userId = $this->manager->create('John', 'john@example.com'); Assert::type('int', $userId); Assert::true($userId > 0); $user = $this->manager->find($userId); Assert::same('John', $user['name']); Assert::same('john@example.com', $user['email']); } public function testUpdateUser() { $userId = $this->manager->create('Jane', 'jane@example.com'); $this->manager->update($userId, 'Jane Doe', 'jane.doe@example.com'); $user = $this->manager->find($userId); Assert::same('Jane Doe', $user['name']); Assert::same('jane.doe@example.com', $user['email']); } public function testDeleteUser() { $userId = $this->manager->create('Bob', 'bob@example.com'); Assert::notNull($this->manager->find($userId)); $this->manager->delete($userId); Assert::null($this->manager->find($userId)); } /** * @throws InvalidArgumentException */ public function testCreateUserWithInvalidEmail() { // Method throws InvalidArgumentException $this->manager->create('Test', 'invalid-email'); } } // Required: instantiate and run the test case (new UserManagerTest)->run(); ``` ### TestCase with Data Providers Use data providers to run the same test with multiple input sets, either from methods or external files. ```php <?php /** * @testCase */ declare(strict_types=1); require __DIR__ . '/bootstrap.php'; use Tester\Assert; use Tester\TestCase; class ValidatorTest extends TestCase { private Validator $validator; protected function setUp() { $this->validator = new Validator; } /** * @dataProvider getEmailValidationData */ public function testEmailValidation(string $email, bool $expected) { $result = $this->validator->isValidEmail($email); Assert::same($expected, $result); } public function getEmailValidationData(): array { return [ ['valid@example.com', true], ['user.name@example.co.uk', true], ['user+tag@example.com', true], ['invalid@', false], ['@example.com', false], ['invalid email@example.com', false], ['', false], ]; } /** * @dataProvider getPasswordStrengthData */ public function testPasswordStrength(string $password, int $expectedScore) { $score = $this->validator->getPasswordStrength($password); Assert::same($expectedScore, $score); } public function getPasswordStrengthData(): array { return [ 'weak' => ['12345', 1], 'medium' => ['Password1', 2], 'strong' => ['P@ssw0rd!123', 3], 'very_strong' => ['c0mPL3x!P@ssW0rD#2024', 4], ]; } } (new ValidatorTest)->run(); ``` ## HTTP Testing ### HttpAssert - HTTP Request Testing Test HTTP endpoints with fluent interface for validating status codes, headers, and response bodies. ```php <?php require __DIR__ . '/bootstrap.php'; use Tester\Assert; use Tester\HttpAssert; use Tester\Environment; Environment::setup(); // Basic GET request $response = HttpAssert::fetch('https://api.example.com/users/123') ->expectCode(200) ->expectHeader('Content-Type', contains: 'application/json') ->expectBody(contains: '"id":123'); // POST request with authentication HttpAssert::fetch( 'https://api.example.com/users', method: 'POST', headers: [ 'Authorization' => 'Bearer token123', 'Content-Type' => 'application/json', ], body: '{"name":"John","email":"john@example.com"}' ) ->expectCode(201) ->expectHeader('Location') ->expectBody(contains: '"id"'); // Custom headers - both formats supported $response = HttpAssert::fetch( 'https://api.example.com/data', headers: [ 'X-API-Key' => 'secret123', // key-value format 'Accept: application/json', // string format 'User-Agent' => 'TestClient/1.0', ] ) ->expectCode(200); // Request with cookies HttpAssert::fetch( 'https://example.com/dashboard', cookies: [ 'session' => 'abc123xyz', 'user_pref' => 'dark_mode', ] ) ->expectCode(200) ->expectBody(contains: 'Welcome back'); // Follow redirects HttpAssert::fetch( 'https://example.com/old-url', follow: true ) ->expectCode(200); // Don't follow redirects HttpAssert::fetch( 'https://example.com/redirect', follow: false ) ->expectCode(302) ->expectHeader('Location', 'https://example.com/new-url'); // Status code validation with closure HttpAssert::fetch('https://api.example.com/health') ->expectCode(fn($code) => $code >= 200 && $code < 300) ->denyCode(fn($code) => $code >= 500); // Header validation patterns HttpAssert::fetch('https://api.example.com/data') ->expectHeader('Content-Type', 'application/json; charset=utf-8') // exact ->expectHeader('Server', contains: 'nginx') // contains ->expectHeader('X-Response-Time', matches: '%d% ms') // pattern ->denyHeader('X-Debug'); // must not exist // Body validation HttpAssert::fetch('https://api.example.com/users/1') ->expectBody(contains: '"status":"active"') ->expectBody(matches: '%A%"email":"%a%@%a%"%A%') ->denyBody(contains: 'password') ->denyBody(contains: 'secret'); // Custom body validation with closure HttpAssert::fetch('https://api.example.com/data') ->expectBody(function($body) { $data = json_decode($body, true); return isset($data['success']) && $data['success'] === true; }); // PUT request HttpAssert::fetch( 'https://api.example.com/users/123', method: 'PUT', headers: ['Content-Type' => 'application/json'], body: '{"name":"Updated Name"}' ) ->expectCode(200); // DELETE request HttpAssert::fetch( 'https://api.example.com/users/123', method: 'DELETE', headers: ['Authorization' => 'Bearer token123'] ) ->expectCode(204); // Testing error responses HttpAssert::fetch('https://api.example.com/invalid') ->expectCode(404) ->expectBody(contains: '"error":"Not Found"'); ``` ## DOM Query - HTML Testing ### DomQuery - CSS Selector Based HTML Querying Query and traverse HTML/XML documents using CSS selectors for testing web page structure and content. ```php <?php require __DIR__ . '/bootstrap.php'; use Tester\Assert; use Tester\DomQuery; use Tester\Environment; Environment::setup(); // Parse HTML string $html = ' <!DOCTYPE html> <html> <head><title>Test Page</title></head> <body> <article class="post featured" data-id="123"> <h1 class="title">Hello World</h1> <div class="meta"> <span class="author">John Doe</span> <time datetime="2024-01-15">January 15, 2024</time> </div> <div class="content"> <p>First paragraph.</p> <p>Second paragraph.</p> </div> <a href="/posts/123" class="read-more">Read more</a> </article> <aside class="sidebar"> <h2>Related Posts</h2> <ul> <li><a href="/posts/456">Post 1</a></li> <li><a href="/posts/789">Post 2</a></li> </ul> </aside> </body> </html> '; $dom = DomQuery::fromHtml($html); // Check element existence Assert::true($dom->has('article.post')); Assert::true($dom->has('h1.title')); Assert::true($dom->has('[data-id]')); Assert::false($dom->has('.non-existent')); // Find elements (returns array of DomQuery objects) $headings = $dom->find('h1'); Assert::count(1, $headings); Assert::same('Hello World', (string) $headings[0]); // Find by class $articles = $dom->find('article.post'); Assert::count(1, $articles); // Find by attribute $dataElements = $dom->find('[data-id]'); Assert::count(1, $dataElements); Assert::same('123', (string) $dataElements[0]['data-id']); // Find multiple elements $paragraphs = $dom->find('p'); Assert::count(2, $paragraphs); Assert::same('First paragraph.', (string) $paragraphs[0]); Assert::same('Second paragraph.', (string) $paragraphs[1]); // Complex selectors $featuredPosts = $dom->find('article.featured'); Assert::count(1, $featuredPosts); $sidebarLinks = $dom->find('aside.sidebar a'); Assert::count(2, $sidebarLinks); // Attribute selectors $links = $dom->find('a[href^="/posts"]'); // starts with /posts Assert::count(3, $links); // Check if element matches selector $article = $dom->find('article')[0]; Assert::true($article->matches('article')); Assert::true($article->matches('.post')); Assert::true($article->matches('.featured')); Assert::false($article->matches('div')); // Find closest ancestor (PHP 8.4+) if (PHP_VERSION_ID >= 80400) { $time = $dom->find('time')[0]; $closestDiv = $time->closest('div'); Assert::true($closestDiv->matches('.meta')); } // Testing table structure $tableHtml = ' <table class="data-table"> <thead> <tr> <th>Name</th> <th>Email</th> <th>Status</th> </tr> </thead> <tbody> <tr class="active"> <td>John</td> <td>john@example.com</td> <td><span class="badge success">Active</span></td> </tr> <tr class="inactive"> <td>Jane</td> <td>jane@example.com</td> <td><span class="badge danger">Inactive</span></td> </tr> </tbody> </table> '; $table = DomQuery::fromHtml($tableHtml); // Verify table structure Assert::true($table->has('table.data-table')); Assert::true($table->has('thead tr th')); $headers = $table->find('thead th'); Assert::count(3, $headers); $rows = $table->find('tbody tr'); Assert::count(2, $rows); // Check specific cells $activeRow = $table->find('tr.active td'); Assert::count(3, $activeRow); Assert::same('John', (string) $activeRow[0]); // Badge verification $badges = $table->find('.badge'); Assert::count(2, $badges); Assert::true($badges[0]->matches('.success')); Assert::true($badges[1]->matches('.danger')); // Testing forms $formHtml = ' <form id="login-form" method="post" action="/login"> <input type="text" name="username" required> <input type="password" name="password" required> <button type="submit">Login</button> </form> '; $form = DomQuery::fromHtml($formHtml); Assert::true($form->has('form#login-form')); Assert::true($form->has('input[type="text"][required]')); Assert::true($form->has('input[name="password"]')); Assert::true($form->has('button[type="submit"]')); $inputs = $form->find('input'); Assert::count(2, $inputs); ``` ## Test Organization Functions ### test() - Named Test Blocks Organize tests into named blocks with automatic setUp/tearDown support and clear output. ```php <?php declare(strict_types=1); require __DIR__ . '/bootstrap.php'; use Tester\Assert; use Tester\Environment; Environment::setup(); Environment::setupFunctions(); // Required for test() function // Global setUp runs before each test() block setUp(function () { echo "Setting up...\n"; }); // Global tearDown runs after each test() block tearDown(function () { echo "Cleaning up...\n"; }); test('basic arithmetic operations', function () { Assert::same(4, 2 + 2); Assert::same(10, 5 * 2); Assert::same(3, 9 / 3); }); test('string operations', function () { Assert::same('hello world', 'hello' . ' ' . 'world'); Assert::same('HELLO', strtoupper('hello')); Assert::same(5, strlen('hello')); }); test('array operations', function () { $arr = [1, 2, 3]; Assert::count(3, $arr); Assert::contains(2, $arr); Assert::same(6, array_sum($arr)); }); test('exception handling', function () { Assert::exception( fn() => throw new InvalidArgumentException('Test error'), InvalidArgumentException::class, 'Test error' ); }); // Test with object state class Calculator { private float $result = 0; public function add(float $n): self { $this->result += $n; return $this; } public function multiply(float $n): self { $this->result *= $n; return $this; } public function getResult(): float { return $this->result; } } test('calculator chaining', function () { $calc = new Calculator; $result = $calc->add(5)->add(3)->multiply(2)->getResult(); Assert::same(16.0, $result); }); test('calculator edge cases', function () { $calc = new Calculator; Assert::same(0.0, $calc->getResult()); $calc->add(10); Assert::same(10.0, $calc->getResult()); $calc->multiply(0); Assert::same(0.0, $calc->getResult()); }); ``` ### Environment - Test Environment Configuration Configure and control the test execution environment including error handling, skipping, and locking. ```php <?php declare(strict_types=1); require __DIR__ . '/bootstrap.php'; use Tester\Assert; use Tester\Environment; // Must be called first - sets up error handling and assertions Environment::setup(); // Skip test conditionally if (!extension_loaded('redis')) { Environment::skip('Redis extension is required for this test'); } if (PHP_VERSION_ID < 80100) { Environment::skip('This test requires PHP 8.1 or higher'); } // Lock to prevent parallel execution (for tests accessing shared resources) Environment::lock('database', __DIR__ . '/tmp'); // Example: Database test that needs exclusive access class DatabaseTest { private PDO $db; public function __construct() { // Lock ensures only one test accesses database at a time Environment::lock('test-database', __DIR__ . '/tmp'); $this->db = new PDO('sqlite:' . __DIR__ . '/tmp/test.db'); $this->db->exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)'); } public function testInsert() { $stmt = $this->db->prepare('INSERT INTO users (name) VALUES (?)'); $stmt->execute(['John']); Assert::same(1, $this->db->lastInsertId()); } } // Bypass final keywords for testing (useful for mocking) Environment::bypassFinals(); final class FinalClass { public function method() { return 'original'; } } // Now you can extend it in tests class TestableClass extends FinalClass { public function method() { return 'mocked'; } } $obj = new TestableClass; Assert::same('mocked', $obj->method()); // Load data from data provider (when using @dataProvider annotation) // This would be called inside a test file with @dataProvider annotation if (getenv('NETTE_TESTER_RUNNER')) { // Running under test runner with data provider try { $data = Environment::loadData(); // Use $data in test } catch (Exception $e) { // No data provider configured } } // Check if running under test runner if (getenv(Environment::VariableRunner)) { echo "Running under Nette Tester runner\n"; } // Get thread number in parallel execution $threadNum = getenv(Environment::VariableThread); if ($threadNum !== false) { echo "Running in thread $threadNum\n"; } // Access test annotations programmatically $annotations = Environment::getTestAnnotations(); if (isset($annotations['skip'])) { Environment::skip($annotations['skip']); } ``` ## File Mocking and Data Providers ### FileMock - Virtual File System Create virtual files in memory for testing file operations without touching the real filesystem. ```php <?php declare(strict_types=1); require __DIR__ . '/bootstrap.php'; use Tester\Assert; use Tester\FileMock; use Tester\Environment; Environment::setup(); // Create virtual file with initial content $file = FileMock::create('Initial content'); // Use with standard PHP file functions Assert::same('Initial content', file_get_contents($file)); // Write to file file_put_contents($file, "Line 1\n"); file_put_contents($file, "Line 2\n", FILE_APPEND); file_put_contents($file, "Line 3\n", FILE_APPEND); Assert::same("Line 1\nLine 2\nLine 3\n", file_get_contents($file)); // Read file line by line $handle = fopen($file, 'r'); $line1 = fgets($handle); $line2 = fgets($handle); fclose($handle); Assert::same("Line 1\n", $line1); Assert::same("Line 2\n", $line2); // Test INI file parsing $iniFile = FileMock::create(' [database] host = localhost port = 3306 user = root [cache] enabled = true ttl = 3600 ', 'ini'); $config = parse_ini_file($iniFile, true); Assert::same('localhost', $config['database']['host']); Assert::same(3306, $config['database']['port']); Assert::true($config['cache']['enabled']); // Test JSON file $jsonFile = FileMock::create('{"name":"John","age":30,"active":true}', 'json'); $data = json_decode(file_get_contents($jsonFile), true); Assert::same('John', $data['name']); Assert::same(30, $data['age']); Assert::true($data['active']); // Test CSV parsing $csvFile = FileMock::create("name,email,status\nJohn,john@example.com,active\nJane,jane@example.com,inactive", 'csv'); $handle = fopen($csvFile, 'r'); $headers = fgetcsv($handle); $row1 = fgetcsv($handle); $row2 = fgetcsv($handle); fclose($handle); Assert::same(['name', 'email', 'status'], $headers); Assert::same(['John', 'john@example.com', 'active'], $row1); Assert::same(['Jane', 'jane@example.com', 'inactive'], $row2); // Test file_exists and unlink Assert::true(file_exists($file)); unlink($file); Assert::false(file_exists($file)); // Testing a file processor class class LogParser { public function parse(string $file): array { $entries = []; $handle = fopen($file, 'r'); while (($line = fgets($handle)) !== false) { if (preg_match('/\[(.*?)\] (.*)/', $line, $matches)) { $entries[] = [ 'level' => $matches[1], 'message' => $matches[2], ]; } } fclose($handle); return $entries; } } $logFile = FileMock::create(" [ERROR] Database connection failed [WARNING] Cache miss for key: user_123 [INFO] Request processed successfully "); $parser = new LogParser; $entries = $parser->parse($logFile); Assert::count(3, $entries); Assert::same('ERROR', $entries[0]['level']); Assert::same('Database connection failed', $entries[0]['message']); ``` ### DataProvider - External Test Data Load test data from external INI or PHP files with query filtering for parameterized testing. ```php <?php /** * @dataProvider data/users.ini */ declare(strict_types=1); require __DIR__ . '/bootstrap.php'; use Tester\Assert; use Tester\Environment; Environment::setup(); // Load data inside test $data = Environment::loadData(); // Example users.ini: // [user1] // name = "John Doe" // email = "john@example.com" // age = 30 // active = true // // [user2] // name = "Jane Smith" // email = "jane@example.com" // age = 25 // active = false Assert::type('array', $data); Assert::type('string', $data['name']); Assert::type('string', $data['email']); Assert::type('int', $data['age']); Assert::type('bool', $data['active']); // Validate user data class UserValidator { public function validate(array $data): bool { return !empty($data['name']) && filter_var($data['email'], FILTER_VALIDATE_EMAIL) && $data['age'] > 0; } } $validator = new UserValidator; Assert::true($validator->validate($data)); ``` ## Advanced Patterns ### Expect - Complex Structure Validation Create chainable expectations for validating complex data structures within Assert::equal(). ```php <?php require __DIR__ . '/bootstrap.php'; use Tester\Assert; use Tester\Expect; use Tester\Environment; Environment::setup(); // API response validation $apiResponse = [ 'status' => 'success', 'code' => 200, 'data' => [ 'user' => [ 'id' => 12345, 'username' => 'john_doe', 'email' => 'john@example.com', 'created_at' => new DateTime('2024-01-15 10:30:00'), 'profile' => [ 'avatar' => 'https://example.com/avatar.jpg', 'bio' => 'Software developer', ], 'roles' => ['user', 'editor'], 'settings' => [ 'notifications' => true, 'theme' => 'dark', ], ], 'token' => 'abc123xyz789', ], 'timestamp' => 1705315800, ]; Assert::equal([ 'status' => 'success', 'code' => Expect::type('int')->andSame(200), 'data' => [ 'user' => [ 'id' => Expect::type('int'), 'username' => Expect::match('[a-z_]+'), 'email' => Expect::match('#^[^@]+@[^@]+\.[^@]+$#'), 'created_at' => Expect::type(DateTime::class), 'profile' => [ 'avatar' => Expect::match('%a%.jpg'), 'bio' => Expect::type('string'), ], 'roles' => Expect::type('array')->andCount(2), 'settings' => Expect::type('array'), ], 'token' => Expect::match('%h%'), // hex string ], 'timestamp' => Expect::type('int'), ], $apiResponse); // Database result validation $dbRow = [ 'id' => 1, 'username' => 'admin', 'password_hash' => '$2y$10$abcdefghijklmnopqrstuv', 'last_login' => '2024-01-15 14:30:00', 'login_count' => 42, 'is_active' => 1, ]; Assert::equal([ 'id' => Expect::type('int'), 'username' => Expect::type('string')->andContains('admin'), 'password_hash' => Expect::match('$2y$%a%'), // bcrypt hash 'last_login' => Expect::match('%d%-%d%-%d% %d%:%d%:%d%'), 'login_count' => Expect::type('int'), 'is_active' => Expect::that(fn($v) => $v === 0 || $v === 1), ], $dbRow); // Custom validation with closure $product = [ 'id' => 123, 'name' => 'Laptop', 'price' => 999.99, 'stock' => 5, ]; Assert::equal([ 'id' => Expect::type('int'), 'name' => Expect::type('string'), 'price' => Expect::that(fn($price) => $price > 0 && $price < 10000), 'stock' => Expect::that(fn($stock) => $stock >= 0), ], $product); // Chaining multiple expectations $data = [ 'items' => [1, 2, 3, 4, 5], 'total' => 15, ]; Assert::equal([ 'items' => Expect::type('array')->andCount(5), 'total' => Expect::type('int')->andSame(15), ], $data); // Testing nested arrays with Expect $nested = [ 'level1' => [ 'level2' => [ 'level3' => [ 'value' => 'deep', 'count' => 3, ], ], ], ]; Assert::equal([ 'level1' => [ 'level2' => [ 'level3' => [ 'value' => Expect::type('string'), 'count' => Expect::type('int')->andSame(3), ], ], ], ], $nested); ``` ## Summary Nette Tester provides a comprehensive testing solution for PHP applications with three primary use cases: unit testing with rich assertions, integration testing with TestCase classes and data providers, and HTTP/DOM testing for web applications. The framework excels at testing individual functions with Assert methods, organizing complex test suites with TestCase lifecycle hooks, and validating API responses and HTML output through HttpAssert and DomQuery helpers. Integration patterns are straightforward across all test styles. Simple tests use direct assertions in `.phpt` files executable as PHP scripts. Complex test suites leverage TestCase classes with setUp/tearDown methods and external data providers from INI or PHP files. The framework integrates seamlessly with CI/CD pipelines through multiple output formats (TAP, JUnit XML) and parallel execution capabilities. Code coverage collection works transparently with Xdebug, PCOV, or PHPDBG, aggregating results across parallel test processes. Tests can be run individually during development or orchestrated through the test runner for full suite execution with sophisticated filtering, caching, and result reporting.