whoami

Lucas Jenß

cat /etc/motd

The Coding Journal ツ — Notes taken on an epic coding journey. Technical solutions, debugging notes, and practical guides from the trenches of software development.

Close-up of white dominoes with black dots standing on green felt surface, shallow depth of field, focused mood

ls -la ~/languages/

total 8
drwxr-xr-x
▶ PHP
▶ Ruby
▶ Scala
▶ C#
▶ JavaScript
▶ Objective-C
▶ Shell Scripting

ls -la ~/toolchain/

total 7
drwxr-xr-x
▶ Typo3
▶ Akka
▶ Capistrano
▶ Git
▶ MAMP
▶ Adobe Illustrator
▶ NSTrackingArea (Cocoa)

uname -a

platforms
drwxr-xr-x
▶ Mac OS X
▶ Unix

Writing a PHP Unit Test for a TYPO3 Domain Model with DataProvider

A TYPO3 domain model often looks simple: it contains properties, getters, setters and a few rules that describe business data. The difficult part of testing is usually not creating the object, but checking several valid and invalid combinations without filling the test class with repetitive methods. PHPUnit’s DataProvider feature gives each scenario a clear name while keeping the assertions in one place.

This approach suits Extbase projects that support several TYPO3 versions or receive data from different forms, APIs and database records. It also works well for Australian development teams maintaining extensions for clients in Sydney, Melbourne, Brisbane or Perth, where a compact, readable test suite can save time across different PHP and CI environments.

Testing concern Practical approach Main benefit
Required model values Pass valid and invalid values through a provider One test covers many cases
Boundary conditions Include empty strings, zeroes and maximum values Business rules become explicit
TYPO3 object construction Build the model inside each dataset or a helper Tests remain isolated
Readable failures Give every dataset a descriptive key PHPUnit identifies the failing scenario
Version compatibility Use syntax supported by the project’s PHPUnit version Fewer surprises during upgrades

Define The Model Behaviour First

Before writing a provider, identify the behaviour that deserves protection. A test for a plain getter and setter may be useful when the property has a conversion or normalisation rule, but it provides little value when it only proves that PHP can store a value. Focus on rules such as a product price being non-negative, a title being trimmed, or an enabled flag being converted to a Boolean.

Consider a small Product model with a setPrice() method that accepts a numeric value and a getPrice() method that returns a float. The test should describe the contract rather than the internal implementation. If the project sells goods in the Australian market, values such as 0, 19.95 and 9999.99 are more useful examples than arbitrary numbers copied from a fixture.

A good test name makes the intended rule obvious:

public function priceIsStoredAsFloat(
    mixed $input,
    float $expected
): void {
    $product = new Product();
    $product->setPrice($input);

    self::assertSame($expected, $product->getPrice());
}

Create Isolated TYPO3 Test Objects

Domain models should be instantiated directly unless the behaviour depends on Extbase persistence, dependency injection or a repository. Calling the constructor directly keeps the unit test fast and prevents a database connection or TYPO3 bootstrap from becoming part of the test’s meaning.

For models with several properties, use a small factory helper in the test. The helper can create a valid baseline object, while each provider dataset changes only the value under examination. This is particularly useful when a TYPO3 extension has fields such as title, slug, price, category and published.

private function createProduct(): Product
{
    $product = new Product();
    $product->setTitle('Sample product');
    $product->setPrice(10.00);
    $product->setPublished(true);

    return $product;
}

Keep the helper deterministic. Do not use the current time, random identifiers or a live database unless the test is specifically intended to cover persistence. For teams working across AEST and other time zones, deterministic dates also prevent a test from passing in Melbourne and failing after a date boundary in Perth.

Supply Scenarios Through A DataProvider

A provider returns an iterable collection of argument lists. Each list maps to the parameters of the test method. Named keys are worth using because PHPUnit can report zero price or decimal price instead of an unhelpful numeric dataset index.

With PHPUnit 10 or newer, the attribute syntax is clear and statically discoverable:

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

final class ProductTest extends TestCase
{
    #[DataProvider('priceProvider')]
    public function priceIsStoredAsFloat(
        mixed $input,
        float $expected
    ): void {
        $product = $this->createProduct();
        $product->setPrice($input);

        self::assertSame($expected, $product->getPrice());
    }

    public static function priceProvider(): iterable
    {
        yield 'zero price' => [0, 0.0];
        yield 'whole number' => [25, 25.0];
        yield 'decimal price' => ['19.95', 19.95];
    }
}

The provider should contain business examples rather than a large collection of meaningless values. Include a zero amount, a decimal, a string representation if the model receives form input, and a realistic upper boundary. If a rule rejects negative prices, add a separate provider for exceptions instead of hiding the expected failure inside the successful cases.

Older TYPO3 projects may use PHPUnit’s @dataProvider annotation because their installed PHPUnit version predates attributes. Use the convention supported by the project rather than upgrading the whole test stack only for a shorter declaration. The important part is the separation between scenario data and assertions.

Test Invalid Input Explicitly

