Back to blog

Converting PHPUnit to JavaScript with Pext

PHPUnit is the standard testing framework for PHP. It is maintained by Sebastian Bergmann, powers the test suites of virtually every major PHP project, and has been in active development for over a decade. The codebase spans roughly 54,000 lines of PHP across 412 files and includes more than 4,500 unit tests.

It is also one of the most technically demanding PHP projects you could attempt to transpile. PHPUnit exercises nearly every corner of the language — from reflection and closures to dynamic code generation and complex OOP hierarchies. That is exactly why we chose it as a flagship target for Pext. If the transpiler can handle PHPUnit, it can handle most real-world PHP codebases.

As of today, Pext achieves a 61.3% test pass rate on the converted PHPUnit codebase. This post walks through what that conversion involves and why it matters.

What makes PHPUnit difficult

PHPUnit is not a typical CRUD application. It is a framework that inspects, generates, and executes code — which makes it a stress test for any transpiler.

DOM and XML configuration parsing

PHPUnit uses DOM with libxml integration to parse its XML configuration files. This means the transpiled code needs a working DOM implementation that correctly handles node traversal, attribute access, XPath queries, and the various edge cases that come with XML processing. The Pext runtime provides this through its DOM and libxml modules.

File system operations

Test discovery in PHPUnit relies heavily on file system operations — file iterators, directory scanners, and filters that walk the project tree to locate test files. These patterns exercise Pext's file handling and iterator support, including custom iterator classes and filter chains.

Reflection

PHPUnit uses reflection extensively to understand test structure at runtime. It reflects on classes to discover test methods, reads attributes and annotations to identify data providers, @before and @after hooks, expected exceptions, and other metadata that drives test execution. Transpiling this correctly requires that Pext's reflection runtime faithfully mirrors PHP's introspection capabilities.

Assertions and runtime verification

At its core, PHPUnit runs assertions — checking that actual values match expected behaviour. The assertion engine evaluates conditions, formats failure messages, compares complex data structures, and throws exceptions when checks fail. All of this must work identically in the JavaScript output for tests to produce meaningful results.

Mock generation and dynamic code

One of the more challenging aspects is PHPUnit's mock system. It generates mock classes dynamically at runtime through template-based code generation, then loads them via eval. These mocks implement interfaces, extend classes, and use invocation handlers to track method calls and enforce expectations. Supporting this in the transpiled output requires Pext to handle dynamic code conversion — generating JavaScript from PHP templates on the fly and executing them within the same runtime context.

CLI and output

PHPUnit is a command-line tool. It parses options, manages verbosity levels, and produces structured output: progress indicators, success and failure markers, detailed error messages with stack traces, and a summary of results. The CLI layer exercises Pext's support for process arguments, output buffering, and formatted printing.

Error handling

Proper error handling is critical in a test framework. PHPUnit distinguishes between test failures (assertion violations), errors (unexpected exceptions), and expected exceptions (tests that verify a specific exception is thrown). The transpiled code must preserve this distinction — catching the right exceptions at the right level, and surfacing failures with accurate context.

The dependency tree

PHPUnit does not stand alone. It depends on a suite of libraries — all authored by Sebastian Bergmann — that had to be converted as well. These include:

  • sebastian/exporter — exports PHP values for human-readable output in assertions
  • sebastian/diff — computes text differences for failure messages
  • php-file-iterator — provides the file discovery mechanism for test suites
  • sebastian/comparator — handles complex value comparisons
  • sebastian/environment — detects runtime capabilities
  • sebastian/code-unit — maps code units for coverage tracking

Each of these libraries was transpiled independently and integrated into the converted PHPUnit project. They exercise different parts of the Pext runtime — from type juggling and recursion detection in the exporter, to iterator protocols in the file walker.

OOP and language coverage

PHPUnit is deeply object-oriented. The conversion exercises Pext's support across a wide surface:

  • Classes, abstract classes, and interfaces with complex inheritance chains
  • Traits with conflict resolution
  • Closures and anonymous functions used in matchers and callbacks
  • Magic methods — __get, __set, __call, __toString, and others
  • ArrayAccess and Countable implementations on custom collections
  • Namespaces across the entire dependency graph
  • Autoloading via Composer's generated autoloader

The project is built with Composer, which means autoloading support is essential. Pext converts the Composer-generated autoloader so that require and use statements resolve correctly in the JavaScript output, maintaining the same class-loading behaviour as the original project.

Logging and reporting

PHPUnit supports multiple logging formats — JUnit XML, TeamCity, and custom loggers. The transpiled version retains full support for these logging features, producing output files that match the format and content of the PHP original. This is important for CI/CD integration, where downstream tooling consumes test reports in a specific format.

Where we are

At 61.3% of tests passing, there is still work ahead. The failing tests point to specific areas where the Pext runtime needs refinement — edge cases in reflection, certain mock generation patterns, and a handful of file system interactions that behave differently between PHP and Node.js.

But the number itself tells only part of the story. The fact that PHPUnit boots, discovers tests, instantiates mocks, runs assertions, catches failures, and produces structured output — all in transpiled JavaScript — demonstrates that the transpiler and runtime are handling real-world complexity, not toy examples.

We will continue publishing updates as the pass rate improves. PHPUnit is our benchmark: when it runs clean, we know the transpiler is ready for production codebases.

Interested in seeing Pext run against your own project? Book a demo and we will walk through it live.