Logo

dev-resources.site

for different kinds of informations.

Focusing your tests on the domain. A PHPUnit example

Published at
10/4/2024
Categories
phpunit
php
testing
ddd
Author
icolomina
Categories
4 categories in total
phpunit
open
php
open
testing
open
ddd
open
Author
9 person written this
icolomina
open
Focusing your tests on the domain. A PHPUnit example

Introduction

Many times developers try to test the 100% (or almost the 100%) of their code. Apparently, this is the aim every team should reach for their projects but, from my point of view, only one piece of the entire code should be fully tested: Your domain.

The domain is, basically, the part of your code which defines what the project actually does. For instance, when you persist an entity to the database, your domain is not in charge of persisting it on the database, but making sure that the persisted data makes sense according to your business model. Possibly, when you save your data on the database, you will use an external library like PHP Doctrine. This library is already fully tested, there is no need to test what it does. If you pass to doctrine the correct data, it will be saved to the database with no issues.

The example shown in the following sections does not try to show how the Domain Driven Design works, there are many articles which explain it very well. I will try to show how having your domain well defined and decoupled can help to test easily and focused on what your application does.

The example is built over a Symfony environment and using the PHPUnit library, but the idea is valid for any language or framework.

The code to test

Let’s imagine that our application connects to an external api which returns data about the rain probability for a specified date. The returned data looks like this:

{
   "date" : "2022-12-01",
   "rain_probability" : 0.75
}
Enter fullscreen mode Exit fullscreen mode

Now, we have to take those data and classify it following this mapping:

  • rain_probability < 0.40: LOW
  • rain_probability ≥ 0.40 && rain_probability < 0.75: MEDIUM
  • rain_probability ≥ 0.75: HIGH

and save the result on a database table described by the following entity:

#[ORM\Entity(repositoryClass: RainMeasure::class)]
class RainMeasure {

    #[ORM\Column]
    private string $date;

    #[ORM\Column]
    private float $probability;

    #[ORM\Column(length: 10)]
    private string $label;

    // Getters and setters
    // .......
}
Enter fullscreen mode Exit fullscreen mode

Let’s create a handler which gets the external api data, sets the label according to the rain probability and saves it to the database.

class RainMeassureHandler {
    private EntityManagerInterface $em;

    public function __construct(EntityManagerInterface $em)
    {
        $this->em = $em;
    }

    public function saveMeasure(array $measureData): void
    {
        if($measureData['rain_probability'] < 0.40){
            $label = 'LOW';
        }
        elseif ($measureData['rain_probability'] >= 0.40 && $measureData['rain_probability'] < 0.75){
            $label = 'MEDIUM';
        }
        else{
            $label = 'HIGH';
        }

        $rainMeasure = new RainMeassure();
        $rainMeasure->setDate($measureData['date']);
        $rainMeasure->setProbability($measureData['rain_probability']);
        $rainMeasure->setLabel($label);

        $this->em->persist($rainMeasure);
        $this->em->flush();
    }
}
Enter fullscreen mode Exit fullscreen mode

If we try to create a test for the above handler, we will find that we will need to inject the EntityManagerInterface since the behaviour we want to test (setting a label according to the probability value) is coupled in the same handler which saves the result to the database. We could try to load the EntityManagerInterface using mocks and stubs but, is it necessary ?. Obviously not. As said before, we should try to focus on testing the behaviour that belongs to our domain, which is getting the correct label according to the rain probability.

Decoupling behaviour we want to test

In order to simplify our test, we are going to move the behaviour we want to test to another class:

class RainMeasureLabelHandler 
{
    public function getLabelFromProbability(float $prob): string
    {
        if($prob < 0.40){
            $label = 'LOW';
        }
        elseif ($prob >= 0.40 && $prob < 0.75){
            $label = 'MEDIUM';
        }
        else{
            $label = 'HIGH';
        }

        return $label;
    }
}
Enter fullscreen mode Exit fullscreen mode

And now, our RainMeassureHandler will look like this:

class RainMeasureHandler 
{
    private EntityManagerInterface $em;

    public function __construct(EntityManagerInterface $em)
    {
        $this->em = $em;
    }

    public function saveMeasure(array $measureData): void
    {
        $rainMeasureLabelHandler = new RainMeasureLabelHandler();
        $label = $rainMeasureLabelHandler->getLabelFromProbability($measureData['rain_probability']);

        $rainMeasure = new RainMeasure();
        $rainMeasure->setDate($measureData['date']);
        $rainMeasure->setProbability($measureData['rain_probability']);
        $rainMeasure->setLabel($label);

        $this->em->persist($rainMeasure);
        $this->em->flush();
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we can focus on test our RainMeasureLabelHandler which would be part of our domain and would have no dependencies to external layers. Testing it would be as easy as shown:

Rain Measure Handler

Conclusion

I would like to say that other kind of tests would be useful too. Maybe we have an api and we want to test input and outputs with a test environment which includes database and other resources we could need. But, remember to have your domain decoupled and fully tested.

If you like my content and enjoy reading it and you are interested in learning more about PHP, you can read my ebook about how to create an operation-oriented API using PHP and the Symfony Framework. You can find it here: Building an Operation-Oriented Api using PHP and the Symfony Framework: A step-by-step guide

phpunit Article's
30 articles in total
Favicon
6 Steps to Master PHPUnit Testing with Ease!
Favicon
Mastering Unit Testing in PHP: Tools, Frameworks, and Best Practices
Favicon
PHP: Should I mock or should I go?
Favicon
Focusing your tests on the domain. A PHPUnit example
Favicon
Understanding Mock Objects in PHPUnit Testing
Favicon
Wrote a book (And it is not about coding, and yeap it is in Greek)
Favicon
How to run a phpunit unit test with a specific dataset
Favicon
Test Your DOM in Laravel with PHPUnit
Favicon
PHP: Mocking Closures and performing assertions
Favicon
Run PHPUnit locally in your WordPress Plugin with DDEV
Favicon
Setting up for Drupal's Functional JavaScript tests
Favicon
Fix Memory Exhausted Issue in Laravel Tests
Favicon
Another way to create test module configuration
Favicon
Testando filas em projetos Laravel
Favicon
Checklist to Become Software Developer
Favicon
Chat : Test unitaire sur des méthodes privées
Favicon
Chat: Unit test of private methods
Favicon
Upgrading to Laravel 10, PHPUnit 10, and Pest 2
Favicon
Testing an external api using PHPUnit
Favicon
Installing PHP Unit
Favicon
Code coverage for PHPUnit in VSCode
Favicon
Behavior-Driven Testing with PHP and PHPUnit
Favicon
Run Laravel test multiple times
Favicon
Using PHP anonymous classes to test collection services
Favicon
Test coverage: did you set Xdebug's coverage mode?
Favicon
Using github actions to execute your PHP tests after every push
Favicon
Writing a basic Feature Test with PhpUnit in Laravel
Favicon
Fix Symfony tests with PHPUnit 10
Favicon
The most efficient way to debug problems with PHPUnit mocks
Favicon
Why I am unable to assert that method has been called on my mocked service?

Featured ones: