A full path from ticket to CI with Behat and test-driven SOLID, on a dated Symfony 6 backdrop.
Why this book
My library already has Kent Beck, who gives the TDD method (Test-Driven Development: By Example), and Matt Zandstra, who gives the PHP objects (PHP 8 Objects, Patterns, and Practice). What was missing was the whole path for a Symfony dev, from the client's need to the pipeline. I did Symfony 2 and 3 at Netime, Magento and Symfony at Decitre, PHP 8 at Teranga-Software. As a former DWWM instructor and certification examiner, I was looking for a course to hand to someone changing careers.
Rainier Sarabia, a software engineering manager based in Australia, published this book with Packt in January 2023. I read it on ExpertEdge: 5 h 36 of course, eleven sections. The opening makes a real case for TDD, "TDD is a process, and it's a way of thinking" (lesson 3), and takes down four misconceptions (lesson 4). Still, the book is thin for what it promises. Half the course is dated step-by-step (Docker, Jira, the AWS console) or OOP refreshers.
The word Symfony appears 253 times in the text, mostly in paths such as codebase/symfony/tests/Unit. The running example (an inventory of toy cars for a museum) runs on a Symfony 6 skeleton with Doctrine (the ORM, which maps PHP objects to database rows) and MySQL 8. The framework is mostly a backdrop, except for its code generators and one integration test (idea 6).
The ideas that stay
1The need first: code comes after
Not a line of PHP at the start. Museum volunteers log donations in a shared spreadsheet: a car's colour is typed "blue", "flames" or with a typo, and nobody can find anything. Sarabia derives needs, problems and constraints from it, then user stories (a feature described from the user's point of view) filed under epics (the big batches) in Jira, the task tracker. Writing code straight from a story is, for him, "a recipe for future disaster" (lesson 7), and he admits: "this is how I used to develop software" (lesson 7). His chain: the need becomes a scenario, the scenario a test, the test dictates the code.
2Gherkin, Behat, Mink: a shared language for the business
BDD (Behavior-Driven Development) has you write first what the system must do, in a language the business side can read. Gherkin is that language: "a language used to define business scenarios and goals" (lesson 34), in plain English. Behat is the PHP framework that runs those scenarios. Mink is a library that drives a simulated browser: "think about a robot version of yourself testing your program for you through a web browser" (lesson 34).
Feature: Home page
In order to welcome visitors
As a visitor
I need to be able to see the Symfony logo
Scenario: See the Symfony logo
Given I have access to the home page URL
When I visit the home page
Then I should see the Symfony Logo
Given sets the starting state, When the action, Then the expected result. Behat generates the empty PHP methods (--append-snippets), for you to fill in. Sarabia had wanted to skip Gherkin, until a project with a third-party company whose tests lived inside his application: "I wish there were a common format that we could share" (lesson 36).
3One thread from ticket to unit test
Beck, in his book, stays at the unit test (a test of a single class or method, with no database or network). Here the levels chain together on one ticket, registering a clerk for the museum's inventory. That ticket becomes a Gherkin feature that Behat runs and that fails. A Symfony functional test fails next: it sends an HTTP request to a controller and checks the response. Then comes an integration test, which makes several classes work with a real database set aside for tests, then the unit tests. To turn green, the author leans on the security component: "Luckily, Symfony makes it easy to implement security" (lesson 40). The make:user and make:registration-form generators write the user and the registration (lessons 40 and 41).
4Red first, even for a test that tests nothing
The first rule is "We should always fail our test first" (lesson 29), for four reasons (lesson 3). The first: prove that PHPUnit recognizes the test, so that the environment is properly set up. Hence the cycles opened by $this->fail("--- RED ---"). The other three: define what the code must do, list the remaining work, make sure that code gets tested. The first real example (lesson 24) fails through a subtraction placed there on purpose:
// ✗ BEFORE: the test expects 6, it gets 0
return $a + $b - $c;
// ✓ AFTER: we add $c, the test passes
return $a + $b + $c;
To turn green, the rule is "We did the minimum work to pass the test" (lesson 38).
Tests are grouped into suites (Unit, Integration) declared in phpunit.xml, PHPUnit's configuration. The author has known a single suite of thousands of tests: "you run the suite and wait an hour, only to find out there's one broken unit test" (lesson 28). On code coverage (the share of the code executed by the tests): "I still believe that 50% test coverage is better than 0% test coverage" (lesson 25).
5SOLID driven by failing tests
SOLID is an acronym for five design principles:
- S (single responsibility): one class, one reason to change.
- O (open/closed): open for extension, closed for modification.
- L (Liskov): one implementation must be able to replace another without breaking the behaviour.
- I (narrow interfaces): no useless method forced on classes.
- D (dependency inversion): depend on abstractions rather than on concrete classes.
The running example is a car validator, and each principle is born from a red test. For the S, the ToyCar object holds only data, and a separate class orchestrates. For the O, a YearValidator and a NameValidator, each tested, slip into an array, and a future colour validator won't force you to reopen the main class:
$validator = new ToyCarValidator();
$validator->setValidators([
'year' => new YearValidator(),
'name' => new NameValidator(),
]);
For the L, the year validator throws a ToyCarTooOldException that extends \Exception. Nobody catches it and the test blows up, because the interface promises a different exception.
// ✗ BEFORE: outside the interface contract
class ToyCarTooOldException extends \Exception {}
// ✓ AFTER: the interface declares ToyCarValidationException
class ToyCarTooOldException extends ToyCarValidationException {}
For the I, adding validateLength to the shared interface doesn't suit a colour validator: the colour is a ToyColor object, not a string. So a StringValidatorInterface is split out. Sarabia admits to interfaces full of empty methods and "TODO"s.
6Inject, double, and keep the domain away from Doctrine
For the D, Sarabia calls this principle "In terms of making a class more testable, the DIP is probably the most important principle on the list for me" (lesson 51). The validator no longer creates its dependencies, they are passed in. The test replaces the missing classes with test doubles (fake objects whose answer you fix), which saves waiting for the colleague who takes two weeks to write the database save:
$validatorStub = $this->createMock(ToyCarValidatorInterface::class);
$validatorStub->method('validate')->willReturn($validationResultStub);
$toyWriterStub = $this->createMock(WriterInterface::class);
$toyWriterStub->method('write')->willReturn(true);
$processor = new ToyCarCreator($validatorStub, $toyWriterStub);
ToyCarCreator, the processing class (the book says processor), validates, then writes. It handles a ToyCar object with no Doctrine annotation (a special comment that ties a class to a table), a POPO (a PHP object with no dependency). Yet the book gives the same name to the Doctrine entity (lesson 30), which its annotations tie to the ORM. For SOLID, Sarabia injects by hand.
Symfony can inject by itself, and the book shows it once (lesson 31). A coffee service there saves a total in the test database. The services.yaml file, where Symfony declares its services, turns on autowiring: Symfony reads the constructor and supplies the requested objects by itself. The test extends KernelTestCase, the test class that boots Symfony. It asks the container, the object that builds services, for the service instead of writing new.
7CI replays everything on every push
CI (continuous integration) runs the tests on every change. Bitbucket Pipelines, Bitbucket's CI service, reads a YAML file. The book's file (excerpt, image installation omitted) brings up the Docker containers. It then runs the Symfony tests with coverage and the Behat scenarios (five announced, four written out):
script:
- cd docker
- docker-compose build && docker-compose up -d
- docker exec -i docker_server-web_1 /var/www/html/symfony/setup.sh
- docker exec -i docker_server-web_1 /var/www/html/symfony/runCoverage.sh
- docker exec -i docker_server-web_1 /var/www/html/behat/runBehatTests.sh
The setup.sh scripts install the packages and prepare the database. CD (continuous delivery, automatic release to production) goes through AWS. Its pipeline has three steps: the tests, uploading the archive to S3 (Amazon's storage), installing on the server with CodeDeploy (the AWS service that deploys). That pipeline runs in 18 minutes (lesson 59), then the author checks the application by hand.
My take, honestly
I liked seeing a single ticket go through every level of testing. The test that blows up because an exception is of the wrong type teaches me more about the Liskov principle than a diagram does. The author is credible when he admits to three assertions in one test: "it's still better to do one assertion per test" (lesson 48), and then he moves on anyway.
The rest smells of the cellar. The Docker image is a release candidate (php:8.1.3RC1) and PHPUnit is at 9.5, with annotations in comments. No syntax specific to PHP 8 appears. Only the str_contains function, new in PHP 8.0, slips in (lesson 42): "PHP 8" in the title mostly refers to the Docker image. Mink's Goutte driver is abandoned (my addition). A stray "Laravel" turns up at the end of lesson 13 although the project is Symfony.
The worst: ToyCarCreator, the class that is supposed to validate a car, calls the validator without ever reading its answer. Only an exception thrown by the validator would stop the flow, and the test never checks that: its fake validator always answers yes. A data provider (the method that feeds a test with sets of inputs) declared private completes the picture. It passes in 9.5, but PHPUnit's documentation wants public providers (my addition).
My verdict, 5.2 out of 10, comes from that contrast: the BDD-to-SOLID thread is good, the packaging has aged. Read the sections on Behat and SOLID, skim the rest, and keep Beck and Zandstra beside you. I now want the same chain with recent PHPUnit attributes and autowiring throughout.
Odilon
Still relevant in 2026?
The principles, yes. The tools, partly. From my own practice, replace Goutte with Symfony's HTTP client or Panther (a tool that drives a real browser), docker-compose with docker compose, and annotations with attributes if you are on PHPUnit 10 or later. Check every command before you copy it.
Who is it for?
Read it if
- You are a PHP or Symfony dev and have never tied a ticket to an automated test.
- You want to see SOLID born from red tests rather than from a diagram, or you coach juniors.
- You want to try Behat and Gherkin before proposing them to a team.
Skip it if
- You already master TDD: this book only adds Behat, SOLID and CI.
- You are looking for modern Symfony 7: the framework is mostly a backdrop.
- You want current deployment: the AWS part is a dated step-by-step.
This book is part of ExpertEdge, Packt's subscription for technical books. The code ODILON50 takes 50% off the yearly plan: the book on ExpertEdge. Affiliate link: I earn a commission if you subscribe with this code, and my ratings don't depend on it.
For going further
- The Test-Driven Development: By Example review, by Kent Beck: the method, without Docker or Jira.
- The PHP 8 Objects, Patterns, and Practice review, by Matt Zandstra: the PHP objects that this book only recaps.
Source: the ExpertEdge course version of the book, lessons 1 to 63 read in detail, that is every lesson extracted out of the 72 announced, quizzes excluded. Year and pages taken from Packt's publisher page.
Comments (0)