Valid values are only half of the model’s contract. A domain object should have predictable behaviour when it receives blank text, malformed numeric input or a value outside its permitted range. If the model throws an InvalidArgumentException, write a provider that supplies both the input and the expected exception message.

#[DataProvider('invalidPriceProvider')]
public function invalidPriceIsRejected(mixed $input): void
{
    $product = $this->createProduct();

    $this->expectException(\InvalidArgumentException::class);

    $product->setPrice($input);
}

public static function invalidPriceProvider(): iterable
{
    yield 'negative price' => [-1];
    yield 'non numeric text' => ['free'];
}

If messages are part of the public contract, add expectExceptionMessage() as well. If they are implementation details, asserting only the exception class makes the test less brittle. This distinction matters when TYPO3 core or a validation library changes wording during an upgrade.

Unit tests can also expose assumptions about input gathered from outside the application. An imported title may contain unusual whitespace or encoded characters, while content copied from an online source can carry unexpected markup. A short SEO case note is a reminder that external content should be treated as data to validate, not as a trusted fixture.

Keep Assertions Focused And Readable

A single provider-driven test should normally verify one behaviour. It may assert several related effects when they form one contract, such as checking that a setter trims a title and that the getter returns the trimmed result. Avoid turning one method into a general-purpose test for every property on the model.

For a normalisation rule, provide the raw value and the expected value:

#[DataProvider('titleProvider')]
public function titleIsTrimmed(string $raw, string $expected): void
{
    $product = $this->createProduct();
    $product->setTitle($raw);

    self::assertSame($expected, $product->getTitle());
}

public static function titleProvider(): iterable
{
    yield 'plain title' => ['Widget', 'Widget'];
    yield 'leading and trailing spaces' => ['  Widget  ', 'Widget'];
    yield 'internal spaces remain' => ['Widget Pro', 'Widget Pro'];
}

Do not overuse assertEquals() when the type matters. assertSame() catches a string "25" being returned where the model promises a float 25.0. This is especially valuable when values pass through TYPO3 properties, form submissions and database hydration, because PHP’s loose conversions can conceal a broken domain contract.

Run The Suite Across The Project Matrix

Run the focused test first while developing, then run the complete extension suite before committing. Typical commands include vendor/bin/phpunit for the whole suite and a test file path for a single model. Check the project’s composer.json, because TYPO3 extensions often pin PHPUnit through TYPO3’s testing framework and may use a project-specific configuration file.

When a provider test fails, read the dataset name before inspecting the implementation. A failure called decimal price immediately narrows the investigation, while an unnamed dataset forces you to count array positions. Keep provider names stable and specific so they remain useful in continuous integration logs.

The same discipline helps when tests are mixed with asynchronous or integration-heavy code. For background services, documenting graceful shutdown separately from model tests keeps a fast unit test from inheriting unrelated server lifecycle concerns. A TYPO3 domain model test should not need to start a web server, queue worker or external service.

A provider is also a practical way to document regression cases. If a customer in Sydney reports that a zero-dollar promotional item cannot be saved, add that exact scenario with a descriptive key. If a Melbourne client’s catalogue contains prices with decimal fractions, preserve that case in the provider rather than relying on a vague fixture. The test then records the business lesson for the next developer.

Unexpected failures in a wider distributed test environment should be diagnosed separately from model behaviour. Notes about cluster node removal illustrate why infrastructure symptoms need their own investigation; they should not lead to adding retries or sleeps to a deterministic unit test.

Add a provider whenever a new boundary or production regression appears, keep each dataset meaningful, and run the model test in CI alongside the rest of the TYPO3 extension. This gives the domain object a precise, maintainable safety net without turning a small PHP class into a slow integration test.


cat ~/interests.json

KeyValue
editorTerminal-first workflow
osMac OS X / Unix
vcsGit, distributed version control
deployCapistrano, cron automation
graphicsSVG, Adobe Illustrator troubleshooting
networkingIP validation, SSH, VPN

git log --oneline --reverse

2013-10-30

Solving SVG import issues in Adobe Illustrator CS6 and CC

When importing an SVG into Illustrator, the operation fails with an unknown error [CANT]. A workaround for this Adobe-side bug.

2013

Solving NDK build issues on OS X

Troubleshooting native development kit compilation problems on Mac OS X.

2013

Programmatically adding PHP generated TypoScript to the backend configuration

Integrating dynamically generated TypoScript into Typo3 backend setups using PHP.

2013

ArgumentError: Could not parse PKey: no start line

Debugging an SSH key parsing error encountered during deployment.

2011-08-04

Validating IP-Addresses in PHP

Using PHP filter functions with flags like FILTER_FLAG_IPV4 and FILTER_FLAG_IPV6, and understanding how filter_var handles reserved IP addresses.

2011-07-09

Cocoa: Using NSTrackingArea

A short tutorial on using Cocoa's NSTrackingArea to capture mouseEntered and mouseExited events.


cat ~/contact.txt

github: github.com/x3ro
stackoverflow: x3ro
coderwall: coderwall.com/x3ro
twitter: @x3rames