Real Unit Testing

Behavioral Real Unit Testing

I'm Chris

We Will Talk About...

We Won't Talk About...

Types of Tests

Acceptance Tests

Test the entire system — end to end tests.

Does the system as a whole work behave as expected?

Examples

Integration Tests

Test large scale components and libraries by actually using them.

Do my components work behave as expected?

Examples

Unit Tests

Test the smallest units of your application.

For most applications built with an OO language, this means a class.

Do my individual routines and abstractions work behave as expected?

All Tests Are Driven by Behavior

How to Write Unit Tests

https://github.com/chrisguitarguy/RealUnitTesting

The Simplest Shopping Cart

We're going to write tests for a shopping cart. It's a bad shopping cart — way too simple. Ignore that.

We're going to test the Cart object.

Products

<?php
// Product.php
interface Product
{
    public function name();

    public function cost();
}

// StubProduct.php
class StubProduct implements Product
{
    private $name;
    private $cost;

    public function __construct($name, $cost)
    {
        $this->name = $name;
        $this->cost = $cost;
    }

    public function name()
    {
        return $this->name;
    }

    public function cost()
    {
        return $this->cost;
    }
}

 

<?php
interface Product
{
    public function name();

    public function cost();
}

The Cart

<?php
// Cart.php
interface Cart
{
    public function addProduct(Product $prod);

    public function total();
}

// DefaultCart.php
class DefaultCart implements Cart
{
    private $products = array();

    public function addProduct(Product $prod)
    {
        $this->products[] = $prod;
    }

    public function total()
    {
        $total = 0.0;
        foreach ($this->products as $product) {
            $total += $product->amount();
        }

        return $total;
    }
}

 

<?php
// Cart.php
interface Cart
{
    public function addProduct(Product $prod);

    public function total();
}

1. Start With a Stub

<?php
class DefaultCartTest
{
    use \Counterpart\Assert;

    // we will change this name
    public function testTotal()
    {
        
    }
}

2. Write an Action

We want to make sure total works, so we're going to call it.

This will fail. We'll get to where $cart comes from in a minute.

// we will change this name
public function testTotal()
{
    $total = $cart->total();
}

3. Write a Verification

Now we need to check $total. Let's test the simplest case: an empty cart should have a zero total.

public function testTotal()
{
    $total = $cart->total();

    $this->assertEquals(
        0.0,
        $total,
        'Total should be zero for empty carts'
    );
}

4. Name It

A test's job is to fail. When it does fail, a test should give as much context as possible about what failed.

Name your tests based on behavior. Not methods.

Describe the behavor as a sentence, then put it in a test name.

Empty carts should have a zero total.

 

public function testEmptyCartHasZeroTotal()
{
    $total = $cart->total();

    $this->assertEquals(
        0.0,
        $total,
        'Total should be zero for empty carts'
    );
}

 

Lots of context on what behavior was incorrect or broken.

Test Failure

5. Create the Setup

Now we can finally make the test pass: create the setup code necessary to run the test.

Simply creating the $cart object is all we need to do.

 

public function testEmptyCartHasZeroTotal()
{
    $cart = new DefaultCart();

    $total = $cart->total();

    $this->assertEquals(
        0.0,
        $total,
        'Total should be zero for empty carts'
    );
}

Every Test Has Three Parts

  1. Setup — prepare the test
  2. Action — interact with the object under test
  3. Verification — verify that the object under test behaved as expected

 

public function testEmptyCartHasZeroTotal()
{
    // Setup
    $cart = new DefaultCart();

    // Action
    $total = $cart->total();

    // Verification
    $this->assertEquals(
        0.0,
        $total,
        'Total should be zero for empty carts'
    );
}

What About a Cart With Products?

This is an entirely different behavior, so it should be a different test.

Cart total should be the sum of all its product costs.

Stub the Test

<?php
class DefaultCartTest
{
    // ...

    public function testTotal()
    {
        
    }
}

Add an Action

public function testTotal()
{
    $total = $cart->total();
}

Verify Expecations

public function testTotal()
{
    $total = $cart->total();

    $this->assertEquals(
        21.0,
        $total,
        'Total should be the sum of all product costs'
    );
}

Rename the Test

public function testTotalShouldBeTheSumOfProductCostsInCart()
{
    $total = $cart->total();

    $this->assertEquals(
        21.0,
        $total,
        'Total should be the sum of all product costs'
    );
}

Complete the Setup

public function testTotalShouldBeTheSumOfProductCostsInCart()
{
    $cart = new DefaultCart();
    $cart->addProduct(new StubProduct(
        'one product',
        10.50
    ));
    $cart->addProduct(new StubProduct(
        'two product',
        10.50
    ));

    $total = $cart->total();

    $this->assertEquals(
        21.0,
        $total,
        'Total should be the sum of all product costs'
    );
}

