Over the last decade the idea that we should test our applications has slowly made its way from a niche idea to the mainstream of PHP development. With many tools and approaches to testing now available it can be difficult to choose which ones to use.
In this talk we will explore the current landscape of PHP testing practices, look at the different tools and approaches available, and find out how we can decide which are best for our project, team, and context.
3. 3 Dimensions of Testing
Goal - why we are writing the test
Scope - how much of the system is involved in the test
Form - how we express the test
4.
5. 3 4 Dimensions of Testing
Goal - why we are writing the test
Scope - how much of the system is involved in the test
Form - how we express the test
Time - when we write the test
6. What we will talk about
— Characterisation Tests
— Acceptance Tests
— Integration Tests
— Unit Tests
11. Characterisation Tests
Best practice:
— Treat these tests as a temporary measure
— Use a tool that makes it easy to create tests
— Expect pain and suffering
12. Characterisation Tests
Form: Behat + MinkExtension builtin steps
Scenario: Product search returns results
Given I am on "/"
And I fill in "search" with "Blue jeans"
When I press "go"
Then I should see "100 Results Found"
13. Characterisation Tests
Form: PHPUnit + phpunit-mink-trait
class SearchTest extends PHPUnit_Framework_TestCase
{
use phpunitminkTestCaseTrait;
public function testProductSearchReturnsResult()
{
$page = $this->visit('http://example.com/');
$page->fillField('search', 'Blue Jeans');
$page->pressButton('go');
$this->assertContains('100 Results Found', $page->getText());
}
}
18. Acceptance Tests
Goals:
— Match system behaviour to business requirements
— Get feedback on proposed implementations
— Understand business better
— Document behaviour for the future
19. Acceptance Tests
Scopes:
— At a UI layer
— At an API layer
— At the service layer
— Lower level may be too disconnected
20. Acceptance Tests
Times:
— Before implementation
— Before commitment (as long as it's not expensive?)
— Hard to write in retrospect
21. Acceptance Tests
Best practices:
— Get feedback on tests early
— Make them readable, or have readable output, for the
intended audience
— Apply for the smallest scope first
— Apply to core domain model first
— Minimise end-to-end tests
22. Acceptance Tests
Form: Behat at service level
Scenario: Sales tax is applied to basket
Given "Blue Jeans" are priced as €100 in the catalogue
When I add "Blue Jeans" to my shopping basket
Then the basket total should be €120
23. class BasketContext implements Context
{
public function __construct()
{
$this->catalogue = new InMemoryCatalogue();
$this->basket = new Basket($catalogue);
}
/**
* @Given :productName is/are priced as :cost in the catalogue
*/
public function priceProduct(ProductName $productName, Cost $cost)
{
$this->catalogue->price($productName, $cost);
}
//...
}
24. class BasketContext implements Context
{
//...
/**
* @When I add :productName to my shopping basket
*/
public function addProductToBasket(ProductName $productName)
{
$this->basket->add($productName);
}
/**
* @Then the basket total should be :cost
*/
public function checkBasketTotal(Cost $cost)
{
assert($this->basket->total == $cost->asInt());
}
}
25. Acceptance Tests
Form: PHPUnit at service level
class BasketTest extends PHPUnit_Framework_TestCase
{
public function testSalesTaxIsApplied()
{
$catalogue = new InMemoryCatalogue();
$basket = new Basket($catalogue);
$productName = new ProductName('Blue Jeans');
$catalogue->price($productName, new Cost('100'));
$basket->add($productName);
$this->assertEquals(new Cost('120'), $basket->calculateTotal());
}
}
27. Acceptance Tests
Form: Behat at UI level
Scenario: Sales tax is applied to basket
Given "Blue Jeans" are priced as €100 in the catalogue
When I add "Blue Jeans" to my shopping basket
Then the basket total should be €120
28. class BasketUiContext extends MinkContext
{
public function __construct()
{
$this->catalogue = new Catalogue(/* ... */);
}
/**
* @Given :productName is/are priced as :cost in the catalogue
*/
public function priceProduct(ProductName $productName, Cost $cost)
{
$this->catalogue->price($productName, $cost);
}
//...
}
29. class BasketUiContext extends MinkContext
{
//...
/**
* @When I add :productName to my shopping basket
*/
public function addProductToBasket(ProductName $productName)
{
$this->visitPath('/products/'.urlencode($productName));
$this->getSession()->getPage()->pressButton('Add to Basket');
}
/**
* @Then the basket total should be :cost
*/
public function checkBasketTotal(Cost $cost)
{
$this->assertElementContains('#basket .total', '€120');
}
}
30. Acceptance Tests
Form: PHPUnit at UI level
class BasketUiTest extends PHPUnit_Framework_TestCase
{
use phpunitminkTestCaseTrait;
public function testSalesTaxIsApplied()
{
$catalogue = new Catalogue(/* ... */);
$catalogue->price(new ProductName('Blue Jeans'), new Cost(120));
$page = $this->visit('http://example.com/products/'.urlencode($productName));
$this->getSession()->getPage()->pressButton('Add to Basket');
$this->assertContains(
'€120', $page->find('css', '#basket .total')->getText()
);
}
}
33. Integration Tests
Scopes:
— Large parts of the system
— Focus on the edges (not core domain)
— Areas where your code interacts with third-party
code
34. Integration Tests
Times:
— After the feature is implemented in core / contracts
are established
— During integration with real infrastructure
— When you want to get more confidence in
integration
— When cases are not covered by End-to-End
acceptance test
35. Integration Tests
Best Practices:
— Use tools with existing convenient integrations
— Focus on testing through your API
— Make sure your core domain has an interface
36. Integration Tests
Form: PHPUnit + DbUnit
class CatalogueTest extends PHPUnit_Extensions_Database_TestCase
{
public function getConnection()
{
$pdo = new PDO(/* ... */);
return $this->createDefaultDBConnection($pdo, 'myDatabase');
}
public function getDataSet()
{
return $this->createFlatXMLDataSet(dirname(__FILE__)
. '/_files/catalogue-seed.xml');
}
// ...
}
37. class CatalogueTest extends PHPUnit_Extensions_Database_TestCase
{
// ...
public function testProductCanBePriced()
{
$catalogue = new Catalogue(/* ... */);
$catalogue->price(
new ProductName('Blue Jeans'),
new Cost('100')
);
$this->assertEquals(
new Cost('100'),
$catalogue->lookUp(new ProductName('Blue Jeans')
);
}
}
42. Unit Tests:
Times:
— Just before you implement
OK, maybe...
— Just after you implement
— If you want to learn more about a class
— But always before you share the code!
43. Unit Tests
Best Practices
— Write in a descriptive style
— Describe interactions using Test Doubles
— Don't get too hung up on isolation
— Don't touch infrastructure
— Don't test other people's code
— Don't double other people's code?
44. Unit Tests
Form: PHPUnit
class BasketTest extends PHPUnit_Framework_TestCase
{
public function testSalesTaxIsApplied()
{
$catalogue = $this->getMock(Catalogue::class);
$catalogue->method('lookUp')->with(new ProductName('Blue Jeans'))
->willReturn(new Cost('100'));
$basket = new Basket($catalogue);
$basket->add(new ProductName('Blue Jeans'));
$this->assertSame(new Cost('120'), $basket->calculateTotal());
}
}
45. Unit Tests
Form: PhpSpec
class BasketSpec extends ObjectBehavior
{
function it_applies_sales_tax(Catalogue $catalogue)
{
$catalogue->lookUp(new ProductName('Blue Jeans'))->willReturn(new Cost('100'));
$this->beConstructedWith($catalogue);
$this->add(new ProductName('Blue Jeans'));
$basket->calculateTotal()->shouldBeLike(new Cost('120'));
}
}
46. 5th Dimension -Who?
— Choose the right approaches for your context
— What mix of languages can the team use?
— What styles of testing will add the most value?
— What formats make the most sense to the team?
— How will tests fit into the development process?
There is no right answer, there are many right
answers!
47. Photo Credits
— "tools" by velacreations (CC) - https://flic.kr/p/
8ZSb3r
— "Components" by Jeff Keyzer (CC) - https://flic.kr/p/
4ZNZp1
— Doctor Who stolen from BBC.co.uk
— Other images used under license
48. Thank You & Questions?
@s_bergmann
@ciaranmcnulty
https://joind.in/talk/80dbd