Using Faker for Realistic Test Data in PHP Projects
AI generated
@test
assert
PHPUnit · Test Data · PHP
Faker for Test Data
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.

14 min read FakerPHP Reproducible seeds Test data strategy

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.

11. FAQ: Faker for Test Data: The Key Facts at a Glance

1Which Faker package should be used for PHP today?
fakerphp/faker is the actively maintained successor to fzaninotto/faker and should be used for new projects.
2How do you ensure Faker-generated tests stay reproducible?
Via the seed() method of the Faker generator. With the same seed value, Faker returns exactly the same sequence of values on every run.
3Where should the seed value be set?
Best set centrally in a shared test factory or bootstrap file instead of manually in every individual test, so the entire suite stays consistent.
4Can Faker be called directly inside a PHPUnit DataProvider method?
Technically yes, but with caution: the values then get frozen once before all test runs. With a fixed seed that is entirely practical for stable test cases.
5How do you generate domain-specific test data like SKUs?
Through custom provider classes that extend Faker\Provider\Base and offer project-specific generation methods like sku().
6Does Faker replace targeted boundary tests?
No. Edge cases like empty strings, very long names, or negative prices should still be explicitly formulated as dedicated test cases.
7Is Faker suitable for tax calculations with fixed percentages?
Not directly. For exact, business-mandated input-output relationships, a fixed, documented test value belongs in the test case.
8How do you prevent inconsistent test data across a team?
Through a central Faker wrapper per project with a fixed locale and fixed seed, so not every developer uses their own settings.
9What should you do if a seed causes a real data collision?
Deliberately change the seed value and document the decision in code review, instead of silently working around the collision.
10Does Faker also produce valid, rounded price formats?
With randomFloat(2, min, max) plus round(), you can generate prices that have exactly two decimal places and are therefore business-valid.