building a pyramid: symfony testing strategies

64
Building a Test Pyramid: Symfony testing strategies with Ciaran McNulty

Upload: ciaranmcnulty

Post on 18-Jan-2017

1.067 views

Category:

Technology


0 download

TRANSCRIPT

Page 1: Building a Pyramid: Symfony Testing Strategies

Building a Test Pyramid: Symfony testing strategies

with Ciaran McNulty

Page 2: Building a Pyramid: Symfony Testing Strategies

Before we start:

You must test your applications!

Page 3: Building a Pyramid: Symfony Testing Strategies

What kind of tests to use?» Manual testing

» Acceptance testing

» Unit testing

» Integration testing

» End-to-end testing

» Black box / white box

Page 4: Building a Pyramid: Symfony Testing Strategies

What tools to use?» PHPUnit

» PhpSpec

» Behat

» Codeception

» BrowserKit / Webcrawler

Page 5: Building a Pyramid: Symfony Testing Strategies

Question:

Why can't someone tell me which one to

use?

Page 6: Building a Pyramid: Symfony Testing Strategies

Answer:

Because there is no best answer that fits

all casesYou have to find the

Page 7: Building a Pyramid: Symfony Testing Strategies

Testing different layersIntroducing the pyramid

» Defined by Mike Cohn in Succeeding with Agile

» For understanding different layers of testing

Page 8: Building a Pyramid: Symfony Testing Strategies
Page 9: Building a Pyramid: Symfony Testing Strategies

UI layer tests» Test the whole application end-to-end

» Sensitive to UI changes

» Aligned with acceptance criteria

» Does not require good code

» Probably slow

e.g. Open a browser, fill in the form and submit it

Page 10: Building a Pyramid: Symfony Testing Strategies

Service layer tests» Test the application logic by making service calls

» Faster than UI testing

» Aligned with acceptance criteria

» Mostly written in the target language

» Requires high-level services to exist

e.g. Instantiate the Calculator service and get it to add two numbers

Page 11: Building a Pyramid: Symfony Testing Strategies

Unit level tests» Test individual classes

» Much faster than service level testing

» Very fine level of detail

» Requires good design

Page 12: Building a Pyramid: Symfony Testing Strategies

Why a pyramid?» Each layer builds on the one below it

» Lower layers are faster to run

» Higher levels are slower and more brittle

» Have more tests at the bottom than at the top

Page 13: Building a Pyramid: Symfony Testing Strategies

Why do you want tests?

The answer will affect the type of tests you write

Page 14: Building a Pyramid: Symfony Testing Strategies

If you want existing features from

breaking... write Regression Tests

Page 15: Building a Pyramid: Symfony Testing Strategies

Regression tests» Check that behaviour hasn't changed

» Easiest to apply at the UI level

» ALL tests become regression tests eventually

Page 16: Building a Pyramid: Symfony Testing Strategies

'Legacy' codeclass BasketController extends Controller{ public function addAction(Request $request) { $productId = $request->attributes->get('product_id'); $basket = $request->getSession()->get('basket_context')->getCurrent();

$products = $basket->getProducts(); $products[] = $productId;

$basket->setProducts($products);

return $this->render('::basket.html.twig', ['basket' => $basket]); }}

Page 17: Building a Pyramid: Symfony Testing Strategies

Regression testing with PHPUnit + BrowserKitclass PostControllerTest extends WebTestCase{ public function testShowPost() { $client = static::createClient();

$crawler = $client->request('GET', '/products/1234'); $form = $crawler->selectButton('Add to basket')->form();

$client->submit($form, ['id'=>1234]);

$product = $crawler->filter('html:contains("Product: 1234")');

$this->assertCount(1, $product); }}

Page 18: Building a Pyramid: Symfony Testing Strategies

Regression testing with Codeception$I = new AcceptanceTester($scenario);$I->amOnPage('/products/1234');$I->click('Add to basket');$I->see('Product: 1234');

Page 19: Building a Pyramid: Symfony Testing Strategies

Regression testing with Behat + MinkExtensionScenario: Adding a product to the basket Given I am on "/product/1234" When I click "Add to Basket" Then I should see "Product: 1234"

Page 20: Building a Pyramid: Symfony Testing Strategies

Regression testing with Ghost Inspector

Page 21: Building a Pyramid: Symfony Testing Strategies

When regression testing» Use a tool that gets you coverage quickly and easily

» Plan to phase out regression tests later

» Lean towards testing end-to-end

» Recognise they will be hard to maintain

Page 22: Building a Pyramid: Symfony Testing Strategies

If you want to match customer

requirements better... write Acceptance Tests

Page 23: Building a Pyramid: Symfony Testing Strategies

Acceptance Tests» Check the system does what the customer wants

» Are aligned with customer language and intention

» Write them in English (or another language) first