Do I Really Have to Do All That?

No.

It's a System

Stick with it until you become fluent, then make it your own.

The more you practice, the more transparent the system becomes.

Like learning to cook a new recipe.

Why Bother With Behavior?

Behavior Driven Test Grow Better

Behavior of an object rarely changes; it's to tied to its name and methods.

Tests driven by behavior have value as the application grows.

New Shopping Cart Requirements

The shopping cart is going international, so simple float values won't fly any more.

Add a library! https://github.com/sebastianbergmann/money

 

<?php
use SebastianBergmann\Money\Money;

class StubProduct implements Product
{
    private $name;
    private $cost;

    public function __construct($name, Money $cost)
    {
        $this->name = $name;
        $this->cost = $cost;
    }

    public function name()
    {
        return $this->name;
    }

    public function cost()
    {
        return $this->cost;
    }
}

 

<?php
use SebastianBergmann\Money\Currency;

class DefaultCart implements Cart
{
    const DEFAULT_CURRENCY = 'USD';

    private $products = array();
    private $currency;

    public function __construct(Currency $currency=null)
    {
        if (!$currency) {
            $currency = new Currency(self::DEFAULT_CURRENCY);
        }

        $this->currency = $currency;
    }

    public function addProduct(Product $prod)
    {
        $this->products[] = $prod;
    }

    public function total()
    {
        $total = new Money(0, $this->currency);
        foreach ($this->products as $product) {
            $total = $total->add($product->cost());
        }

        return $total;
    }
}

All Tests are Broken

But the behavior they verify is still relevant.

New Expectations

 

<?php
class DefaultCartTest
{
    private function assertMoney($object)
    {
        $this->assertInstanceOf(
            'SebastianBergmann\\Money\\Money',
            $object
        );
    }
}

 

<?php
class DefaultCartTest
{
    public function testEmptyCartHasZeroTotal()
    {
        // ...
        $this->assertMoney($total);
        // ...
    }

    public function testTotalShouldBeTheSumOfProductCostsInCart()
    {
        // ...
        $this->assertMoney($total);
        // ...
    }
}

Test Total Against Expected

use SebastianBergmann\Money\Money;
use SebastianBergmann\Money\Currency;

public function testEmptyCartHasZeroTotal()
{
    $currency = new Currency('USD');
    $cart = new DefaultCart($currency);

    $total = $cart->total();

    $this->assertMoney($total);
    $this->assertTrue(
        (new Money(0, $currency))->equals($total),
        'Total should be zero for empty carts'
    );
}

 

use SebastianBergmann\Money\Money;
use SebastianBergmann\Money\Currency;

public function testTotalShouldBeTheSumOfProductCostsInCart()
{
    $currency = new Currency('USD');
    $cart = new DefaultCart($currency);
    $cart->addProduct(new StubProduct(
        'one product',
        new Money(1050, $currency)
    ));
    $cart->addProduct(new StubProduct(
        'two product',
        new Money(1050, $currency)
    ));

    $total = $cart->total();

    $this->assertMoney($total);
    $this->assertTrue(
        (new Money(2100, $currency))->equals($total),
        'Total should be the sum of all product costs'
    );
}

Behavior Still Mattered

The action part of the tests stayed the same.

Nature of the assertions stayed (mostly) the same.

This Guy Really Doesn't Like Mocks

We Haven't Need a Mock Yet

Every collaborator we've used so far has been a value object or another type of test double.

Using Mocks

Using Mocks
Test Doubles

Read These

http://martinfowler.com/articles/mocksArentStubs.html

http://en.wikipedia.org/wiki/Test_double

We've Already Used One Test Double

The product objects we've been using are stubs.

There Several Types of Test Doubles

There Several Types of Test Doubles

Mocks & Spies Verify Behavior

They let you check and define how messages are passed to collaborators.

Mock Example

https://github.com/padraic/mockery

<?php

use SebastianBergmann\Money\Money;
use SebastianBergmann\Money\Currency;

$cart = \Mockery::mock('Chrisguitarguy\\RealUnitTesting\\Cart')
    ->shouldRecieve('total')
    ->once()
    ->andReturn(new Money(100, new Currency('USD')));

// ...
$total = $cart->total();
// ...

// throws if expectations weren't met
\Mockery::close();

Spy Example

https://github.com/phpspec/prophecy — Prophecy is a full-featured test double framework.

<?php
$prophet = new \Prophecy\Prophet();
$cartProph = $prophet->prophesize(
    'Chrisguitarguy\\RealUnitTesting\\Cart'
);
$cart = $cartProph->reveal();

$cart->total();

// throws if the method wasn't called
$cartProph->total()->shouldHaveBeenCalled();

Differences?

Mocks set up expectations before.

Spies verify expectations after.

