The last few years have seen a huge adoption of testing practices, and an explosion of different testing tools, in the PHP space. The difficulties come when we have to choose which tools to use, in what combinations, and how to apply them to existing codebases.
In this talk we will look at what tools are available, what their strengths are, how to decide which set of tools to use for new or legacy projects, and when to prioritise decoupling and testability over the convenience we get from our frameworks.
9. 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
10. 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
11. Unit level tests
» Test individual classes
» Much faster than service level testing
» Very fine level of detail
» Requires good design
12. 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
13. Why do you want
tests?
The answer will affect the type of
tests you write
14. If you want existing
features from
breaking
... write Regression Tests
15. Regression tests
» Check that behaviour hasn't changed
» Easiest to apply at the UI level
» ALL tests become regression tests eventually
17. Regression testing with PHPUnit
+ BrowserKit
class 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);
}
}
18. Regression testing with
Codeception
$I = new AcceptanceTester($scenario);
$I->amOnPage('/products/1234');
$I->click('Add to basket');
$I->see('Product: 1234');
19. Regression testing with Behat +
MinkExtension
Scenario: Adding a product to the basket
Given I am on "/product/1234"
When I click "Add to Basket"
Then I should see "Product: 1234"
21. 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
22. If you want to match
customer
requirements better
... write Acceptance Tests
23. 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
25. » "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...?"
26. Write the examples
out in business-
readable tests
Try and make the code look like
the natural conversation you had
28. UI Acceptance testing with
PHPUnit + BrowserKit
class 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
}
}
29. 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');
30. UI Acceptance testing with Behat
+ MinkExtension
Scenario: Adding a product to the basket
When I add product 1234 to the basket
Then I should see product 1234 in the basket
31. 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);
}
34. 'Service-oriented' code
class 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]);
}
}
35. A very small change
but now the business logic is out of
the controller
36. Service layer Acceptance testing
with PHPUnit
class PostControllerTest extends PHPUnit_Framework_TestCase
{
public function testAddingProductToTheBasket()
{
$basket = new Basket(new BasketArrayStorage());
$basket->addProduct(1234);
$this->assertContains(1234, $basket->getProducts());
}
}
37. Service layer acceptance testing
with Behat + MinkExtension
Scenario: Adding a product to the basket
When I add product 1234 to the basket
Then I should see product 1234 in the basket
38. 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());
}
39. When all of the
acceptance tests are
running against the
Service layer
... how many also need to be run
through the UI?
41. If you test everything
through services
... you only need
enough UI tests to be
sure the UI is
42. Multiple Behat suites
Scenario: 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
@ui
Scenario: 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
44. If you want the design
of your code to be
better
... write Unit Tests
45. 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
46. Unit tests are too granular
Customer: "The engine needs to produce 500bhp"
Engineer: "What should the diameter of the main drive shaft
be?"
47. Unit testing in PHPUnit
class 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);
}
}
48. Unit testing in PhpSpec
class BasketSpec extends ObjectBehavior
{
function it_gets_products_from_storage(BasketStorage $storage)
{
$this->beConstructedWith($storage);
$this->addProduct(1234);
$storage->persistProducts([1234])->shouldHaveBeenCalled([1234]);
}
}
49. Unit test
... code that is responsible for
business logic
... not code that interacts with
infrastructure including Symfony
50. You can unit test
interactions with
Symfony (e.g.
controllers)
You shouldn't need to if you have
acceptance tests