Realistic, reproducible values instead of dummy strings
Hand-written test data like 'Test Name' or 'test@test.com' hides real bugs because it never reflects the diversity of real input. The Faker library generates realistic names, addresses, and prices while staying fully reproducible with fixed seeds.
Table of Contents
- 1. Why hand-written dummy values are dangerous
- 2. Installing Faker and getting started
- 3. Fixed seeds for deterministic tests
- 4. Custom providers for domain-specific data
- 5. Using Faker inside PHPUnit data providers
- 6. Generating realistic prices and number formats
- 7. Limits of Faker in everyday testing
- 8. Best practices for team usage
- 9. Conclusion: combining realism and reproducibility
- 10. Summary
- 11. FAQ
1. Why hand-written dummy values are dangerous
Many grown test suites contain lines like $name = 'Test Test' or $email = 'test@test.com'. Such values formally fulfill the purpose of filling a variable, but they systematically hide real bugs. A validation function for names with accented characters, hyphens, or multiple first names is never actually exercised if the only name ever used is consistently 'Test Test'.
It gets even more problematic when multiple tests reuse the same static value and thereby accidentally mask collisions, for instance when an email uniqueness check never fails because every test happens to use the same address, or conversely a real collision never occurs because every test deliberately uses a different hard-coded address. Both cases produce green tests without the actual business logic ever being verified under realistic conditions.
2. Installing Faker and getting started
The actively maintained library today is called fakerphp/faker and is the successor to the no longer actively developed fzaninotto/faker. It is installed as a dev dependency via Composer, since test data generation has no place in production code. The entry point is always Faker\Factory::create(), which returns a Faker generator instance with a German or English locale.
A central Faker generator knows dozens of providers for names, addresses, companies, internet data, numbers, and even industry-specific formats such as IBAN numbers or barcodes. Access happens through magic properties like $faker->name or explicit methods like $faker->numberBetween(10, 500), which makes everyday usage very compact.
<?php
declare(strict_types=1);
namespace Tests\Support;
use Faker\Factory;
use Faker\Generator;
/**
* Central Faker factory for the entire test suite.
*/
final class TestDataFactory
{
private static ?Generator $faker = null;
public static function faker(): Generator
{
if (self::$faker === null) {
self::$faker = Factory::create('en_US');
self::$faker->seed(20260807);
}
return self::$faker;
}
}
3. Fixed seeds for deterministic tests
The biggest objection to random test data is rightly this: a test that passes today must not turn red tomorrow for no discernible reason. Faker solves this via the seed() method. If the random generator is initialized with the same seed value, it returns exactly the same sequence of 'random' values on every test run, regardless of whether the test runs locally, in the CI pipeline, or again three months from now.
In practice, it is best to set the seed centrally in a bootstrap file or a shared setUp() base class instead of setting it manually in every individual test. That way, reproducibility stays guaranteed while individual tests still benefit from realistic data diversity, without anyone having to know or document the concrete generated values in advance.
<?php
declare(strict_types=1);
namespace Tests\Unit\Customer;
use App\Customer\CustomerRegistrar;
use PHPUnit\Framework\TestCase;
use Tests\Support\TestDataFactory;
final class CustomerRegistrarTest extends TestCase
{
public function testRegistersCustomerWithRealisticData(): void
{
$faker = TestDataFactory::faker();
$registrar = new CustomerRegistrar();
$customer = $registrar->register(
firstName: $faker->firstName(),
lastName: $faker->lastName(),
email: $faker->unique()->safeEmail(),
street: $faker->streetAddress(),
postcode: $faker->postcode(),
city: $faker->city(),
);
self::assertNotEmpty($customer->getFullName());
self::assertStringContainsString('@', $customer->getEmail());
}
}
4. Custom providers for domain-specific data
Faker's standard providers cover generic data like names and addresses, but every project also has domain-specific concepts, for example SKU formats, customer numbers, or product categories. For such cases you can write custom provider classes that extend the generic Faker generator and offer project-specific generation methods.
A custom provider encapsulates not just the format but also the business rules, for instance that an SKU always starts with a two-letter category prefix followed by a six-digit sequential number. That prevents different tests from hand-writing slightly different, sometimes invalid SKU formats, and at the same time makes visible which format rules actually apply in the domain. Another benefit is that format changes, for instance extending the category prefix from two to three letters, can be applied in exactly one central place instead of across dozens of scattered test files.
<?php
declare(strict_types=1);
namespace Tests\Support\Faker;
use Faker\Provider\Base;
/**
* Domain-specific Faker provider for product SKUs.
*/
final class ProductProvider extends Base
{
private const CATEGORY_PREFIXES = ['EL', 'MO', 'GA', 'BU'];
public function sku(): string
{
$prefix = static::randomElement(self::CATEGORY_PREFIXES);
$number = str_pad((string) $this->generator->numberBetween(1, 999999), 6, '0', STR_PAD_LEFT);
return sprintf('%s-%s', $prefix, $number);
}
}
5. Using Faker inside PHPUnit data providers
A common mistake is calling Faker directly inside a #[DataProvider] method, since that method is evaluated once before all test runs and the generated values then get frozen for multiple test executions at once, which undermines the actual point of realistic spread. It is better to define only the structure or fixed edge cases inside the data provider and call Faker specifically inside the test method itself.
For cases where many random combinations are genuinely needed as data provider rows, for instance to check a validation function against twenty different but realistic addresses, you can use Faker with a fixed seed directly when building the data provider array. Because the seed is fixed, this list also stays stable across test runs.
<?php
declare(strict_types=1);
namespace Tests\Unit\Validation;
use App\Validation\AddressValidator;
use Faker\Factory;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
final class AddressValidatorTest extends TestCase
{
public static function validAddressProvider(): array
{
$faker = Factory::create('en_US');
$faker->seed(4242);
$cases = [];
for ($i = 0; $i < 20; $i++) {
$cases[] = [$faker->streetAddress(), $faker->postcode(), $faker->city()];
}
return $cases;
}
#[DataProvider('validAddressProvider')]
public function testAcceptsValidAddress(string $street, string $postcode, string $city): void
{
$validator = new AddressValidator();
self::assertTrue($validator->isValid($street, $postcode, $city));
}
}
6. Generating realistic prices and number formats
For e-commerce projects such as Magento shops, realistic prices matter a great deal because rounding errors and formatting problems often only surface with specific combinations of decimal places and tax rates. Faker offers randomFloat() and numberBetween() as building blocks to generate both typical prices and deliberate edge cases like very small or very large amounts.
It makes sense to wrap these building blocks in a dedicated helper method that also guarantees valid amounts rounded to two decimal places, instead of feeding raw Faker values directly into tests. That way, generated prices are guaranteed to match the same formatting rule used in production instead of occasionally producing technically impossible values like three decimal places.
<?php
declare(strict_types=1);
namespace Tests\Support\Faker;
use Faker\Generator;
/**
* Helper functions for realistic, business-valid prices.
*/
final class PriceFaker
{
public function __construct(private readonly Generator $faker)
{
}
public function grossPrice(float $min = 1.0, float $max = 999.99): float
{
return round($this->faker->randomFloat(2, $min, $max), 2);
}
}
7. Limits of Faker in everyday testing
Faker is excellent for generic, broadly spread data, but it does not replace targeted boundary tests. Anyone who wants to explicitly verify how a function handles an empty string, a 256-character name, or a negative price should still deliberately and explicitly formulate these cases as dedicated test cases, instead of hoping Faker randomly produces those edge cases at some point.
Faker is equally unsuited for tests that verify an exact, business-mandated input-output relationship, for example a tax calculation with a legally fixed percentage. Here a fixed, documented test value belongs in the test case, because the traceability of the expected result matters more than realistic data variety.
8. Best practices for team usage
A central Faker wrapper per project, as shown in the first example, prevents every developer from using their own locale settings or seeds and thereby making test runs inconsistent across the team. It is equally important to raise or change the seed value when a particular random sequence unexpectedly leads to a real data collision, for example two randomly identical email addresses in a uniqueness test.
It is also recommended to briefly document in the team documentation which seed value is currently in use and why, so that changing the seed becomes a deliberate, traceable decision visible in code review rather than an accidental side effect of some other change.
9. Conclusion: combining realism and reproducibility
Faker elegantly resolves the apparent contradiction between realistic test data and deterministic, repeatable test runs through fixed seeds. For Magento and other PHP projects, moving away from hand-written dummy values pays off especially for validation logic, form processing, and anything involving personal or address-related data.
Getting started requires little effort: a central factory class, a fixed seed, and if needed one or two custom providers for domain-specific formats are usually enough to make a test suite noticeably more meaningful without jeopardizing the traceability of individual test runs.
| Situation | Hand-written dummy value | Faker approach | Benefit |
|---|---|---|---|
| Names with special characters | 'Test Test' | $faker->name() with en_US locale | Covers accents and hyphens realistically |
| Email uniqueness | Fixed address in every test | $faker->unique()->safeEmail() | Actually exercises the collision logic |
| Price formatting | Fixed values like 9.99 | $faker->randomFloat(2, min, max) | Checks rounding across many combinations |
| Reproducibility | Pure randomness without seed | $faker->seed() with a fixed value | Same values on every test run |
| Domain formats | Hand-written SKU strings | Custom Faker provider | Business format rules encapsulated centrally |
Mironsoft
Test automation, Magento quality assurance, and CI integration
Tests that catch real bugs instead of just turning green?
We review existing PHPUnit suites for implementation-detail tests, flaky tests, and missing coverage at critical points, then build a test strategy that provides real confidence with every Magento update.
Test Audit
Reviewing existing suites for mocking antipatterns and blind spots.
Test Strategy
Meaningfully combining unit, integration, and MFTF tests for Magento projects.
CI Integration
Setting up fast, reliable test runs in GitLab CI or GitHub Actions.
10. Summary
Faker for Test Data: The Key Facts at a Glance
Problem
Hand-written dummy values like 'Test Test' hide bugs because they never reflect real data diversity.
Reproducibility
A fixed seed ensures Faker returns exactly the same values on every test run.
Extensibility
Custom providers centrally encapsulate domain-specific formats like SKUs or customer numbers.
Limits
Targeted edge cases like empty strings or negative prices still belong explicitly in the test case.