Building a Pyramid: Symfony Testing Strategies

Download Building a Pyramid: Symfony Testing Strategies

Post on 18-Jan-2017

1.045 views

Category:

Technology

0 download

Embed Size (px)

TRANSCRIPT

<ul><li><p>Building a Test Pyramid: Symfony testing strategies</p><p>with Ciaran McNulty</p></li><li><p>Before we start:</p><p>You must test your applications!</p></li><li><p>What kind of tests to use? Manual testing</p><p> Acceptance testing</p><p> Unit testing</p><p> Integration testing</p><p> End-to-end testing</p><p> Black box / white box</p></li><li><p>What tools to use? PHPUnit</p><p> PhpSpec</p><p> Behat</p><p> Codeception</p><p> BrowserKit / Webcrawler</p></li><li><p>Question:</p><p>Why can't someone tell me which one to </p><p>use?</p></li><li><p>Answer:</p><p>Because there is no best answer that fits </p><p>all casesYou have to find the </p></li><li><p>Testing different layersIntroducing the pyramid</p><p> Defined by Mike Cohn in Succeeding with Agile</p><p> For understanding different layers of testing</p></li><li><p>UI layer tests Test the whole application end-to-end</p><p> Sensitive to UI changes</p><p> Aligned with acceptance criteria</p><p> Does not require good code</p><p> Probably slow</p><p>e.g. Open a browser, fill in the form and submit it</p></li><li><p>Service layer tests Test the application logic by making service calls</p><p> Faster than UI testing</p><p> Aligned with acceptance criteria</p><p> Mostly written in the target language</p><p> Requires high-level services to exist</p><p>e.g. Instantiate the Calculator service and get it to add two numbers</p></li><li><p>Unit level tests Test individual classes</p><p> Much faster than service level testing</p><p> Very fine level of detail</p><p> Requires good design</p></li><li><p>Why a pyramid? Each layer builds on the one below it</p><p> Lower layers are faster to run</p><p> Higher levels are slower and more brittle</p><p> Have more tests at the bottom than at the top</p></li><li><p>Why do you want tests?</p><p>The answer will affect the type of tests you write</p></li><li><p>If you want existing features from </p><p>breaking... write Regression Tests</p></li><li><p>Regression tests Check that behaviour hasn't changed</p><p> Easiest to apply at the UI level</p><p> ALL tests become regression tests eventually</p></li><li><p>'Legacy' codeclass BasketController extends Controller{ public function addAction(Request $request) { $productId = $request-&gt;attributes-&gt;get('product_id'); $basket = $request-&gt;getSession()-&gt;get('basket_context')-&gt;getCurrent();</p><p> $products = $basket-&gt;getProducts(); $products[] = $productId;</p><p> $basket-&gt;setProducts($products);</p><p> return $this-&gt;render('::basket.html.twig', ['basket' =&gt; $basket]); }}</p></li><li><p>Regression testing with PHPUnit + BrowserKitclass PostControllerTest extends WebTestCase{ public function testShowPost() { $client = static::createClient();</p><p> $crawler = $client-&gt;request('GET', '/products/1234'); $form = $crawler-&gt;selectButton('Add to basket')-&gt;form();</p><p> $client-&gt;submit($form, ['id'=&gt;1234]);</p><p> $product = $crawler-&gt;filter('html:contains("Product: 1234")');</p><p> $this-&gt;assertCount(1, $product); }}</p></li><li><p>Regression testing with Codeception$I = new AcceptanceTester($scenario);$I-&gt;amOnPage('/products/1234');$I-&gt;click('Add to basket');$I-&gt;see('Product: 1234');</p></li><li><p>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"</p></li><li><p>Regression testing with Ghost Inspector</p></li><li><p>When regression testing Use a tool that gets you coverage quickly and easily</p><p> Plan to phase out regression tests later</p><p> Lean towards testing end-to-end</p><p> Recognise they will be hard to maintain</p></li><li><p>If you want to match customer </p><p>requirements better... write Acceptance Tests</p></li><li><p>Acceptance Tests Check the system does what the customer wants</p><p> Are aligned with customer language and intention</p><p> Write them in English (or another language) first</p><p> Can be tested at the UI or Service level</p></li><li><p>Start with an example-led conversation</p><p>... before you start working on it... but not too long before</p></li><li><p> "What should the system do when X happens?"</p><p> "Does Y always happen when X?"</p><p> "What assumptions Z are causing Y to be the outcome?"</p><p> "Given Z when X then Y"</p><p> "What other things aside from Y might happen?"</p><p> "What if...?"</p></li><li><p>Write the examples out in business-</p><p>readable testsTry and make the code look like </p><p>the natural conversation you had</p></li><li><p>Easiest to test through the User Interface</p></li><li><p>UI Acceptance testing with PHPUnit + BrowserKitclass PostControllerTest extends WebTestCase{ public function testAddingProductToTheBasket() { $this-&gt;addProductToBasket(1234); $this-&gt;productShouldBeShownInBasket(1234); }</p><p> private function addProductToBasket($productId) { //... browser automation code }</p><p> private function productShouldBeShownInBasket($productId) { //... browser automation code }}</p></li><li><p>UI Acceptance testing with Codeception$I = new AcceptanceTester($scenario);</p><p>$I-&gt;amGoingTo('Add a product to the basket');$I-&gt;amOnPage('/products/1234');$I-&gt;click('Add to basket');</p><p>$I-&gt;expectTo('see the product in the basket');$I-&gt;see('Product: 1234');</p></li><li><p>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</p></li><li><p>UI Acceptance testing with Behat + MinkExtension/** * @When I add product :productId to the basket */public function iAddProduct($productId){ $this-&gt;visitUrl('/product/' . $productId); $this-&gt;getSession()-&gt;clickButton('Add to Basket');}</p><p>/** * @Then I should see product :productId in the basket */public function iShouldSeeProduct($productId){ $this-&gt;assertSession()-&gt;elementContains('css', '#basket', 'Product: ' . $productId);}</p></li><li><p>Acceptance testing through the UI is slow </p><p>and brittleTo test at the service layer, we </p><p>need to introduce services</p></li><li><p>'Legacy' codeclass BasketController extends Controller{ public function addAction(Request $request) { $productId = $request-&gt;attributes-&gt;get('product_id'); $basket = $request-&gt;getSession()-&gt;get('basket_context')-&gt;getCurrent();</p><p> $products = $basket-&gt;getProducts(); $products[] = $productId;</p><p> $basket-&gt;setProducts($products);</p><p> return $this-&gt;render('::basket.html.twig', ['basket' =&gt; $basket]); }}</p></li><li><p>'Service-oriented' codeclass BasketController extends Controller{ public function addAction(Request $request) { $basket = $this-&gt;get('basket_context')-&gt;getCurrent(); $productId = $request-&gt;attributes-&gt;get('product_id');</p><p> $basket-&gt;addProduct($productId);</p><p> return $this-&gt;render('::basket.html.twig', ['basket' =&gt; $basket]); }}</p></li><li><p>A very small changebut now the business logic is out of </p><p>the controller</p></li><li><p>Service layer Acceptance testing with PHPUnitclass PostControllerTest extends PHPUnit_Framework_TestCase{ public function testAddingProductToTheBasket() { $basket = new Basket(new BasketArrayStorage()); $basket-&gt;addProduct(1234); $this-&gt;assertContains(1234, $basket-&gt;getProducts()); }}</p></li><li><p>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</p></li><li><p>Service layer acceptance testing with Behat/** * @When I add product :productId to the basket */public function iAddProduct($productId){ $this-&gt;basket = new Basket(new BasketArrayStorage()); $this-&gt;basket-&gt;addProduct($productId);}</p><p>/** * @Then I should see product :productId in the basket */public function iShouldSeeProduct($productId){ assert(in_array($productId, $this-&gt;basket-&gt;getProducts());}</p></li><li><p>When all of the acceptance tests are running against the </p><p>Service layer... how many also need to be run </p><p>through the UI?</p></li><li><p>Symfony is a controller for your app</p></li><li><p>If you test everything through services... you only need </p><p>enough UI tests to be sure the UI is </p></li><li><p>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</p><p>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</p><p>@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</p></li><li><p>Multiple Behat suitesdefault: suites: ui: contexts: [ UiContext ] filters: { tags: @ui } service: contexts: [ ServiceContext ]</p></li><li><p>If you want the design of your code to be </p><p>better... write Unit Tests</p></li><li><p>Unit Tests Check that a class does what you expect</p><p> Use a tool that makes it easy to test classes in isolation</p><p> Move towards writing them first</p><p> Unit tests force you to have good design</p><p> Probably too small to reflect acceptance criteria</p></li><li><p>Unit tests are too granularCustomer: "The engine needs to produce 500bhp"Engineer: "What should the diameter of the main drive shaft be?"</p></li><li><p>Unit testing in PHPUnitclass BasketTest extends PHPUnit_Framework_Testcase{ public function testGetsProductsFromStorage() { $storage = $this-&gt;getMock('BasketStorage'); $storage-&gt;expect($this-&gt;once()) -&gt;method('persistProducts') -&gt;with([1234]);</p><p> $basket = new Basket($storage);</p><p> $basket-&gt;addProduct(1234); }}</p></li><li><p>Unit testing in PhpSpecclass BasketSpec extends ObjectBehavior{ function it_gets_products_from_storage(BasketStorage $storage) { $this-&gt;beConstructedWith($storage);</p><p> $this-&gt;addProduct(1234);</p><p> $storage-&gt;persistProducts([1234])-&gt;shouldHaveBeenCalled([1234]); }}</p></li><li><p>Unit test... code that is responsible for </p><p>business logic... not code that interacts with </p><p>infrastructure including Symfony</p></li><li><p>You can unit test interactions with </p><p>Symfony (e.g. controllers)</p><p>You shouldn't need to if you have acceptance tests</p></li><li><p>Coupled architecture</p></li><li><p>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-&gt;beConstructedWith($client, $validator);</p><p> $validator-&gt;validate($file)-&gt;willReturn(true);</p><p> $client-&gt;startUpload()-&gt;shouldBeCalled(); $client-&gt;uploadData(Argument::any())-&gt;shouldBeCalled(); $client-&gt;uploadSuccessful()-&gt;willReturn(true);</p><p> $this-&gt;process($file)-&gt;shouldReturn(true); }}</p></li><li><p>Coupled architecture</p></li><li><p>Layered architecture</p></li><li><p>Testing layered architecture</p></li><li><p>class FileHandlerSpec extends ObjectBehaviour{ public function it_uploads_data_to_the_cloud_when_valid( FileStore $filestore, FileValidator $validator, File $file ) { $this-&gt;beConstructedWith($filestore, $validator); $validator-&gt;validate($file)-&gt;willReturn(true);</p><p> $this-&gt;process($file);</p><p> $filestore-&gt;store($file)-&gt;shouldHaveBeenCalled(); }}</p></li><li><p>Testing layered architecture</p></li><li><p>class CloudFilestoreTest extends PHPUnit_Framework_TestCase{ function testItStoresFiles() { $testCredentials = $file = new File();</p><p> $apiClient = new CloudApi($testCredentials); $filestore = new CloudFileStore($apiClient);</p><p> $filestore-&gt;store($file);</p><p> $this-&gt;assertTrue($apiClient-&gt;fileExists()); }}</p></li><li><p>Testing layered architecture</p></li><li><p>To build your pyramid...</p></li><li><p>Have isolated unit-tested objects </p><p>representing your core business logic</p><p>10,000s of tests running in </p></li><li><p>Have acceptance tests at the service level</p><p>1,000s of tests running in </p></li><li><p>Have the bare minimum of </p><p>acceptance tests at the UI level</p><p>10s of tests running in </p></li><li><p>Thank You!Any questions?</p><p>https://joind.in/talk/view/14972</p><p>@ciaranmcnultyciaran@sessiondigital.co.uk</p></li></ul>