Nette Tester Documentation

repository·master·Indexed 19 days ago

https://github.com/nette/tester

A productive unit testing framework for PHP used by the Nette Framework. It features an exception-based assertion model via the Tester\Assert class, support for testing HTML content with DomQuery, and a command-line runner with parallel execution and code coverage analysis. The framework supports process isolation, custom annotations like @dataProvider, and the ability to bypass final keywords using a FileMutator stream wrapper.

Tokens
8K
Snippets
27
Records
36
Agent score
64%

What's inside Nette Tester

  1. The Runner-to-Child communication bridge

    master

    Communication between the test runner (orchestrator) and the child processes (individual tests) happens through two channels:

    Exit Codes (Child $\rightarrow$ Runner)

    Children report outcomes via process exit codes:

    • CodeOk: Success
    • CodeFail: An AssertException occurred
    • CodeError: A fatal error or other Throwable occurred
    • CodeSkip: The test was skipped

    Environment Variables (Runner $\rightarrow$ Child)

    Configuration is passed to child processes via environment variables:

    • NETTE_TESTER_RUNNER
    • NETTE_TESTER_THREAD
    • NETTE_TESTER_COVERAGE / NETTE_TESTER_COVERAGE_ENGINE
    • NETTE_TESTER_COLORS (controls ANSI color output in the child)
  2. How code coverage is merged

    master

    In a parallel execution environment, each child process collects its own coverage data. Upon shutdown, every child process attempts to merge its data into a single shared coverage file.

    To prevent corruption, the merge uses flock(LOCK_EX) and performs a recursive replacement: array_replace_recursive($negative, $original, $positive). This ensures that 'covered' (positive) data takes precedence over 'uncovered' (negative) data across all processes.

  3. Process isolation and TestCase execution phases

    master

    Every test runs in its own isolated PHP process using proc_open. For files using the @testCase annotation, the runner follows a two-phase execution model:

    1. Listing Phase: The runner executes the file with a special sentinel (--method=nette-tester-list-methods) to retrieve a list of all test* methods. This list is cached in a temporary directory to speed up subsequent runs.
    2. Execution Phase: Each identified method is scheduled as a separate individual job (e.g., --method=testFoo).

    Scheduling Logic: When running with a temporary directory, the runner sorts jobs based on their previous results to optimize developer workflow: Prepared (new tests) run first, followed by Failed tests, then Passed tests, and finally Skipped tests.

  4. How `Assert::match()` handles regex vs wildcard masks

    master

    The Assert::match() and isMatching() methods support two different pattern grammars, determined by a delimiter heuristic:

    1. PCRE Regex: If the pattern starts and ends with ~ or # (including optional flags), it is treated as a standard PCRE regular expression.
    2. Wildcard Mask: Otherwise, it is treated as a wildcard mask where special characters like %a%, %A%, %d%, and %h% are translated into regex fragments, and all other characters are escaped via preg_quote.

    Warning: Be careful with literal masks that happen to be wrapped in # or ~, as they will be misread as regex patterns.

    // Treated as regex
    Assert::match('~^prefix.*$~', $value);
    Assert::match('#pattern#', $value);
    
    // Treated as wildcard mask
    Assert::match('%prefix%suffix%', $value);
  5. How test annotations and dispatching work

    master

    Nette Tester uses a two-step process to handle test metadata and execution:

    1. Static Annotation Parsing

    Metadata (like @dataProvider, @multiple, or @testCase) is parsed from the file's first docblock using regex before the file is actually executed. This allows the runner to decide which tests to skip or create variants for without loading the full environment.

    2. Dispatch via Reflection

    Execution is handled through convention-based dispatching:

    • initiate* methods: The runner scans for methods matching the pattern initiate<AnnotationName>. These methods can return null (keep the test), a Test object (replace it), or an array of Test objects (to fan out one file into multiple variants, such as with @dataProvider).
    • assess* methods: After a job finishes, the runner scans for assess<AnnotationName> methods (e.g., for @exitCode, @httpCode, or @outputMatch) to determine if the result meets the requirements defined in the annotations.
  6. How `FileMutator` enables `bypassFinals()`

    master

    The FileMutator class allows Nette Tester to bypass final keywords in PHP code. It achieves this by registering itself as a file:// stream wrapper.

    How it works: When a .php file is included (opened in rb mode), the wrapper intercepts the request, reads the source, runs it through mutators (like bypassFinals, which tokenizes the code and removes T_FINAL tokens), writes the modified code to a temporary file, and serves that instead.

    Implementation Note: To avoid infinite recursion, the class uses a native() method to temporarily restore the original stream wrapper before performing any standard filesystem operations (like fopen or stat), and re-registers itself in a finally block.

  7. How the assertion model works in Nette Tester

    master

    Nette Tester uses an exception-based assertion model that is designed to be 'soft-by-default' and catchable.

    Key behaviors:

    • Assertion Counting: Assert::$counter increments at the start of every assertion. This allows the Environment to detect if a test finished without executing any assertions (the 'forgot-assert' guard).
    • Failure Handling: Assert::fail() throws an AssertException by default. However, during the shutdown phase, Environment installs a handler via Assert::$onFailure to catch late assertions and report them without crashing the process.
    • Catchable Exceptions: AssertException is intentionally catchable. The framework uses this to implement features like Assert::notEqual (which catches the exception to invert a comparison) and to ensure that a single failing method in a TestCase doesn't prevent other test* methods from running.
    • Output-Match Tests: When using @outputMatch or @outputMatchFile annotations, the 'forgot-assert' guard is automatically disabled because these tests verify behavior via captured output rather than explicit assertion calls.
  8. Configure Code Coverage analysis

    master

    To find untested code, use Code-Coverage Analysis. This requires the Xdebug, PCOV, or PHPDBG extension. You can generate an HTML report using the --coverage and --coverage-src flags.

    tester . -c php.ini --coverage coverage.html --coverage-src /my/source/codes
  9. Write and run a basic test

    master

    To write a test, create a file with the .test.phpt extension. You must include a bootstrap file (e.g., src/bootstrap.php or vendor/autoload.php) to load your classes and the Nette Tester environment. Use Tester\Assert`` to perform assertions.

    Example test file greeting.test.phpt:

    require 'src/bootstrap.php';
    
    use Tester\Assert;
    
    $h = new Greeting;
    
    // use an assertion function to test say()
    Assert::same('Hello John', $h->say('John'));

    Run the tests using the tester command in your terminal.

    require 'src/bootstrap.php';
    
    use Tester\Assert;
    
    $h = new Greeting;
    
    Assert::same('Hello John', $h->say('John'));
  10. Understand the Test class and its result states

    master

    The Tester\Runner\Test class is an immutable value object representing a single test case and its execution outcome. A test can exist in one of four states, defined by integer constants:

    • Prepared (0): The test has been initialized but not yet executed.
    • Failed (1): The test execution resulted in a failure.
    • Passed (2): The test execution was successful.
    • Skipped (3): The test was intentionally skipped.

    Once a result is set using withResult(), the test object becomes effectively locked; you cannot change its title, arguments, or result again.

    use Tester\Runner\Test;
    
    // Initial state
    $test = new Test('path/to/test.php', 'My Test Title');
    
    // Transitioning to a result state (creates a new instance)
    $passedTest = $test->withResult(Test::Passed, 'Everything looks good', 0.045);
  11. Test exceptions and PHP errors

    master

    To test for exceptions, pass a closure to Assert::exception. To test for PHP errors, warnings, or notices, use Assert::error with the appropriate error level.

    Testing exceptions:

    Assert::exception(function () {
        $h = new Greeting;
        $h->say(null);
    }, InvalidArgumentException::class, 'Invalid name.');

    Testing PHP errors/notices:

    Assert::error(function () {
        $h = new Greeting;
        echo $h->abc;
    }, E_NOTICE, 'Undefined property: Greeting::$abc');