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
}
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
// .......
}
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();
}
}
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;
}
}
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();
}
}
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:
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)