Upload
others
View
4
Download
0
Embed Size (px)
Citation preview
Why, What, and How: Testing in 2014Sebastian Bergmann | June 3rd 2014
Sebastian BergmannDriven by his passion to help developers build better software.
sharing experience
Web Application» Presentation
(Presentation Model, View, Template)
» Application Logic(HTTP Abstraction, Routing, Controller)
» Domain Logic
» Persistence
<?phpuseuse Behat\Mink\Session;useuse Behat\Mink\Driver\GoutteDriver;
abstract classabstract class MinkTestCase extendsextends PHPUnit_Framework_TestCase{
privateprivate $session$session;
protected functionprotected function setUp(){
$this$this->session = newnew Session(newnew GoutteDriver);}
protected functionprotected function visit($url$url){
$this$this->session->visit($url$url);
returnreturn $this$this->session->getPage();}
}
<?phpclassclass ApplicationTest extendsextends MinkTestCase{
public functionpublic function testExamplePageContainsExampleText(){
$page$page = $this$this->visit('http://example.com/');
$this$this->assertContains('Example Domain',$page$page->getContent()
);}
}
$ phpunit ApplicationTestPHPUnit 4.1.1 by Sebastian Bergmann.
.
Time: 1.52 seconds, Memory: 6.75Mb
OK (1 test, 1 assertion)
$ phpunit --testdox ApplicationTestPHPUnit 4.1.1 by Sebastian Bergmann.
Application[x] Example page contains example text
# features/example.featureFeature: Example...
Scenario: Accessing an example web page worksGiven I am on "/"Then I should see "Example Domain"
$ behat features/example.featureFeature: Example...
Scenario: Accessing an example web page worksGiven I am on "/"Then I should see "Example Domain"
1 scenario (1 passed)2 steps (2 passed)0m1.789s
"[T]he Rails community has adopted the strategy of writing hoards of
cucumber tests that drive the application through the web server. This leads
to slow and fragile tests and breaks many of the rules of TDD which tries to
keep tests from coupling to unnecessary system components like the GUI."
— Robert C. Martin
<?phpnamespacenamespace Company\Project;
classclass Application{
public functionpublic function run(RequestInterface $request$request, ResponseInterface $response$response){
// ...}
}
<?phpnamespacenamespace Company\Project;
interfaceinterface ResponseInterface{
public functionpublic function setData($key$key, $value$value);public functionpublic function getData($key$key);public functionpublic function hasData($key$key);
// ...}
<?phpnamespacenamespace Company\Project\Tests;useuse Company\Project\Application;useuse Company\Project\Request;useuse Company\Project\Response;useuse PHPUnit_Framework_TestCase;
classclass HomepageTest extendsextends PHPUnit_Framework_TestCase{
protected functionprotected function setUp(){
$this$this->application = newnew Application;$this$this->request = newnew Request;$this$this->response = newnew Response;
}
public functionpublic function testHasNavigation(){
$this$this->application->run($this$this->request, $this$this->response);
$this$this->assertTrue($this$this->response->hasData('navigation'));}
}
<?phpnamespacenamespace Company\Project\Tests;useuse Company\Project\Application;useuse Company\Project\Request;useuse Company\Project\Response;
classclass HomepageTest extendsextends IntegrationTestCase{
protected functionprotected function setUp(){
$this$this->application = newnew Application;$this$this->request = newnew Request;$this$this->response = newnew Response;
}
public functionpublic function testHasNavigation(){
$this$this->application->run($this$this->request, $this$this->response);
$this$this->assertResponseHasData('navigation', $this$this->response);}
}
<?phpnamespacenamespace Company\Project\Tests;useuse Company\Project\ResponseInterface;useuse PHPUnit_Framework_TestCase;
abstract classabstract class IntegrationTestCase extendsextends PHPUnit_Framework_TestCase{
public functionpublic function assertResponseHasData($name$name, ResponseInterface $response$response){
$constraint$constraint = newnew DataExistsConstraint($response$response);$this$this->assertThat($name$name, $constraint$constraint);
}
public functionpublic function assertResponseNotHasData($name$name, ResponseInterface $response$response){
$constraint$constraint = newnew DataExistsConstraint($response$response);$this$this->assertThat($name$name, $this$this->logicalNot($constraint$constraint));
}}
<?phpnamespacenamespace Company\Project\Tests;useuse Company\Project\ResponseInterface;useuse PHPUnit_Framework_Constraint;
classclass DataExistsConstraint extendsextends PHPUnit_Framework_Constraint{
privateprivate $response$response;
public functionpublic function __construct(ResponseInterface $response$response){
$this$this->response = $response$response;}
protected functionprotected function matches($other$other){
returnreturn $this$this->response->hasData($other$other);}
public functionpublic function toString(){
returnreturn 'data exists';}
}
<?phpnamespacenamespace Company\Project;
classclass SampleWorkflow{
privateprivate $backend$backend;privateprivate $service$service;
public functionpublic function __construct(Backend $backend$backend, Service $service$service){
$this$this->backend = $backend$backend;$this$this->service = $service$service;
}
public functionpublic function execute(Request $request$request){
$this$this->service->doWork($this$this->backend->getObjectById($request$request->getValue('id'))
);}
}
<?phpnamespacenamespace Company\Project;useuse PHPUnit_Framework_TestCase;
classclass SampleWorkflowTest extendsextends PHPUnit_Framework_TestCase{
public functionpublic function testServiceCallUpdatesObject(){
$service$service = $this$this->getMockBuilder('Service')->enableProxyingToOriginalMethods()->getMock();
$service$service->expects($this$this->once())->method('doWork');
$backend$backend = newnew Backend;$workflow$workflow = newnew SampleWorkflow($backend$backend, $service$service);
$workflow$workflow->execute(newnew Request(arrayarray('id' => 2204)));}
}
Domain Logic"Responsible for representing concepts of the business, information about
the business situation, and business rules. State that reflects the business
situation is controlled and used here, even though the technical details of
storing it are delegated to the infrastructure. This layer is the heart of
business software."
— Eric Evans
Domain LogicWhen you cannot test your domain logic, the heart of your software, using
unit tests and in isolation from persistence and the web context ...
TDD is dead. Long live testing."I rarely unit test in the traditional sense of the word, where all
dependencies are mocked out, and thousands of tests can close in seconds.
It just hasn't been a useful way of dealing with the testing of Rails
applications. I test active record models directly, letting them hit the
database, and through the use of fixtures. Then layered on top is currently a
set of controller tests, but I'd much rather replace those with even higher
level system tests through Capybara or similar."
— David Heinemeier Hansson
Monogamous TDD"If you trust those integration tests so much that you are willing to deploy
when they pass; and if they execute so quickly that you can continuously
and effectively refactor and clean the code, then you aren't doing any better
than me. Do it."
— Robert C. Martin
Monogamous TDD"But (and this is a big "but"), it seems to me that integration tests have very
little chance of meeting my two predicates."
— Robert C. Martin
Effective Testing
— Rich Martin
» A high-fidelity test is one which is very sensitive to defects in the codeunder test
» A resilient test is one that only fails when a breaking change is made to thecode under test
» A high-precision test tells you exactly where the defect lies
External and Internal Quality"Running end-to-end tests tells us about the the external quality of our
system, and writing them tells us something about how well we [...]
understand the domain, but end-to-end tests don't tell us how well we've
written the code. Writing unit tests gives us a lot of feedback about the
quality of our code, and running them tell us that we haven't broken any
classes [...]"
— Steve Freeman and Nat Pryce
talks.thePHP.cc
@s_bergmann
sharing experience