DEV Community

Nacho Colomina Torregrosa
Nacho Colomina Torregrosa

Posted on • Edited on • Originally published at Medium

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

Top comments (0)