Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

FEEDBACK

Slide 8

Slide 8 text

RELIABILITY

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

INVERTED TEST PYRAMID Unit Service UI

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

// 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/

Slide 18

Slide 18 text

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!

Slide 19

Slide 19 text

No content

Slide 20

Slide 20 text

Doctrine Manager Guzzle Client DB HTTP Unit tests Integration tests PriceCalculator

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

Doctrine Manager Guzzle Client DB HTTP Unit tests Integration tests PriceCalculator

Slide 28

Slide 28 text

Doctrine Manager Guzzle Client DB HTTP Unit tests Integration tests PriceCalculator

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

HEXAGONAL ARCHITECTURE Ports & Adapters https://www.flickr.com/photos/ghozttramp/15390041831/ https://www.flickr.com/photos/25689440@N06/2537853794/

Slide 43

Slide 43 text

PriceCalculator CurrencyConverter ProductRepository AcmeApiCurrencyConverter AcmeApi

Slide 44

Slide 44 text

PriceCalculator CurrencyConverter ProductRepository AcmeApiCurrencyConverter AcmeApi DoctrineProductRepository Doctrine Manager

Slide 45

Slide 45 text

PriceCalculator CurrencyConverter ProductRepository AcmeApiCurrencyConverter AcmeApi DoctrineProductRepository GuzzleCurrencyConverter Doctrine Manager Guzzle Client

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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