Presented by Guyllaume Cardinal for Cogeco Connexion
Guyllaume Cardinal
Consultant at Cogeco since May 2017
Development since 2006
Started with PHP, then ActionScript 3
Now back to PHP and recently React
5 years of TDD
I am far from an expert
I still have a lot to learn
I am far from an expert
...
I am also very opiniated on the subject. Oops.
TDD: Test-Driven Development
BDD: Behaviour-Driven Development
As you code (and even before you write a single line of code) you write an automated test for that code.
Then you write code to make that test pass.
TDD and BDD are complements, but they have a little subtlety that we'll explain a bit later
<?php
use PHPUnit\Framework\TestCase;
final class CalculatorTest extends TestCase
{
public function testItCanAddNumbersTogether(): void
{
$calculator = new Calculator();
$this->assertGreaterThan(0, $calculator->add(1, 1));
}
}
Only true on long-lived projects
Client will rarely see the value
Nothing is ever perfect...
Ok, I'm done with the sales pitch...
Simply put, BDD is just a way to write your tests. It complements TDD. It focusses on object behavior rather than input/output only.
Unit Test - TDD
<?php
use PHPUnit\Framework\TestCase;
final class CalculatorTest extends TestCase
{
public function testOnePlusOneEqualsTwo(): void
{
$calculator = new Calculator();
$this->assertEquals(2, $calculator->add(1, 1));
}
}
Unit Test - BDD
<?php
use PHPUnit\Framework\TestCase;
final class CalculatorTest extends TestCase
{
public function testItShouldAddNumbersTogether(): void
{
$calculator = new Calculator();
$this->assertGreaterThan(1, $calculator->add(1, 1));
}
}
Enforces S.O.L.I.D, G.R.A.S.P and single responsability principles
Your class will have only one reason to change: it's only responsability changed.
This translates into tests only breaking when behavior changes, not when implementation changes.
As with anything in TDD, it will take time to get a handle on this. Don't let that discourage you!
SOLID is a mnemonic acronym for five design principles intended to make software designs more understandable, flexible and maintainable.
Single responsibility principle: a class should have only a single responsibility
Open/closed principle: software entities should be open for extension, but closed for modification
Liskov substitution principle: objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program
Interface segregation principle: many client-specific interfaces are better than one general-purpose interface
Dependency inversion principle: one should depend upon abstractions, not concretions
A class should have only a single responsibility.
<?php
public class Calculator
{
public function add(int $x, int $y): int {
return $x + $y;
}
public function printEquation(string $equation, int $result): string {
echo "${equation} = ${result}";
}
}
$calculator = new Calculator();
$calculator->printEquation("1 + 1", $calculator->add(1, 1));
What's wrong here?
<?php
public class Calculator
{
public function add(int $x, int $y): int {
return $x + $y;
}
}
public class EquationPrinter
{
public function print(string $equation, int $result): string {
echo "${equation} = ${result}";
}
}
$printer = new EquationPrinter();
$calculator = new Calculator();
$printer->print("1 + 1", $calculator->add(1, 1));
Software entities should be open for extension, but closed for modification
<?php
public class AreaCalculator
{
/**
* @param Shape[] $shapes
*/
public function calculate($shapes): int {
$area = 0;
foreach ($shape in $shapes) {
$area += shape.width * shape.height;
}
return area;
}
}
<?php
public class Rectangle implements Shape
{
public function area(): int {
return this.width * this.height;
}
}
public class AreaCalculator
{
/**
* @param Shape[] $shapes
*/
public function calculate($shapes): int {
$area = 0;
foreach ($shape in $shapes) {
$area += shape.area();
}
return area;
}
}
Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program
<?php
public class Ellipse
{
public function getMajorAxisLength(): int { ... }
public function getMinorAxisLength(): int { ... }
}
public class Circle extends Ellipse
{
// Whatever specific implementation
}
<?php
public class Ellipse implements TwoDiameterFigure
{
public function getMajorAxisLength(): int { ... }
public function getMinorAxisLength(): int { ... }
}
public class Circle implements OneDiameterFigure
{
public function getDiameter(): int { ... }
}
Many client-specific interfaces are better than one general-purpose interface
<?php
public class IndustrialPrinterJob implements PrinterJob
{
public function sendPrintJob() { ... }
public function sendStapleJob() { ... }
public function sendCopyJob() { ... }
}
<?php
interface PrinterJob {
public function sendPrintJob()
}
interface StapleJob {
public function sendStapleJob()
}
interface CopyJob {
public function sendCopyJob()
}
public class IndustrialPrinterJob implements PrinterJob, StapleJob, CopyJob
{
public function sendPrintJob() { ... }
public function sendStapleJob() { ... }
public function sendCopyJob() { ... }
}
One should depend upon abstractions, not concretions
<?php
public class ContactFormBuilder
{
public function submit() {
// Tons of logic
this.submitHandler.submit(this);
}
public function setSubmitHandler(ContactFormSubmitHandler $handler) {
this.submitHandler = $handler;
}
}
<?php
public class ContactFormBuilder
{
public function submit() {
// Tons of logic
this.submitHandler.submit(this);
}
public function setSubmitHandler(SubmitHandler $handler) {
this.submitHandler = $handler;
}
}
<?php
public class DatabaseConnectionManager
{
public __construct() {
this.databaseConfiguration = new DatabaseConfiguration();
this.databaseConnection = new DatabaseConnection();
}
}
public class DatabaseConnectionManager
{
public __construct() {
this.databaseConfiguration = Drupal::get('database.configuration');
this.databaseConnection = Drupal::get('database.connection');
}
}
No class should ever instantiate its own dependencies or fetch them from a container.
Fear the new and get keywords!
<?php
use PHPUnit\Framework\TestCase;
final class DatabaseConnectionManagerTest extends TestCase
{
public function testItShouldBeEasyToTest(): void
{
$manager = new DatabaseConnectionManager(
Phake::mock(DatabaseConfiguration::class),
Phake::mock(DatabaseConnection::class)
);
}
}
Not only is it good practice, it also makes your class a breeze to test.
Be very aware of code smells as they often indicate a design problem in your code.
Code smells can also manifest themselves by a class being painful to test.
Common smells:
Testing Pyramid
Unit Tests
Mocks