Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Lightning fast tests at Dutch PHP Conference

Lightning fast tests at Dutch PHP Conference

One of the benefits of having an automated test suite is the feedback given when code is being changed. As the project grows, the test suite becomes slower and slower every day, until it’s so slow it stops being useful. Tests are disabled, skipped and finally removed.

Huge part of the problem lies in getting the testing pyramid wrong and putting to much effort into wrong type of testing.

Learn how to structure your project to benefit from lightning fast tests. Apply the right amount of testing on appropriate levels, and run your tests in seconds, not hours.

Jakub Zalas

June 25, 2016
Tweet

More Decks by Jakub Zalas

Other Decks in Programming

Transcript

  1. LIGHTNING FAST TESTS
    Jakub Zalas
    https://www.flickr.com/photos/captainkimo/7934118876/
    @jakub_zalas
    @jakzal
    DUTCH PHP CONFERENCE
    25th of JUNE 2016 - RAI - AMSTERDAM

    View Slide

  2. WHAT MAKES TESTS SLOW?
    https://www.flickr.com/photos/davemedia/6158362891/

    View Slide

  3. Selenium
    Web server
    + PHP
    Symfony
    WebDriverTest
    DB API
    MyCode
    Controller
    Service A
    Service B
    GoutteTest
    BrowserKitTest
    ServiceTest
    TYPES OF AUTOMATED TESTS
    ➤ Integration
    ➤ UI
    ➤ Service
    ➤ Acceptance
    ➤ etc
    ➤ Unit

    View Slide

  4. if ($serviceA->isEnabled()) {

    $serviceB->callFoo();

    } else {

    $serviceB->callBar();

    }


    if ($serviceA->has('foo') && $serviceA->has('bar')) {

    $serviceB->callBaz();

    }
    COUNT EXECUTION PATHS
    2
    4
    2 x 4 = 8

    View Slide

  5. EXECUTION PATHS IN AN INTEGRATION TEST
    MyCode
    Controller
    Repository
    Service A
    Service B
    4
    2
    5
    2
    4 x 2 x 5 x 2 = 80

    View Slide

  6. EXECUTION PATHS IN AN INTEGRATION TEST
    MyCode
    Controller
    Repository
    Service A
    Service B
    4
    2
    2
    2
    4 x 2 x 5 x 2 = 80
    Service C
    3
    4 x 2 x 2 x 3 x 2 = 96

    View Slide

  7. FEEDBACK

    View Slide

  8. RELIABILITY

    View Slide

  9. SPEED’S NOT THE ONLY ISSUE
    https://www.flickr.com/photos/hsing/3580898648/
    Unit Integration
    Fast Slow
    Reliable Brittle
    Good feedback Poor feedback
    Grow linearly Grow exponentially
    Force better design Put pressure off the design
    Isolated Can simulate user journeys

    View Slide

  10. TEST PYRAMID
    Unit
    Service
    UI
    Succeeding with Agile: Software Development Using Scrum, Mike Cohn

    View Slide

  11. INVERTED TEST PYRAMID
    Unit
    Service
    UI

    View Slide

  12. TESTING ICE-CREAM CONE
    Unit
    Service
    UI
    Manual tests
    http://watirmelon.com/2012/01/31/introducing-the-software-testing-ice-cream-cone/

    View Slide

  13. UNIT TESTING
    https://www.flickr.com/photos/ghozttramp/15390041831/

    View Slide

  14. IMPLEMENTING A PRICE CALCULATOR
    ➤ A small service to calculate a product price in chosen currency
    ➤ Products are stored in a database
    ➤ Currency converter is provided as a third party API

    View Slide

  15. THE OBVIOUS IMPLEMENTATION
    PriceCalculator
    Guzzle Client
    use Doctrine\ORM\EntityManager;

    use GuzzleHttp\Client;


    final class PriceCalculator

    {

    /**

    * @var EntityManager

    */

    private $em;


    /**

    * @var Client

    */

    private $guzzle;


    public function __construct(EntityManager $em, Client $guzzle)

    {

    $this->em = $em;

    $this->guzzle = $guzzle;

    }


    public function price($productUuid, Currency $targetCurrency)

    {

    }

    }
    Doctrine EntityManager

    View Slide

  16. PriceCalculator
    Guzzle Client
    public function price($productUuid, Currency $targetCurrency)

    {

    $products = $this->em->getRepository(Product::class);

    $product = $products->findOneByUuid($productUuid);


    if (!$product) {

    throw new ProductNotFoundException();

    }


    if ($product->isOutOfStock()) {

    throw new OutOfStockException();

    }


    if ($product->isPriceInCurrency($targetCurrency)) {

    return $product->getPrice();

    }


    $price = $product->getPrice();

    $query = http_build_query([

    'amount' => $price->getAmount(),

    'from_currency' => $price->getCurrencyCode(),

    'to_currency' => $targetCurrency->getCode(),

    ]);

    $response = $this->guzzle->get('/convert?'.$query);


    $price = json_decode($response->getBody());


    return new Price($price->amount, $targetCurrency);

    }
    Doctrine EntityManager
    THE OBVIOUS IMPLEMENTATION

    View Slide

  17. // Dummy

    $dummy = $this->prophesize(Foo::class);


    // Stub

    $stub = $this->prophesize(Foo::class);

    $stub->something(‘abc’)->willReturn('123');


    // Mock

    $mock = $this->prophesize(Foo::class);

    $mock->somethingImportant()->shouldBeCalled();
    TEST DOUBLES
    https://www.flickr.com/photos/loriwright/164567533/

    View Slide

  18. UNIT TESTING THE PRICE CALCULATOR
    PriceCalculator
    class PriceCalculatorTest extends \PHPUnit_Framework_TestCase

    {

    const UUID = 'foo-u-u-i-d';


    public function test_it_converts_price_to_the_target_currency()

    {

    $price = new Price(200, new Currency('GBP'));

    $product = new TestProduct($price);

    $expectedPrice = new Price(277, new Currency('EUR'));


    $em = $this->prophesize(EntityManager::class);
    $productRepository = $this->prophesize(EntityRepository::class);
    $em->getRepository(Product::class)->willReturn($productRepository);

    $productRepository->findOneByUuid(self::UUID)->willReturn($product);


    $guzzle = $this->prophesize(Client::class);
    $guzzle->get(‘/convert?amount=200&from_currency=GBP&to_currency=EUR’);
    ->willReturn($this->createResponse(227));


    $priceCalculator = new PriceCalculator(

    $productRepository->reveal(),

    $guzzle->reveal()

    );

    $price = $priceCalculator->price(self::UUID, new Currency('EUR'));


    $this->assertSame($expectedPrice, $price);

    }

    }
    Guzzle Client
    Doctrine EntityManager
    Don’t mock what
    you don’t own!

    View Slide

  19. View Slide

  20. Doctrine Manager Guzzle Client
    DB HTTP
    Unit tests
    Integration
    tests
    PriceCalculator

    View Slide

  21. UNIT TESTING THE PRICE CALCULATOR
    PriceCalculator
    Guzzle Client
    interface ProductRepository

    {

    /**

    * @param string $uuid

    *

    * @return Product

    *

    * @throws ProductNotFoundException if there is no product
    * for the given uuid

    */

    public function findOneByUuid($uuid);

    }
    ProductRepository

    View Slide

  22. UNIT TESTING THE PRICE CALCULATOR
    PriceCalculator
    CurrencyConverter
    interface CurrencyConverter

    {

    /**

    * @param Price $price

    * @param Currency $targetCurrency

    *

    * @return Currency

    */

    public function convert(Price $price, Currency $targetCurrency);

    }
    ProductRepository

    View Slide

  23. UNIT TESTING THE PRICE CALCULATOR
    PriceCalculator
    CurrencyConverter
    class PriceCalculatorTest extends \PHPUnit_Framework_TestCase

    {

    const UUID = 'foo-u-u-i-d';


    public function test_it_converts_price_to_the_target_currency()

    {

    $price = new Price(200, new Currency('GBP'));

    $product = new TestProduct($price);

    $expectedPrice = new Price(277, new Currency('EUR'));


    $productRepository = $this->prophesize(ProductRepository::class);

    $productRepository->findOneByUuid(self::UUID)

    ->willReturn($product);


    $currencyConverter = $this->prophesize(CurrencyConverter::class);

    $currencyConverter->convert($price, new Currency('EUR'))

    ->willReturn($expectedPrice);


    $priceCalculator = new PriceCalculator(

    $productRepository->reveal(),

    $currencyConverter->reveal()

    );

    $price = $priceCalculator->price(self::UUID, new Currency('EUR'));

    $this->assertSame($expectedPrice, $price);

    }

    }
    ProductRepository

    View Slide

  24. UNIT TESTING THE PRICE CALCULATOR
    PriceCalculator
    CurrencyConverter
    final class PriceCalculator

    {

    /**

    * @var ProductRepository

    */

    private $products;


    /**

    * @var CurrencyConverter

    */

    private $converter;


    public function __construct(

    ProductRepository $products,

    CurrencyConverter $converter

    ) {

    $this->products = $products;

    $this->converter = $converter;

    }


    public function price($productUuid, Currency $targetCurrency)

    {

    $product = $this->products->findOneByUuid($productUuid);


    return $this->converter->convert(

    $product->getPrice(),

    $targetCurrency

    );

    }

    }
    ProductRepository

    View Slide

  25. UNIT TESTING THE PRICE CALCULATOR
    PriceCalculator
    CurrencyConverter
    /**

    * @expectedException OutOfStockException

    */

    public function test_it_throws_an_exception_if_product_is_out_of_stock()
    {

    $product = new OutOfStockProduct(

    new Price(200, new Currency('GBP'))

    );


    $productRepository = $this->prophesize(ProductRepository::class);

    $productRepository->findOneByUuid(self::UUID)

    ->willReturn($product);


    $currencyConverter = $this->prophesize(CurrencyConverter::class);


    $priceCalculator = new PriceCalculator(

    $productRepository->reveal(),

    $currencyConverter->reveal()

    );

    $priceCalculator->price(self::UUID, new Currency('EUR'));

    }
    ProductRepository

    View Slide

  26. UNIT TESTING THE PRICE CALCULATOR
    PriceCalculator
    CurrencyConverter
    final class PriceCalculator

    {

    // ...


    public function price($productUuid, Currency $targetCurrency)

    {

    $product = $this->products->findOneByUuid($productUuid);


    if ($product->isOutOfStock()) {

    throw new OutOfStockException($product);

    }


    return $this->converter->convert(

    $product->getPrice(),

    $targetCurrency

    );

    }

    }
    ProductRepository

    View Slide

  27. Doctrine Manager Guzzle Client
    DB HTTP
    Unit tests
    Integration
    tests
    PriceCalculator

    View Slide

  28. Doctrine Manager Guzzle Client
    DB HTTP
    Unit tests
    Integration
    tests
    PriceCalculator

    View Slide

  29. Doctrine Manager Guzzle Client
    DB HTTP
    Unit tests
    Integration
    tests
    CurrencyConverter
    ProductRepository
    PriceCalculator

    View Slide

  30. Doctrine Manager Guzzle Client
    DB HTTP
    Unit tests
    Integration
    tests
    CurrencyConverter
    ProductRepository
    DoctrineProductRepository GuzzleCurrencyConverter
    PriceCalculator

    View Slide

  31. INTEGRATION TESTING THE REPOSITORY
    PriceCalculator
    CurrencyConverter
    class DoctrineProductRepositoryTest extends \PHPUnit_Framework_TestCase

    {

    protected function setUp()

    {

    // set up the database with fixtures

    }


    public function test_it_returns_a_product_by_its_uuid()

    {

    $repository = new DoctrineProductRepository(

    $this->createEntityManager()

    );


    $product = $repository->findByUuid('known-u-u-i-d');


    $this->assertInstanceOf(Product::class, $product);

    }


    /**

    * @expectedException ProductNotFound

    */

    public function test_it_throws_an_exception_if_product_is_not_found(
    {

    $repository = new DoctrineProductRepository(

    $this->createEntityManager()

    );


    $repository->findByUuid('unknown-u-u-i-d');

    }

    }
    ProductRepository
    DoctrineProductRepository

    View Slide

  32. TWO SIDES OF THE CONTRACT
    ➤ An interface is a contract
    ➤ User of the interface makes assumptions
    about its behaviour in unit tests with test
    doubles
    ➤ The implementor of the interface needs
    to prove it implements the contract
    properly (test double assumptions are
    true)
    ➤ For each expectation in the unit test there
    should be at least one test to verify that
    expectation
    PriceCalculator
    CurrencyConverter
    ProductRepository
    DoctrineProductRepository

    View Slide

  33. Doctrine Manager Guzzle Client
    DB HTTP
    Unit tests
    Integration
    tests
    CurrencyConverter
    ProductRepository
    DoctrineProductRepository GuzzleCurrencyConverter
    PriceCalculator

    View Slide

  34. UNIT TESTING THE CURRENCY CONVERTER
    PriceCalculator
    CurrencyConverter interface AcmeApi

    {

    /**

    * @param int $price

    * @param string $fromCurrency

    * @param string $toCurrency

    *

    * @return int

    */

    public function convert($price, $fromCurrency, $toCurrency);

    }
    ProductRepository
    DoctrineProductRepository AcmeApiCurrencyConverter
    GuzzleCurrencyConverter
    AcmeApi

    View Slide

  35. UNIT TESTING THE CURRENCY CONVERTER
    PriceCalculator
    CurrencyConverter
    class AcmeApiCurrencyConverterTest extends \PHPUnit_Framework_TestCase

    {

    public function test_it_returns_the_original_price_if_no_conversion_
    {

    $price = new Price(200, new Currency('GBP'));

    $targetCurrency = new Currency('GBP');


    $acmeApi = $this->prophesize(AcmeApi::class);


    $converter = new AcmeApiCurrencyConverter($acmeApi);

    $actualPrice = $converter->convert($price, $targetCurrency);


    $this->assertSame($price, $actualPrice);

    }


    public function test_it_calls_the_api_to_convert_the_currency()

    {

    $price = new Price(200, new Currency('GBP'));

    $targetCurrency = new Currency('GBP');


    $acmeApi = $this->prophesize(AcmeApi::class);

    $acmeApi->convert(200, 'GBP', 'EUR')->willReturn(277);


    $converter = new AcmeApiCurrencyConverter($acmeApi);

    $actualPrice = $converter->convert($price, $targetCurrency);


    $this->assertEquals(new Price(277, 'EUR'), $actualPrice);

    }

    }
    ProductRepository
    DoctrineProductRepository AcmeApiCurrencyConverter
    GuzzleCurrencyConverter
    AcmeApi

    View Slide

  36. INTEGRATION TESTING THE CURRENCY CONVERTER
    PriceCalculator
    CurrencyConverter
    use GuzzleHttp\Client;


    class GuzzleAcmeApiTest extends \PHPUnit_Framework_TestCase

    {

    public function test_it_converts_the_price()

    {

    $acmeApi = new GuzzleAcmeApi(new Client());


    $amount = $acmeApi->convert(200, 'GBP', 'EUR');


    $this->assertInternalType('int', $amount);

    }

    }
    ProductRepository
    DoctrineProductRepository AcmeApiCurrencyConverter
    GuzzleCurrencyConverter
    AcmeApi

    View Slide

  37. INTEGRATION TESTING THE CURRENCY CONVERTER
    PriceCalculator
    CurrencyConverter
    use GuzzleHttp\Client;


    final class GuzzleAcmeApi implements AcmeApi

    {

    /**

    * @var Client

    */

    private $guzzle;


    public function __construct(Client $guzzle)

    {

    $this->guzzle = $guzzle;

    }


    public function convert($price, $fromCurrency, $toCurrency)

    {

    $query = http_build_query([

    'amount' => $price,

    'from_currency' => $fromCurrency,

    'to_currency' => $toCurrency,

    ]);

    $response = $this->guzzle->get('/convert?'.$query);


    $price = json_decode($response->getBody());


    return (int) $price->amount;

    }

    }
    ProductRepository
    DoctrineProductRepository AcmeApiCurrencyConverter
    GuzzleCurrencyConverter
    AcmeApi

    View Slide

  38. Doctrine Manager Guzzle Client
    DB HTTP
    Unit tests
    Integration
    tests
    CurrencyConverter
    ProductRepository
    DoctrineProductRepository GuzzleCurrencyConverter
    PriceCalculator

    View Slide

  39. Doctrine Manager Guzzle Client
    DB HTTP
    Unit tests
    Integration
    tests
    CurrencyConverter
    ProductRepository
    DoctrineProductRepository GuzzleCurrencyConverter
    PriceCalculator

    View Slide

  40. Doctrine Manager Guzzle Client
    DB HTTP
    Unit tests
    Integration
    tests
    CurrencyConverter
    ProductRepository
    DoctrineProductRepository GuzzleCurrencyConverter
    PriceCalculator
    AcmeApi

    View Slide

  41. Doctrine Manager Guzzle Client
    DB HTTP
    Unit tests
    Integration
    tests
    CurrencyConverter
    ProductRepository
    DoctrineProductRepository GuzzleCurrencyConverter
    PriceCalculator
    AcmeApiCurrencyConverter
    AcmeApi

    View Slide

  42. HEXAGONAL ARCHITECTURE
    Ports & Adapters
    https://www.flickr.com/photos/ghozttramp/15390041831/
    https://www.flickr.com/photos/[email protected]/2537853794/

    View Slide

  43. PriceCalculator
    CurrencyConverter
    ProductRepository
    AcmeApiCurrencyConverter
    AcmeApi

    View Slide

  44. PriceCalculator
    CurrencyConverter
    ProductRepository
    AcmeApiCurrencyConverter
    AcmeApi
    DoctrineProductRepository
    Doctrine Manager

    View Slide

  45. PriceCalculator
    CurrencyConverter
    ProductRepository
    AcmeApiCurrencyConverter
    AcmeApi
    DoctrineProductRepository
    GuzzleCurrencyConverter
    Doctrine Manager
    Guzzle Client

    View Slide

  46. PriceCalculator
    CurrencyConverter
    ProductRepository
    AcmeApiCurrencyConverter
    AcmeApi
    DoctrineProductRepository
    GuzzleCurrencyConverter
    Doctrine Manager
    Guzzle Client
    InMemoryRepository

    View Slide

  47. PriceCalculator
    CurrencyConverter
    ProductRepository
    AcmeApiCurrencyConverter
    AcmeApi
    DoctrineProductRepository
    GuzzleCurrencyConverter
    Doctrine Manager
    Guzzle Client
    InMemoryRepository
    UI

    View Slide

  48. PriceCalculator
    CurrencyConverter
    ProductRepository
    AcmeApiCurrencyConverter
    AcmeApi
    DoctrineProductRepository
    GuzzleCurrencyConverter
    Doctrine Manager
    Guzzle Client
    InMemoryRepository
    UI
    PriceCalculatorController
    Symfony

    View Slide

  49. PriceCalculator
    CurrencyConverter
    ProductRepository
    AcmeApiCurrencyConverter
    AcmeApi
    DoctrineProductRepository
    GuzzleCurrencyConverter
    Doctrine Manager
    Guzzle Client
    InMemoryRepository
    UI
    PriceCalculatorController
    Symfony
    BehatContext
    Behat

    View Slide

  50. @jakub_zalas @jakzal
    Thank you.
    Rate my talk, please:
    https://joind.in/talk/998fb

    View Slide