DEV Community

spO0q
spO0q

Posted on

PHP: random failures in tests

Context:

Tests have been working fine so far, and, suddenly, it seems to fail.

Why?

While it's easy to write basic unit tests, it can be tricky when dealing with dates, complex calculations, or random values.

Errors with dates and intervals

While it's usually a good practice to stick with the native API (regardless of the programming language), I recommend Carbon, especially in unit tests:

  • it just works
  • it's more readable
  • it's more precise
  • it's only an additional layer, as it extends the built-in DateTime
  • it simplifies tests
  • it's already packed in major frameworks such as Laravel

You may prevent funny situations with dates:

  • mismatch due to server configuration: pipelines (CI/CD) vs. local machines
  • mismatch due to overflows: assertions fail because date intervals do not match this month
  • mismatch due to timezone differences and microseconds
  • mismatch due to time variability: your tests depend on the current date but the current date is not mocked correctly

Testable code vs. complex logic

The business logic is the heart of your job.

Such logic often needs to cover various cases, which can be hard to implement.

Because you have to make it work, you will get there eventually, but the code might not be maintainable.

Writing testable code simplifies tests drastically (e.g., SOLID principles, DRY, composition over inheritance).

Here are some red signs:

  • the tests replicate the implementation while it should focus on behaviors instead
  • race conditions: your logic involve concurrent operations that fail randomly

Property-based testing: intermittent failures

Property-based testing is a popular approach.

You can use randomized inputs to cover more cases.

In other words, you don't write values/examples manually. Every time, the computer generates a new set of inputs.

This is pretty cool but ensure you do the following before:

  • think about the specification very carefully
  • fine tune properties according to your business logic (e.g., unsupported values, scopes)

Besides, be aware it does not replace unit tests (example-based tests), so do not rely solely on property-based testing.

Otherwise, you may encounter the following issues:

  • wrong or incomplete notion of correctness
  • overuse leading to slower dev
  • performance issues

Besides, relying too much on randomized inputs can lead to a false sense of security.

It can be hard to cover all scenarios.

Manual "hacks"

Of course it's bad, but it's not uncommon to find some hacks, especially in legacy projects.

These hacks do the job but aren't maintainable.

For example:

sleep(9);
Enter fullscreen mode Exit fullscreen mode

The above code defines an arbitrary wait time of 9 seconds and may allow you to "mock" some real behavior.

However, it's not reliable and may lead to random failures in your pipelines.

The Faker trap

Faker is a very popular library to generate fake data:

$email1 = $faker->email();
$email2 = $faker->email();
Enter fullscreen mode Exit fullscreen mode

In the above code, $email1 and $email2 can be the same sometimes, making your assertion fail randomly.

In this case, using Faker is probably not mandatory (particularly for diff).

If you want to test a set of inputs you may use data providers instead.

Array comparisons

You may encounter random errors when testing that two arrays are equal or not equal.

That's often because one of the two arrays is not sorted:

$this->assertEquals([2, 1], [1, 2]);
Enter fullscreen mode Exit fullscreen mode

PHPUnit has specific methods to assess equality.

Just pick the right one. For example, assertEqualsCanonicalizing will sort arrays automatically before comparing them.

Be careful, though. It does not mean you can use these methods to make your tests pass while it should not.

Sometimes, you do want your arrays to be exactly the same.

Wrong execution order

This one can be a huge pain!

The execution order of your tests should not affect the outcome, but it's not uncommon to find such configuration in projects.

You may want to use @depends annotation to "fix" the problem, but it's best if you can refactor your tests to be independent instead.

Troubleshooting: help yourself

There are simple measures you can take to ease the pain with your tests:

  • keep it simple (which is not stupid at all)
  • while CI/CD pipelines can fail, it runs automatically on pushes, merges, or manual deployments
  • the AAA pattern improves readability
  • use randomness judiciously
  • name your methods carefully
  • reduce unnecessary complexity, including useless dependencies
  • refactor your tests on a regular basis
  • include tests in code reviews

Wrap up

If your tests fail randomly, it's usually due to a known issue.

However, there are some traps to avoid, especially when you introduce some randomness on purpose.

Top comments (0)