Building a Pyramid: Symfony Testing Strategies

Post on 18-Jan-2017

1.052 views 0 download

Transcript of Building a Pyramid: Symfony Testing Strategies

Building a Test Pyramid: Symfony testing strategies

with Ciaran McNulty

Before we start:

You must test your applications!

What kind of tests to use?» Manual testing

» Acceptance testing

» Unit testing

» Integration testing

» End-to-end testing

» Black box / white box

What tools to use?» PHPUnit

» PhpSpec

» Behat

» Codeception

» BrowserKit / Webcrawler

Question:

Why can't someone tell me which one to

use?

Answer:

Because there is no best answer that fits

all casesYou have to find the

Testing different layersIntroducing the pyramid

» Defined by Mike Cohn in Succeeding with Agile

» For understanding different layers of testing

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

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

Unit level tests» Test individual classes

» Much faster than service level testing

» Very fine level of detail

» Requires good design

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

Why do you want tests?

The answer will affect the type of tests you write

If you want existing features from

breaking... write Regression Tests

Regression tests» Check that behaviour hasn't changed

» Easiest to apply at the UI level

» ALL tests become regression tests eventually

'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]); }}

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); }}

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

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"

Regression testing with Ghost Inspector

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

If you want to match customer

requirements better... write Acceptance Tests

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

Start with an example-led conversation

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

» "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...?"

Write the examples out in business-

readable testsTry and make the code look like

the natural conversation you had

Easiest to test through the User Interface

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

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');

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

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);}

Acceptance testing through the UI is slow

and brittleTo test at the service layer, we

need to introduce services

'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]); }}

'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]); }}

A very small changebut now the business logic is out of

the controller

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()); }}

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

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());}

When all of the acceptance tests are running against the

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

through the UI?

Symfony is a controller for your app

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

enough UI tests to be sure the UI is

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

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

If you want the design of your code to be

better... write Unit Tests

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

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

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); }}

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]); }}

Unit test... code that is responsible for

business logic... not code that interacts with

infrastructure including Symfony

You can unit test interactions with

Symfony (e.g. controllers)

You shouldn't need to if you have acceptance tests

Coupled architecture

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); }}

Coupled architecture

Layered architecture

Testing layered architecture

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(); }}

Testing layered architecture

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(…)); }}

Testing layered architecture

To build your pyramid...

Have isolated unit-tested objects

representing your core business logic

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

Have acceptance tests at the service level

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

Have the bare minimum of

acceptance tests at the UI level

10s of tests running in <10s each

Thank You!Any questions?

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

@ciaranmcnultyciaran@sessiondigital.co.uk