» Can be tested at the UI or Service level

Page 24: Building a Pyramid: Symfony Testing Strategies

Start with an example-led conversation

... before you start working on it... but not too long before

Page 25: Building a Pyramid: Symfony Testing Strategies

» "What should the system do when X happens?"

» "Does Y always happen when X?"

» "What assumptions Z are causing Y to be the outcome?"

» "Given Z when X then Y"

» "What other things aside from Y might happen?"

» "What if...?"

Page 26: Building a Pyramid: Symfony Testing Strategies

Write the examples out in business-

readable testsTry and make the code look like

the natural conversation you had

Page 27: Building a Pyramid: Symfony Testing Strategies

Easiest to test through the User Interface

Page 28: Building a Pyramid: Symfony Testing Strategies

UI Acceptance testing with PHPUnit + BrowserKitclass PostControllerTest extends WebTestCase{ public function testAddingProductToTheBasket() { $this->addProductToBasket(1234); $this->productShouldBeShownInBasket(1234); }

private function addProductToBasket($productId) { //... browser automation code }

private function productShouldBeShownInBasket($productId) { //... browser automation code }}

Page 29: Building a Pyramid: Symfony Testing Strategies

UI Acceptance testing with Codeception$I = new AcceptanceTester($scenario);

$I->amGoingTo('Add a product to the basket');$I->amOnPage('/products/1234');$I->click('Add to basket');

$I->expectTo('see the product in the basket');$I->see('Product: 1234');

Page 30: Building a Pyramid: Symfony Testing Strategies

UI Acceptance testing with Behat + MinkExtensionScenario: Adding a product to the basket When I add product 1234 to the basket Then I should see product 1234 in the basket

Page 31: Building a Pyramid: Symfony Testing Strategies

UI Acceptance testing with Behat + MinkExtension/** * @When I add product :productId to the basket */public function iAddProduct($productId){ $this->visitUrl('/product/' . $productId); $this->getSession()->clickButton('Add to Basket');}

/** * @Then I should see product :productId in the basket */public function iShouldSeeProduct($productId){ $this->assertSession()->elementContains('css', '#basket', 'Product: ' . $productId);}

Page 32: Building a Pyramid: Symfony Testing Strategies

Acceptance testing through the UI is slow

and brittleTo test at the service layer, we

need to introduce services

Page 33: Building a Pyramid: Symfony Testing Strategies

'Legacy' codeclass BasketController extends Controller{ public function addAction(Request $request) { $productId = $request->attributes->get('product_id'); $basket = $request->getSession()->get('basket_context')->getCurrent();

$products = $basket->getProducts(); $products[] = $productId;

$basket->setProducts($products);

return $this->render('::basket.html.twig', ['basket' => $basket]); }}

Page 34: Building a Pyramid: Symfony Testing Strategies

'Service-oriented' codeclass BasketController extends Controller{ public function addAction(Request $request) { $basket = $this->get('basket_context')->getCurrent(); $productId = $request->attributes->get('product_id');

$basket->addProduct($productId);

return $this->render('::basket.html.twig', ['basket' => $basket]); }}

Page 35: Building a Pyramid: Symfony Testing Strategies

A very small changebut now the business logic is out of

the controller

Page 36: Building a Pyramid: Symfony Testing Strategies

Service layer Acceptance testing with PHPUnitclass PostControllerTest extends PHPUnit_Framework_TestCase{ public function testAddingProductToTheBasket() { $basket = new Basket(new BasketArrayStorage()); $basket->addProduct(1234); $this->assertContains(1234, $basket->getProducts()); }}

Page 37: Building a Pyramid: Symfony Testing Strategies

Service layer acceptance testing with Behat + MinkExtensionScenario: Adding a product to the basket When I add product 1234 to the basket Then I should see product 1234 in the basket

Page 38: Building a Pyramid: Symfony Testing Strategies

Service layer acceptance testing with Behat/** * @When I add product :productId to the basket */public function iAddProduct($productId){ $this->basket = new Basket(new BasketArrayStorage()); $this->basket->addProduct($productId);}

/** * @Then I should see product :productId in the basket */public function iShouldSeeProduct($productId){ assert(in_array($productId, $this->basket->getProducts());}

Page 39: Building a Pyramid: Symfony Testing Strategies

When all of the acceptance tests are running against the

Service layer... how many also need to be run

through the UI?

Page 40: Building a Pyramid: Symfony Testing Strategies

Symfony is a controller for your app

Page 41: Building a Pyramid: Symfony Testing Strategies

If you test everything through services... you only need

enough UI tests to be sure the UI is

Page 42: Building a Pyramid: Symfony Testing Strategies

Multiple Behat suitesScenario: Adding a product to the basket When I add product 1234 to the basket Then I should see product 1234 in the basket

Scenario: Adding a product that is already there Given I have already added product 1234 to the basket When I add product 1234 to the basket Then I should see 2 instances of product 1234 in the basket

@uiScenario: Adding two products to my basket Given I have already added product 4567 to the basket When I add product 1234 to the basket Then I should see product 4567 in the basket And I should also see product 1234 in the basket

Page 43: Building a Pyramid: Symfony Testing Strategies

Multiple Behat suitesdefault: suites: ui: contexts: [ UiContext ] filters: { tags: @ui } service: contexts: [ ServiceContext ]

Page 44: Building a Pyramid: Symfony Testing Strategies

If you want the design of your code to be

better... write Unit Tests

Page 45: Building a Pyramid: Symfony Testing Strategies

Unit Tests» Check that a class does what you expect

» Use a tool that makes it easy to test classes in isolation

» Move towards writing them first

» Unit tests force you to have good design

» Probably too small to reflect acceptance criteria

Page 46: Building a Pyramid: Symfony Testing Strategies

Unit tests are too granularCustomer: "The engine needs to produce 500bhp"Engineer: "What should the diameter of the main drive shaft be?"

Page 47: Building a Pyramid: Symfony Testing Strategies

Unit testing in PHPUnitclass BasketTest extends PHPUnit_Framework_Testcase{ public function testGetsProductsFromStorage() { $storage = $this->getMock('BasketStorage'); $storage->expect($this->once()) ->method('persistProducts') ->with([1234]);

$basket = new Basket($storage);

$basket->addProduct(1234); }}

Page 48: Building a Pyramid: Symfony Testing Strategies

Unit testing in PhpSpecclass BasketSpec extends ObjectBehavior{ function it_gets_products_from_storage(BasketStorage $storage) { $this->beConstructedWith($storage);

$this->addProduct(1234);

$storage->persistProducts([1234])->shouldHaveBeenCalled([1234]); }}

Page 49: Building a Pyramid: Symfony Testing Strategies

Unit test... code that is responsible for

business logic... not code that interacts with

infrastructure including Symfony

Page 50: Building a Pyramid: Symfony Testing Strategies

You can unit test interactions with

Symfony (e.g. controllers)

You shouldn't need to if you have acceptance tests

Page 51: Building a Pyramid: Symfony Testing Strategies

Coupled architecture

Page 52: Building a Pyramid: Symfony Testing Strategies

Unit testing third party dependenciesclass FileHandlerSpec extends ObjectBehaviour{ public function it_uploads_data_to_the_cloud_when_valid( CloudApi $client, FileValidator $validator, File $file ) { $this->beConstructedWith($client, $validator);

$validator->validate($file)->willReturn(true);

$client->startUpload()->shouldBeCalled(); $client->uploadData(Argument::any())->shouldBeCalled(); $client->uploadSuccessful()->willReturn(true);

$this->process($file)->shouldReturn(true); }}

Page 53: Building a Pyramid: Symfony Testing Strategies

Coupled architecture

Page 54: Building a Pyramid: Symfony Testing Strategies

Layered architecture

Page 55: Building a Pyramid: Symfony Testing Strategies

Testing layered architecture

Page 56: Building a Pyramid: Symfony Testing Strategies

class FileHandlerSpec extends ObjectBehaviour{ public function it_uploads_data_to_the_cloud_when_valid( FileStore $filestore, FileValidator $validator, File $file ) { $this->beConstructedWith($filestore, $validator); $validator->validate($file)->willReturn(true);

$this->process($file);

$filestore->store($file)->shouldHaveBeenCalled(); }}

Page 57: Building a Pyramid: Symfony Testing Strategies

Testing layered architecture

Page 58: Building a Pyramid: Symfony Testing Strategies

class CloudFilestoreTest extends PHPUnit_Framework_TestCase{ function testItStoresFiles() { $testCredentials = … $file = new File(…);

$apiClient = new CloudApi($testCredentials); $filestore = new CloudFileStore($apiClient);

$filestore->store($file);

$this->assertTrue($apiClient->fileExists(…)); }}

Page 59: Building a Pyramid: Symfony Testing Strategies

Testing layered architecture

Page 60: Building a Pyramid: Symfony Testing Strategies

To build your pyramid...

Page 61: Building a Pyramid: Symfony Testing Strategies

Have isolated unit-tested objects

representing your core business logic

10,000s of tests running in <10ms each

Page 62: Building a Pyramid: Symfony Testing Strategies

Have acceptance tests at the service level

1,000s of tests running in <100ms each

Page 63: Building a Pyramid: Symfony Testing Strategies

Have the bare minimum of

acceptance tests at the UI level

10s of tests running in <10s each

Page 64: Building a Pyramid: Symfony Testing Strategies

Thank You!Any questions?

https://joind.in/talk/view/14972

@[email protected]