Boost PHP Code Quality with Unit Testing and PHPSpec: A Practical Guide
Learn how to enhance PHP code quality by implementing unit tests using PHPSpec and PHPUnit, covering SOLID principles, mock objects, test setup, code coverage integration, and best practices for writing maintainable, reliable backend code.
1. Unit Testing
The most common testing approach is writing unit tests that verify individual units of code under the assumption that everything works as expected. Proper unit tests require the code to follow basic design rules, especially the SOLID principles.
By applying the Single Responsibility Principle, the code focuses on a single functionality, allowing tests to target a small part of the project.
Using the Liskov Substitution Principle and Dependency Inversion Principle ensures the code does not depend on concrete implementations; it works with any object that implements the required interface.
In unit testing we replace all dependent services with mock objects so that only one class is tested at a time. A mock implements the same interface as the real service but provides controlled behavior, e.g., returning a fixed exchange rate for a specific currency.
2. Which framework should be used?
Several frameworks can achieve this goal; the most common is PHPUnit. In practice, behavior‑driven testing with PHPSpec often yields better results. For the project we chose PHPSpec.
$ php composer.phar require --dev phpspec/phpspecIf PHing is configured, a build target can be added to build.xml:
<target name="phpspec">
<exec executable="bin/phpspec" passthru="true" checkreturn="true">
<arg line="run --format=pretty"/>
</exec>
</target>
...
<target name="run" depends="phpcs,phpcpd,phan,phpspec"/>Each service class requires a corresponding test class. PHPSpec simplifies mock creation; you declare mock objects as parameters of the test functions, and PHPSpec generates them automatically.
// spec/Domain/PriceComparatorSpec.php
<?php
namespace spec\Domain;
use Domain\Price;
use Domain\PriceConverter;
use PhpSpec\ObjectBehavior;
class PriceComparatorSpec extends ObjectBehavior
{
public function let(PriceConverter $converter)
{
$this->beConstructedWith($converter);
}
public function it_should_return_equal()
{
$price1 = new Price(100, 'EUR');
$price2 = new Price(100, 'EUR');
$this->compare($price1, $price2)->shouldReturn(0);
}
public function it_should_convert_first(PriceConverter $converter)
{
$price1 = new Price(100, 'EUR');
$price2 = new Price(100, 'PLN');
$priceConverted = new Price(25, 'EUR');
$converter->convert($price2, 'EUR')->willReturn($priceConverted);
$this->compare($price1, $price2)->shouldReturn(1);
}
}The spec defines three functions: let() – initializes the service with its dependencies. it_* methods – implement the actual tests, using mocks for the PriceConverter interface.
Mocks are created by declaring them as arguments; you can also configure return values or expectations on them.
3. How to set up tests?
PHPSpec documentation provides many examples; here are some useful patterns.
Building test objects
The simplest way is to call $this->beConstructedWith(...) with the parameters required by the constructor. If the object should be created via a factory method, use $this->beConstructedThrough($methodName, $argumentsArray).
Matching runtime arguments in mocks
To assert that a mock method is called with a specific argument:
$mockObject->someMethod("desired value")->shouldBeCalled();To make a mock method return a value:
$mockObject->someFunction("some input")->willReturn("some value");If the exact argument is not important, you can use a token that matches any value:
use Prophecy\Argument\Token\AnyValueToken;
$mockObject->someFunction(new AnyValueToken())->willReturn(true);For more complex checks, provide a callback that validates the arguments:
use Prophecy\Argument\CallbackToken;
$checker = function (Message $message) use ($to, $text) {
return $message->to === $to && $message->text === $text;
};
$msgSender->send(new CallbackToken($checker))->shouldBeCalled();Matching runtime exceptions
When an exception is part of the interface, you can assert it is thrown:
$this->shouldThrow(\DomainException::class)->during('execute', [$command, $responder]);5. Where to find more examples?
Refer to the PHPSpec documentation for additional use cases that can make your test code elegant.
Code Coverage
PHPSpec includes an extension for generating code‑coverage reports. Install it with:
$ php composer.phar require --dev leanphp/phpspec-code-coverageEnable the extension in phpspec.yml:
extensions:
LeanPHP\PhpSpec\CodeCoverage\CodeCoverageExtension: ~By default it uses Xdebug, but the native phpdbg debugger is faster: $ phpdbg -qrr phpspec run Adjust the build target in build.xml accordingly:
<target name="phpspec">
<exec executable="phpdbg" passthru="true" checkreturn="true">
<arg line="-qrr bin/phpspec run --format=pretty"/>
</exec>
</target>
...
<target name="run" depends="phpcs,phpcpd,phan,phpspec"/>The coverage report is generated in the coverage/ directory as an HTML page.
6. When should you write unit tests?
The answer is as often as possible. Unit tests run quickly and provide the simplest way to verify code, especially for interpreted languages like PHP. They help catch runtime issues early and encourage writing more organized, testable code.
Summary
Now is the time to pick up your keyboard, write tests for your project, and gain confidence in your code.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
360 Zhihui Cloud Developer
360 Zhihui Cloud is an enterprise open service platform that aims to "aggregate data value and empower an intelligent future," leveraging 360's extensive product and technology resources to deliver platform services to customers.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
