LEARNING TDD/BDD
Session 1 - The Basics
Presented by Guyllaume Cardinal for Cogeco Connexion
Who am I?
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
A little disclaimer
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.
So many acronyms...
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
To it's simplest, this is TDD
<?php
use PHPUnit\Framework\TestCase;
final class CalculatorTest extends TestCase
{
public function testItCanAddNumbersTogether(): void
{
$calculator = new Calculator();
$this->assertGreaterThan(0, $calculator->add(1, 1));
}
}
The Why of TDD
- How long do you spend testing manually?
- Are those manual tests shared with your peers?
- How long do you spend fixing bugs versus implementing features?
- Are you afraid to refactor production code?
- How long do you spend not understanding the intent of the code?
- Do you have to test the same things over and over again?
If you're tired of all the above, that's why TDD
Benefits of TDD/BDD
For a Developer:
- Safer refactoring
- Forces better coding practices
- Provides "living" documentation
- Increasing returns
- Easier maintenance
- Quicker failure when developing
- Faster development (with time)
- Regression is covered
Benefits of TDD/BDD
For The client:
- Becomes an investement (with time)
- Ensures his product works as specified
- Reduce maintenance costs
Only true on long-lived projects
Client will rarely see the value
Cons of TDD/BDD
Nothing is ever perfect...
- Way slower at first
- Your first tests will suck
- It will cause a lot of refactoring, often
- You will have to read (eww) on coding patterns and ideologies
- You will need to change how you code
But it's worth it
Ok, I'm done with the sales pitch...
Difference between TDD and BDD
Simply put, BDD is just a way to write your tests. It complements TDD. It focusses on object behavior rather than input/output only.
What does it look like?
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));
}
}
What does it look like?
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));
}
}
Benefits of BDD
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!
S.O.L.I.D.
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
Single Responsibility Principle
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?
Single Responsibility Principle
<?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));
Open/Closed Principle
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;
}
}
Open/Closed Principle
<?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;
}
}
Liskov substitution principle
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
}
Liskov substitution principle
<?php
public class Ellipse implements TwoDiameterFigure
{
public function getMajorAxisLength(): int { ... }
public function getMinorAxisLength(): int { ... }
}
public class Circle implements OneDiameterFigure
{
public function getDiameter(): int { ... }
}
Interface Segregation principle
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() { ... }
}
Interface Segregation principle
<?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() { ... }
}
Dependency Inversion principle
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;
}
}
Dependency Inversion principle
<?php
public class ContactFormBuilder
{
public function submit() {
// Tons of logic
this.submitHandler.submit(this);
}
public function setSubmitHandler(SubmitHandler $handler) {
this.submitHandler = $handler;
}
}
Quickly before we end...
Dependency INjection
<?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!
Dependency INjection
<?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.
Code Smells
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:
- Large class
- Duplicated code
- Too many parameters
- Very long lines
- Class that does too little
- Very long function names
- Feature envy
- ...and so much more!
Breathe...
We're done! Yay!
Next Week
Testing Pyramid
Unit Tests
Mocks
TDD Lunch and learn - Session 1
By Guyllaume Cardinal
TDD Lunch and learn - Session 1
Presentation done for Cogeco to show the basics of TDD. The first of 5 sessions.
- 732