DEV Community

Cover image for Module 5: Advanced Topics and Best Practices
Antonio Silva
Antonio Silva

Posted on

Module 5: Advanced Topics and Best Practices

Congratulations on reaching the final module of our course! You've come a long way, from the initial setup of PHPUnit to mastering the use of Mocks and Stubs. Now it's time to refine your skills, ensure your tests are a sustainable asset for your project, and integrate this practice into your professional development workflow.

In this module, we will cover code coverage analysis, integration with CI/CD pipelines, and the best practices that separate a junior developer from a senior one when it comes to testing.

1. Code Coverage Analysis: What are your tests not seeing?

Writing tests is great, but how do you know if you're testing enough parts of your code? Code coverage is a metric that answers this question. It measures, as a percentage, how many lines or "branches" (such as if/else statements) of your production code were executed during your test suite.

Why is it important?

  • Identifies Untested Code: Code coverage is a fantastic tool for visualizing which parts of your application lack tests.
  • Increases Confidence: High code coverage gives the team more confidence to perform refactoring and add new features, knowing that there is a safety net to detect regressions.
  • Not a Silver Bullet: It's crucial to understand that 100% coverage doesn't mean 100% quality. It's possible to execute a line of code without actually testing its logic correctly (for example, without adequate assertions). Coverage is a guide, not a blind end goal.

How to Generate a Coverage Report:

To generate coverage reports, PHPUnit needs a code coverage driver such as Xdebug or PCOV. Xdebug is more common in development environments, while PCOV is lighter and faster, making it an excellent option for automation pipelines.

Once you have a driver installed and activated, you can generate an HTML report, which is very visual and easy to navigate.

1. Project structure

coverage/
├── src/
│   └── Calculator.php
├── tests/
│   └── CalculatorTest.php
├── composer.json
└── phpunit.xml
Enter fullscreen mode Exit fullscreen mode

2. Source code – src/Calculator.php

<?php

namespace App;

class Calculator
{
    public function sum(int $a, int $b): int
    {
        return $a + $b;
    }

    public function subtract(int $a, int $b): int
    {
        return $a - $b;
    }

    public function divide(int $a, int $b): float
    {
        if ($b === 0) {
            throw new \InvalidArgumentException("Division by zero");
        }

        return $a / $b;
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Teste PHPUnit – tests/CalculatorTest.php

<?php

use PHPUnit\Framework\TestCase;
use App\Calculator;

class CalculatorTest extends TestCase
{
    public function testSum()
    {
        $calc = new Calculator();
        $this->assertEquals(5, $calc->sum(2, 3));
    }

    public function testSubtract()
    {
        $calc = new Calculator();
        $this->assertEquals(1, $calc->subtract(3, 2));
    }

    public function testDivide()
    {
        $calc = new Calculator();
        $this->assertEquals(2.5, $calc->divide(5, 2));
    }

    public function testDivideByZero()
    {
        $this->expectException(\InvalidArgumentException::class);

        $calc = new Calculator();
        $calc->divide(5, 0);
    }
}
Enter fullscreen mode Exit fullscreen mode

4. PHPUnit configuration – phpunit.xml

<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/12.0/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true">

    <testsuites>
        <testsuite name="Testes da Calculadora">
            <directory>tests</directory>
        </testsuite>
    </testsuites>

    <source>
        <include>
            <directory>src</directory>
        </include>
    </source>

</phpunit>
Enter fullscreen mode Exit fullscreen mode

The new <source> tag (in more recent versions of PHPUnit) or <coverage>/<filter> (in older versions) specifies that only the code within the src directory should be considered for coverage analysis.

5. Run PHPUnit with the coverage flag.

In the terminal, execute the following command:

vendor/bin/phpunit --coverage-html coverage-report
Enter fullscreen mode Exit fullscreen mode

This command will run your tests and create a detailed HTML report in a new folder, coverage-report. Open the index.html file in that folder in your browser. You will see an overview of your project's coverage and can navigate through specific files and lines, seeing exactly what was and what was not executed.

2. Continuous Integration (CI/CD): Automating Your Safety Net

Continuous Integration (CI) is the practice of automatically integrating code changes from multiple contributors into a single software repository. A key part of CI is the automatic execution of tests with each new integration (for example, on every git push).

Why integrate tests into a CI/CD pipeline?

  • Fast Feedback: Developers know almost immediately if their change broke some part of the application.

  • Regression Prevention: Ensures that faulty code doesn’t reach the main branch of your project, let alone production.

  • Consistent Quality: Maintains a quality standard by ensuring all tests pass before any code merge.

Tools like GitHub Actions, GitLab CI/CD, and Jenkins make it easy to set up a pipeline that:

  • Checks out your code.

  • Installs dependencies with composer install.

  • Runs your test suite with ./vendor/bin/phpunit.

  • (Optional) Fails the build if any test fails.

  • (Optional) Publishes coverage reports for analysis.

Your PHPUnit test suite becomes the first automated line of defense for your project.

3. Refactoring for Testability

Sometimes, testing a class is difficult. This is usually not a problem with the testing tool itself, but rather a sign that the class design could be improved. Writing tests forces us to think about coupling and cohesion.

Principles for Testable Code:

  • Dependency Injection: Instead of creating dependencies inside a class (with the new operator), inject them through the constructor. This allows you to easily replace real dependencies with mocks or stubs in your tests. (We’ve done this in modules 3 and 4!)

  • Single Responsibility Principle (SRP): Classes that “do too many things” are hard to test. Break them down into smaller, more focused classes. If your class name contains the word “And”, it may be violating SRP (e.g., UserCreatorAndEmailSender).

  • Avoid Global State and Singletons: Functions and classes that rely on global state are a nightmare to test since one test’s result can affect another.

  • Program to an Interface, Not an Implementation: Depending on interfaces rather than concrete classes (as we did with Logger and Translator) makes your code more flexible and easier to mock in tests.

4. Testing Patterns and Antipatterns

Best Practices (Patterns):

  • One Test, One Logical Assertion: Ideally, each test method should verify one thing. There can be multiple physical asserts, but they should all check the same logical concept.

  • Arrange, Act, Assert (AAA): Structure your tests into three clear sections:

    • Arrange: Prepare the environment, create objects and stubs.
    • Act: Execute the method being tested.
    • Assert: Verify that the result is as expected.
  • Descriptive Names: testSumShouldThrowExceptionWhenArgumentIsNotNumeric() is much better than testSum().

  • Fast Tests: Unit tests should be extremely fast. Avoid interacting with the file system, network, or database—use test doubles for that.

Mistakes to Avoid (Antipatterns):

  • Dependent Tests: A test that only passes if another test has run before it. Use setUp() and tearDown() to ensure isolation.

  • Slow Tests: Tests that take too long to run will eventually be ignored by the team.

  • Fragile Tests: Tests that break with any small refactor in production code. This often happens when you test implementation instead of behavior.

  • Testing Trivial Logic: Don’t waste time testing simple getters and setters that contain no logic. Focus on complex behavior.

Course Conclusion

You’ve reached the end of our PHPUnit journey! Now, you not only know how to write tests, but also why each technique matters. You’ve learned how to isolate your code, test complex interactions, measure your test effectiveness through coverage, and integrate everything into a professional workflow.

The next step is to apply this knowledge. Start small: pick a new feature in your project and write tests for it. Take an existing class without tests and try adding them. Constant practice is what will solidify your skills.

Automated tests are not a waste of time—they’re an investment in your software’s quality, maintainability, and longevity.

Welcome to the world of more reliable and professional software development!

Top comments (0)