Introduction to TDD

​Jan. 19th 2016 – viscaweb.com/meetings

Before starting

I'll be presenting some old knowledge sprinkled with personal opinions and experiences
 

I'll try to present the pros and cons of everything we talk about
 

This is my take on this subject, I may be wrong in several points. Please feel free to voice your opinion and challenge my words!

#TDD

What is a test

Software testing is an investigation conducted to provide stakeholders with information about the quality of the product or service under test."

#TDD

Why do we want them?

  • Reduce defects
  • Increase confidence
  • Reduce stress
  • Make change cheap
  • Improve efficiency
  • And more...

#TDD

less fun time debugging :(

Why isn't everybody using them?

#TDD

Ideas?

Why isn't everybody using them?

#TDD

1) They are slow to write

They increase the initial cost, but reduce the cost of maintenance and change

 

Time/project constraints prevent us, but bugs will slow us down even more if we don't have them

 

Code that was not designed to be tested is difficult to test, but... fuck

#TDD

Legacy code dilemma

In order to refactor, we need to be sure that we are not breaking anything. We need tests to do so.

 

But in order to be able to test the code, we have to refactor.

 

We could do end-to-end tests, but that would stop our development efforts for months.

#TDD

Code discussion

final class Greeter
{
    public function greet(string $name): void {
        echo "Hi {$name}!";
    }
}

$greeter = new Greeter();
$greeter->greet('Ricard');

"Code that was not designed to be tested is difficult to test"

#TDD

final class Greeter
{
    public function greet(string $name): void {
        echo "Hi {$name}!";
    }
}

/** @test */
public function can_greet_a_particular_person()
{
    $greeter = new Greeter();
    $greeter->greet('Ricard');

    $this->assertEquals('Hi Ricard!', $output);
}

#TDD

interface Printer {
    public function output(string $value): void;
}

final class ConsolePrinter implements Printer {
    public function output(string $value): void {
        echo $value;
    }
}

final class Greeter
{
    /** @var Printer **/
    private $printer;

    public function __construct(Printer $printer) {
        $this->printer = $printer;
    }

    public function greet(string $name): void {
        $this->printer->output("Hi {$name}!");
    }
}

#TDD

// Test code (w/ PHPUnit mocks)
$printer = $this->getMock(Printer::class);
$printer->expect($this->once())
        ->method('output')
        ->with('Hi Ricard!');

$greeter = new Greeter($output);
$greeter->greet('Ricard');

// Test code (w/ Prophecy)
$printer = $this->prophesize(Printer::class);

$greeter = new Greeter($printer->reveal());
$greeter->greet('Ricard');

$printer->output('Hi Ricard!')->shouldHaveBeenCalled();

// Production code
$greeter = new Greeter(new ConsolePrinter());
$greeter->greet('Ricard');

Why isn't everybody using them?

#TDD

2) They are difficult to write

They require experience        katas!

They require knowledge        ViscaMeetings, books, articles, talks

Even with some knowledge and experience, some areas are difficult to test        Reduce the confidence by increasing abstraction. Tests will run faster, will be faster to write, but we'll be testing in different levels of abstraction.

#TDD

Design discussion

"Even with some knowledge and experience, some areas are difficult to test"

Let's pretend that we work for an elevator company.

How would we test the different combinations of floors?

#TDD

Design discussion (continued)

Which tests would give you the most confidence?

 

Which would be "good enough" while being fast to run and write?

 

How would you fake some situations which you cannot easily recreate in real life (closed floor, underground, etc.)?

#TDD

The cycle of TDD

#TDD

The cycle of TDD

  1. Think about what you are going to test

  2. Write a test

  3. Wonder if the test is correct (rewrite until it is)

  4. Run the test: red

  5. Write the simplest implementation possible

  6. Run the test: green

  7. Refactor, clean the mess we just created

#TDD

Start small

/** @test */
public function generates_first_batch_of_numbers()
{
    $this->assertEquals(
        [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31],
        generatePrimes()
    );
}

#TDD

/** @test */
public function generates_up_to_first_number()
{
    $this->assertEquals([2], generatePrimes());
}

// Red
function generatePrimes() {
}

// Green
function generatePrimes(): int[] {
    return [2];
}

// Refactor? Not yet

#TDD

/** @test */
public function generates_up_to_second_number()
{
    $this->assertEquals([2, 3], generatePrimes());
}

