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