Introduction
Automated Tests are one of the most important parts of software engineering. Every important author of books says it is necessary and shows examples of how to do it properly. For example, when we read Refactoring by Martin Fowler, we have a whole chapter dedicated to this topic, as well as other books like Clean Code by Uncle Bob.
Well, but then you might be thinking to yourself, "My project is legacy, and nobody writes tests." If you have this problem, I have something to say: I had the same issue, and many other developers did too.
However, you should be bold with your team and explain to your tech manager or PO how important it is. Every time a bug happens, remind them how the lack of unit tests is bad. Eventually, they will understand. This is a technique from The Pragmatic Programmer, where the author teaches how to achieve our technical goals.
To make you more enthusiastic about it, I will share graphs from one of my work projects. When I started advocating for unit tests and worked hard for a week, I was able to increase code coverage from 0% to 30%.
Okay, so now, what are automated tests? Let's begin with a concept. A while ago, someone asked me, "What is the automated test pyramid?" At that time, I didn’t know, but now I will share it with you and explain which topic we will focus on.
Basically, writing tests follows a hierarchy of effort. You can research more using an AI engine:
3 - E2E Tests
2 - Integration Tests
1 - Unit Tests <- our focus
PHP Unit
PHPUnit is one of the most famous test frameworks for PHP environments. Inside your PHP environment, run with Composer:
composer require --dev phpunit/phpunit
After that, navigate inside the folder and run:
./vendor/bin/phpunit
You will see the tests running.
Create a class called CalculatorService
inside the src
folder and another one called CalculatorServiceTest
inside the test
folder.
<?php
declare(strict_types=1);
namespace App\Service;
class CalculatorService
{
}
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Service;
use PHPUnit\Framework\TestCase;
class CalculatorServiceTest extends TestCase
{
}
Mindset
It took me a while to understand, but unit testing is about reverse engineering. For example, if you are new to the project’s code and need to write tests for a function you don't know, prepare yourself to think in reverse. That’s actually what testing is about. You need to mock methods and other classes that are dependencies of your tests. To do it properly, you need to analyze parameters and return types, and sometimes mock objects inside the function. This is why writing unit tests is hard for classes with many dependencies, complex logic flows, or multiple conditional branches. If you want everything green, you need to use a lot of mocks.
Another mindset is to test both valid and invalid responses while considering all possible edge cases.
Assertions and Types of Assertions
Update your service with the following code:
<?php
declare(strict_types=1);
namespace App\Service;
class CalculatorService
{
function sum(int $x, int $y): int
{
return $x + $y;
}
function division(int $x, int $y): int
{
if ($x <= 0) {
throw new \Exception("Error Processing Request", 1);
}
return $x / $y;
}
}
Now let's write a test:
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Service;
use App\Service\CalculatorService;
use AsyncAws\Core\Test\TestCase;
use PHPUnit\Framework\Attributes\Test;
class CalculatorServiceTest extends TestCase
{
#[Test]
function testSum() {
$service = new CalculatorService();
$this->assertSame(
$service->sum(10,10),
20
);
}
}
Run the test:
bin/phpunit --filter=testSum
Note: Running with --filter
executes only the specified test method. PHPUnit has other flags, which can be found running the command with --help.
You will see:
Assertions and Expecting
Assertions are basically if
statements to check if everything is fine. PHPUnit provides different types of assertions, but these are the most important:
-
assertSame
-> strict equality (===
) -
assertInstanceOf
-> validates two objects or interfaces
More details: PHPUnit Assertions Documentation
A common use case is expecting an exception, as shown in the test code:
-
expectException
-> used to test exceptions
Skipping and Data Providers
-
markTestIncomplete
/markTestSkipped
-> marks the test as incomplete/skipped so it doesn’t count as failed. -
#[DataProvider('sumProvider')]
-> Uses the return of a function as test data.
Now, copy and run the updated test file:
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Service;
use App\Service\CalculatorService;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
class CalculatorServiceTest extends TestCase
{
static function sumProvider() {
yield [10, 10, 20];
yield [1, 1, 2];
yield [2, 2, 4];
}
#[Test]
#[DataProvider('sumProvider')]
function testSum($x, $y, $expected) {
$service = new CalculatorService();
$this->assertSame(
$service->sum($x, $y),
$expected
);
}
#[Test]
function testDivision() {
$service = new CalculatorService();
$this->assertEquals(
$service->division(10,10),
1
);
}
#[Test]
function testDivisionException() {
$this->expectException(\Exception::class);
$service = new CalculatorService();
$service->division(0,10);
}
}
Run the tests, and you will get:
Next chapter: Mocks and Stubs with more tips to finish on a high note!
Recommended reading: PHPUnit Attributes Documentation
Github repository: Leave a star!
What do you think was this useful ? Leave a comment!
Top comments (1)
If you have any questions leave a comment!