// Red
function generatePrimes(): int[] {
    return [2];
}

// Green
function generatePrimes(): int[] {
    return [2, 3];
}

// Refactor?

#TDD

When & why to refactor?

Fake it

Obvious implementation

Triangulate

(or third strike and refactor?)

#TDD

Start small

Don't try to accomplish too much, you might have to backtrack

 

Start with small steps, only increase complexity if you feel confident enough

#TDD

Discussion

Personal experiences with testing

 

What type of testing do you know, or currently use in LIFE/LIBE?

 

Why do you think some kinds of test have been discarded in the past?

#TDD

Hoare logic

  • Precondition
  • Command
  • Postcondition

#TDD

Testing strategies

  • Manual
  • Automated
  • Test-first
  • Test-driven

#TDD

Testing strategies

Manual

  • No regression test.
  • Test is only checked once, and then the knowledge is lost

#TDD

Testing strategies

Automated

  • Test is written after the fact. We are proving implementation as it is working now, but we may be wrong
  • Time constraints and laziness will frequently prevent us from creating tests after writing the implementation. We are focused on delivering features, and testing interferes with that

#TDD

Testing strategies

Test-first

  • We design upfront and then write tests that express that knowledge
  • We are violating KISS and YAGNI
  • We are going back to waterfall

#TDD

Testing strategies

Test-driven

  • We write the code we wish we had, then implement it. JUST what we need.
  • Emergent design
  • Serve as regression tests, they are not as thorough

#TDD

Effects on design

  • Small, loosely coupled objects
  • KISS
  • YAGNI

#TDD

Types of tests

  • Unit
    • A single part of your own code cut from every dependencies
    • Different definition of unit depending on school of thought
  • Integration
    • Same as unit test but for the code that must interact with an external component/library
  • Functional end-to-end test
    • The whole application tested via the inputs/outputs
    • System / application / acceptance

#TDD

Definition of unit

  • Mockist: complete isolation from collaborators
  • Classic: use real implementations if they are cheap to build. Tests act as mini-integration tests

#TDD

Speed of a test suite

  • Slow
    • Slow feedback loop
    • Slow to run, which leads to running them less often
    • Running them less often leads to writing more code without testing, which leads to defects
  • Fast
    • Fast feedback loop
    • Can be run hundreds of times a day
    • Every modification is immediately validated against existing assumption

#TDD

Test doubles

In automated unit testing, it may be necessary to use objects or procedures that look and behave like their release-intended counterparts, but are actually simplified versions that reduce the complexity and facilitate testing. A test double is a generic (meta) term used for these objects or procedures."

#TDD

Types of test doubles

  • Dummy: objects are passed around but never actually used. Usually they are just used to fill parameter lists.
  • Fake: actually have working implementations, but usually take some shortcut which makes them not suitable for production (an InMemoryTestDatabase is a good example).
  • Stub: provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test.
  • Spy: are stubs that also record some information based on how they were called. One form of this might be an email service that records how many messages it was sent.
  • Mock: are pre-programmed with expectations which form a specification of the calls they are expected to receive. They can throw an exception if they receive a call they don't expect and are checked during verification to ensure they got all the calls they were expecting.

#TDD

Black-box testing

Is a method of software testing that examines the functionality of an application without peering into its internal structures or workings. This method of test can be applied to virtually every level of software testing: unit, integration, system and acceptance. It typically comprises most if not all higher level testing, but can also dominate unit testing as well."

#TDD

White-box testing

(Also known as clear box testing, glass box testing, transparent box testing, and structural testing.)

Is a method of testing software that tests internal structures or workings of an application, as opposed to its functionality (i.e. black-box testing). In white-box testing an internal perspective of the system, as well as programming skills, are used to design test cases."

#TDD

Starting at the edges

  • Outside-in
    • Mockist / London school / top-down
    • Write with an acceptance test, start developing from the use case mocking the interactions with lower-level collaborators
  • Inside-out
    • Classic / Chicago school / Detroit school / bottom-up
    • Start small and work your way up
  • Middle-out
    • Create a domain model, and connect to the necessary commands to make acceptance tests pass

#TDD

What else?

Lots more!  This is too big of a topic, and will require years of experience, reading, pairing, etc., to discover which things work for you, your organization, your deadlines...

#TDD

Thanks!