Testing only what matters
Many times developers try to test 100% (or almost 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 save an entity to the database, your domain is not in charge of pesisting it on the database but making sure data saved 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.
In the following sections , I will try to show it with a simple example
Introduction
The example shown in the following sections does not try to show how Domain Driven Desing 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 examples, are built over a PHP environment with symfony but the idea is valid for any language or framework.
Testing example with no decoupled domain
Let’s imagine our application connect to an external api which returns data about rain probability for a specified date. Data returned looks like this:
{
"date" : "2022-12-01",
"rain_probability" : 0.75
}
Now, we have to take those data and clasify it following this mapping:
- rain_probability < 0.40: LOW
- rain_probability ≥ 0.40 && rain_probability < 0.75: MEDIUM
- rain_probability ≥ 0.75: HIGH
and then save on our database on the 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;
public function getDate(): string
{
return $this->date;
}
public function setDate(string $date): void
{
$this->date = $date;
}
public function getProbability(): float
{
return $this->probability;
}
public function setProbability(float $probability): void
{
$this->probability = $probability;
}
public function getLabel(): string
{
return $this->label;
}
public function setLabel(string $label): void
{
$this->label = $label;
}
}
Let’s create a handler which gets the external api data, sets the label according to the rain probability and save 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 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 data to the database. Now, we could try to load the EntityManagerInterface using mocks and stubs but, is it necessary ?. Obviously not. As said at the begining of the short, 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 make simple our test, we are going to move 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:
class RainMeasureHandlerTest extends \Codeception\Test\Unit
{
protected UnitTester $tester;
private RainMeasureLabelHandler $rainMeasureLabelHandler;
protected function _before()
{
$this->rainMeasureLabelHandler = new RainMeasureLabelHandler();
}
public function testLowProbability()
{
$label = $this->rainMeasureLabelHandler->getLabelFromProbability(0.35);
$this->assertEquals('LOW', $label);
}
public function testMediumProbability()
{
$label = $this->rainMeasureLabelHandler->getLabelFromProbability(0.60);
$this->assertEquals('MEDIUM', $label);
}
public function testHighProbability()
{
$label = $this->rainMeasureLabelHandler->getLabelFromProbability(0.83);
$this->assertEquals('HIGH', $label);
}
}
For making the test I’ve used codeception but any other library would be valid
After executing the test, results look as shown below
Final words
I would like to say that other kind of tests will 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.