Accepting Payments

<?php
interface PaymentGateway
{
    public function charge($amount);
}

interface Cart
{
    // ...

    public function pay(PaymentGateway $gateway);
}

class DefaultCart implements Cart
{
    // ...

    public function pay(PaymentGateway $gateway)
    {
        if (!$this->products) {
            throw new EmptyCartException();
        }

        $gateway->charge($this->total());
    }
}

 

interface PaymentGateway
{
    public function charge($amount);
}

 

interface Cart
{
    // ...

    public function pay(PaymentGateway $gateway);
}

Mocking Frameworks are Not Required

They are convenient.

Testing Cart::pay With a Spy

<?php
class PaymentGatewaySpy implements PaymentGateway
{
    use \Counterpart\Assert;

    private $amount = null;

    public function charge($amount)
    {
        $this->amount = $amount;
    }

    public function assertCharged($amount)
    {
        $this->assertEquals($amount, $this->amount);
    }
}

 

public function testPayChargesTheTotalAmountInTheCart()
{
    $gateway = new PaymentGatewaySpy();
    $currency = new Currency('USD');
    $cart = new DefaultCart($currency);
    $cart->addProduct(new StubProduct(
        'one product',
        new Money(1000, $currency)
    ));

    $cart->pay($gateway);

    $gateway->assertCharged(new Money(1000, $currency));
}

Using a Mocking Framework

https://github.com/padraic/mockery

 

<?php
public function testPayChargesTheTotalAmountInTheCart()
{
    $currency = new Currency('USD');
    $gateway = \Mockery::mock(
        'Chrisguitarguy\\RealUnitTesting\\PaymentGateway'
    );
    $gateway->shouldReceive('charge')
        ->once()
        ->with(\Mockery::mustBe(
            new Money(1000, $currency)
        ));
    $cart = new DefaultCart($currency);
    $cart->addProduct(new StubProduct(
        'one product',
        new Money(1000, $currency)
    ));

    $cart->pay($gateway);
}

Differences?

Similarities?

In both, we care that the cart sent a message to the payment gateway — the behavior under test.

Readable Mocking

Use Factory Methods

Especially for mocks you'll be creating a lot.

 

<?php
class DefaultCartTest
{
    // ...

    public function testPayChargesTheTotalAmountInTheCart()
    {
        $gateway = $this->paymentGateway();

        // ...
    }

    private function paymentGateway()
    {
        return \Mockery::mock(
            'Chrisguitarguy\\RealUnitTesting\\PaymentGateway'
        );
    }
}

Hide Expectations

Using the language of your domain.

Do Extract Method on your tests.

 

<?php
public function testPayChargesTheTotalAmountInTheCart()
{
    $currency = new Currency('USD');
    $gateway = $this->paymentGateway();
    $this->gatewayCharges(
        $gateway,
        new Money(1000, $currency)
    );

    // ...
}

 

<?php
private function gatewayCharges(
    $paymentGateway,
    Money $expected
) {
    $paymentGateway->shouldReceive('charge')
        ->once()
        ->with(\Mockery::mustBe($expected));
}

Don't Mock Everything

Why haven't we mocked Money objects?

Money is a Value Object

As long as currency and amount are equal, Money objects are equal.

Don't Mock Value Objects

Just use them.

Only Mock True Dependencies

No need to mock every collaborator.

Do you need to mock that logger?

 

<?php
use Psr\Log\NullLogger;
use Psr\Log\LoggerInterface;
use SebastianBergmann\Money\Money;
use SebastianBergmann\Money\Currency;

class DefaultCart implements Cart
{
    // ...

    private $logger;

    public function __construct(
        Currency $currency=null,
        LoggerInterface $logger=null
    ) {
        // ...
        $this->logger = $logger ?: new NullLogger();
    }

    public function total()
    {
        $total = new Money(0, $this->currency);
        foreach ($this->products as $product) {
            $total = $total->add($product->cost());
        }

        $this->logger->info('Calculated total {total}{cur}', [
            'total' => $total->getAmount(),
            'cur'   => $total->getCurrency(),
        ]);

        return $total;
    }
}

The Logger is Not a Dependency

Has no impact on the object behavior.

It's a notification. Fire and forget.

<?php
$this->logger->info('Calculated total {total}{cur}', [
    'total' => $total->getAmount(),
    'cur'   => $total->getCurrency(),
]);

Don't Mock Non-Dependencies

Just use a dummy or null object.

Doesn't Mean All Loggers Aren't Dependencies

Sometimes objects that are traditionally notifications are valuable to the business

Depends on the situation.

Wrap Up

Write Tests For Behavior

Don't write tests to cover lines.

Name Your Tests Better

Will instantly improve and clarify intent.

Real Unit Testing

Questions?

https://joind.in/11483

/