Do you TDD or BDD? Why not both? Come learn the "Double Loop" workflow and discover how you can use both Behavior Driven Development and Test Driven Development to write well designed, tested and documented code. Double Loop works for lone engineers, small teams or entire product departments. I'll cover the steps you'll take in the workflow in detail with best practices for behavior testing, integration testing and unit testing.
4. @jessicamauerhan | http://bit.ly/double-loop
Benefits of Test Driven Development
● Guides you to create a
well-abstracted and highly
modular system
● Makes software easier to change,
maintain and understand
● Promotes design patterns,
refactoring, and helps prevent
scope creep
● Complete test coverage
● Prevent and identify regressions
and mistakes
● Executable documentation
Primary Secondary
6. Behaviour-driven development is an “outside-in”
methodology. It starts at the outside by identifying
business outcomes, and then drills down into the
feature set that will achieve those outcomes. Each
feature is captured as a “story”, which defines the scope
of the feature along with its acceptance criteria.
Dan North, “What’s in a Story”
http://dannorth.net/whats-in-a-story
7. It’s the idea that you start by writing human-readable
sentences that describe a feature of your application
and how it should work, and only then implement this
behavior in software.
Behat Documentation
http://docs.behat.org/en/v2.5
8. @jessicamauerhan | http://bit.ly/double-loop
Why Practice BDD?
● Well Designed Code
● Automated Testing
● Implementation
● UI Testing
BDD Is Not About...
● Communication
● Collaboration
● Documentation
● Preventing Regressions
BDD Is About...
9. @jessicamauerhan | http://bit.ly/double-loop
Why Practice
Behavior Driven
Development?
“You can turn an idea for a
requirement into implemented,
tested, production-ready code
simply and effectively, as long as the
requirement is specific enough that
everyone knows what’s going on.”
Dan North, “What’s in a Story”
http://dannorth.net/whats-in-a-story
11. @jessicamauerhan | http://bit.ly/double-loop
Testing Pyramid
Maintenance
Coverage
Fragility
Duration
Cost
Number of Tests
Unit Tests
Behavior Tests
System Tests
(performance)
GUI
Tests
Unit
Behavior
System
GUI
Manual QA
Testing Ice Cream Cone
12. @jessicamauerhan | http://bit.ly/double-loop
The Vicious Cycle of the Testing Ice Cream Cone
Unit
Behavior
System
GUI
Manual QA
Non Testable
Code
More Expensive
Tests
Developers Don't
Run Tests
More Bugs
Developers Don't
Write Unit Tests
13. Add
Behavior
Test
Add
Unit
Test
Run Unit
Test
Add/Edit
Some Code
Refactor
Run All
Unit Tests
Run All
Behavior
Tests
Pass* Fail
Fail
Pass
Run
Behavior
Test Pass*
Fail
Fail Pass
Revert Changes
Jessica Mauerhan | @jessicamauerhan
Double Loop: TDD & BDD Done Right!
Feature
Done?
NoYes
* A test that passes the first time
before code has been written means
the test is inaccurate or poorly
written, not added to the test suite,
or the code/feature already exists.
16. @jessicamauerhan | http://bit.ly/double-loop
Gherkin
● Business Readable, Domain
Specific Language
● Living Documentation / User
Manual
● User Story with Narrative
● Contains at least one
Scenario: Acceptance Criteria
17. @jessicamauerhan | http://bit.ly/double-loop
● Narrative
○ As a [role]
I want [feature]
So that [benefit / business reason]
○ Use “5 Whys” to determine narrative
As an Account Holder
I want to withdraw cash from an ATM
So that I can get money when the bank is closed
Scenario: Account has sufficient funds
Given the account has a balance
And the card is valid
And the machine contains at least the amount of my balance
When I request an amount less than or equal to my balance
Then the ATM should dispense that amount
And the amount should be subtracted from my account balance
And the card should be returned
Scenario: Account has insufficient funds
Given the card is valid
When I request an amount greater than my balance
Then the ATM should not dispense any money
And the ATM should say there are insufficient funds
And my account balance should not change
And the card should be returned
● Feature: Short, Descriptive, Action
● Acceptance Criteria: Scenarios
○ Given: Exact Context
○ When: Action/Event
○ Then: Outcomes
○ And/But: More of the same...
●
Feature: Account Holder Withdraws Cash
18. @jessicamauerhan | http://bit.ly/double-loop
As a user viewing a broadcast page before the broadcast starts
I want to see a countdown timer
So that I can know how long until the broadcast actually starts
Scenario: View Countdown before Broadcast
Given I view the “Future Broadcast” broadcast
Then I should see "Future Broadcast" on the page
And I should see "Future Broadcast Author" on the page
And I should see "This broadcast begins at 6:00 pm EST" on the
page
And I should see a countdown timer
Feature: Display Countdown Timer before Broadcast
Process
● Author describes Feature with
implementation specific
example
● Developer adds fixture data
Issues
● Test only works before 6pm
● What is the intent?
● Confusion for business users
19. @jessicamauerhan | http://bit.ly/double-loop
As a user viewing a broadcast page before the broadcast starts
I want to see a countdown timer
So that I can know how long until the broadcast actually starts
Scenario: View Countdown before Broadcast
Given there is a broadcast scheduled for the future
When I view that broadcast’s page
Then I should see the broadcast title
And I should see the broadcast author’s name
And I should see "This broadcast begins at" followed by the start
time in EST
And I should see a countdown timer
Feature: View Countdown before Broadcast Changes
● Given now explains exact
context
● Test is no longer
time-dependent
● Intent - not implementation
Why?
● Overall understanding
● Communication value
● Help identify poorly written
code
20. @jessicamauerhan | http://bit.ly/double-loop
Feature: Customer Views Product and Services Catalog
As a customer
I want to view the product catalog
So that I can browse products and services
Scenario: Customer views Product Catalog
Given I view the catalog
When I select a state from the list of states
Then I should see the list of products for sale
Scenario: Display Local Services in Product Catalog
Given the company has a regional office in a state
When I view the catalog
And I select that state from the list of states
Then I should see the list of services offered
echo '<h1>Products</h1>';
foreach ($products AS $product) {
echo '<p>' . $product->getName() . '</p>';
}
echo '<h1>Services</h1>';
foreach ($services AS $service) {
echo '<p>' . $service->getName() . '</p>';
}
Scenario: Don’t Display Local Services in Product Catalog For States
With No Regional Office
Given the company does not have a regional office in a
state
When I view the catalog
And I select that state from the list of states
Then I should not see a list of services offered
21. @jessicamauerhan | http://bit.ly/double-loop
Feature: Customer Views Product and Services Catalog
As a customer
I want to view the product catalog
So that I can browse products and services
Scenario: Customer views Product Catalog
Given I view the catalog
When I select a state from the list of states
Then I should see the list of products for sale
Scenario: Display Local Services in Product Catalog
Given the company has a regional office in a state
When I view the catalog
And I select that state from the list of states
Then I should see the list of services offered
Scenario: Don’t Display Local Services in Product Catalog For States
With No Regional Office
Given the company does not have a regional office in a
state
When I view the catalog
And I select that state from the list of states
Then I should not see a list of services offered
echo '<h1>Products</h1>';
foreach ($products AS $product) {
echo '<p>' . $product->getName() . '</p>';
}
if(count($services) > 0) {
echo '<h1>Services</h1>';
foreach ($services AS $service) {
echo '<p>' . $service->getName() . '</p>';
}
}
22. @jessicamauerhan | http://bit.ly/double-loop
Writing Great Features
● Exact Context
● Independent Scenarios &
Features
● Intention, not Implementation
● Defined Narrative
● All Paths Explored
Feature “Smells”
● Time Dependency
● Interdependency
● Multi-Scenario Scenarios
● Missing Scenarios
● Overuse of Variables
● Examples of Behavior
(Implementation, not Intention)
25. @jessicamauerhan | http://bit.ly/double-loop
Scenario: Customer views
Product Catalog
Given I view the catalog
When I select a state from the list
of states
Then I should see the list of
products for sale
Behat Code/**
* @Given I view the catalog
*/
public function iViewTheCatalog()
{
$this->visit('catalog');
}
/**
* @When I select a state from the list of states
*/
public function iSelectAStateFromTheListOfStates()
{
$this->selectOption('states', rand(1, 52));
}
/**
* @Then I should see the list of products for sale
*/
public function iShouldSeeTheListOfProductsForSale()
{
$productsDiv = $this->getSession()
->getPage()
->find('css', '#products');
Assertion::notNull($productsDiv);
}
26. Add
Behavior
Test
Run
Behavior
Test Pass*
@jessicamauerhan | http://bit.ly/double-loop
Double Loop: TDD & BDD Done Right!
* A test that passes the first time
before code has been written means
the test is inaccurate or poorly
written, not added to the test suite,
or the code/feature already exists.
28. Add
Behavior
Test
Add
Unit
Test
Run
Behavior
Test Pass*
Fail
Double Loop: TDD & BDD Done Right!
* A test that passes the first time
before code has been written means
the test is inaccurate or poorly
written, not added to the test suite,
or the code/feature already exists.
@jessicamauerhan | http://bit.ly/double-loop
29. @jessicamauerhan | http://bit.ly/double-loop
Writing Unit Tests
What is a unit?
● Unit: As small as possible
● Design and Describe behavior
○ Setup scenario for behavior
○ Cause behavior to occur
○ Assert result is as expected
● Verbose and Descriptive
Names
30. Add
Behavior
Test
Add
Unit
Test
Run Unit
Test
Run
Behavior
Test Pass*
Fail
Double Loop: TDD & BDD Done Right!
* A test that passes the first time
before code has been written means
the test is inaccurate or poorly
written, not added to the test suite,
or the code/feature already exists.
@jessicamauerhan | http://bit.ly/double-loop
31. @jessicamauerhan | http://bit.ly/double-loop
Executing Unit
Tests
● Ensures test fails
when code is broken
● Ensures test is added
to suite
● Tells you exactly what
to do next
Always execute before writing
production code
32. Add
Behavior
Test
Add
Unit
Test
Run Unit
Test
Add/Edit
Some Code
Fail
Run
Behavior
Test Pass*
Fail
Double Loop: TDD & BDD Done Right!
* A test that passes the first time
before code has been written means
the test is inaccurate or poorly
written, not added to the test suite,
or the code/feature already exists.
@jessicamauerhan | http://bit.ly/double-loop
34. @jessicamauerhan | http://bit.ly/double-loop
Three "C" Types of Unit Tests
Contract
Unit adheres to
the contract
Correctness
Unit works
correctly
Collaboration
Unit talks to
collaborators
correctly
"Integration Testing"
Uses Assertions
Test Doubles: None, or Dummies, Stubs
and Fakes.
Or Expects Exception
Uses
Expectations
Test Doubles:
Mocks & Spies
35. New Class
For User
Registration
Class:
Registration
Service
Unit Tests
Registration Service
Correctness
Third Party
Service API
(Mailer)
Integrated
Test
MailChimp API
Forgot
Password
Weekly
Newsletter
Direct
Message
Exciting
Event
Reminder!
TDD: Integration Testing with Contract & Collaboration Tests
@jessicamauerhan | http://bit.ly/double-loop
36. New Class
For User
Registration
Class:
Registration
Service
Third Party
Service API
(Mailer)
Collaborator:
Mailer
Mailer
Contract
(Interface /
Abstract)
Mailer
Interface
Test Double
Persistence
Layer
Persistence
Layer
(Interface /
Abstract)
Persistence
Layer
Test Double
Fully Unit Tested &
Implemented Class:
Registration
Service
Unit Tests
Registration Service
Correctness
Collaboration
Tests
Collaboration
Tests
TDD: Integration Testing with Contract & Collaboration Tests
@jessicamauerhan | http://bit.ly/double-loop
37. Third Party
Service API
(Mailer)
Mailer
Implementation
Class:
Mailchimp
Adapter
New Class
For User
Registration
Class:
Registration
Service
Collaborator:
Mailer
Mailer
Contract
(Interface /
Abstract)
Mailer
Interface
Test Double
Persistence
Layer
Persistence
Layer
(Interface /
Abstract)
Persistence
Layer
Test Double
Fully Unit Tested &
Implemented Class:
Registration
Service
Unit Tests
Registration Service
Correctness
Collaboration
Tests
Collaboration
Tests
Third Party Tool:
MailChimp API
(This is a Contract)
Mailchimp
API Double
Fully Unit Tested &
Implemented Class:
Mailchimp
Adapter
Correctness
Unit Tests
Contract
Tests
Collaboration
Tests
TDD: Integration Testing with Contract & Collaboration Tests
@jessicamauerhan | http://bit.ly/double-loop
38. Mailer
Implementation
Class:
SendBlue
Adapter
New Class
For User
Registration
Class:
Registration
Service
Third Party
Service API
(Mailer)
Collaborator:
Mailer
Mailer
Contract
(Interface /
Abstract)
Mailer
Interface
Test Double
Persistence
Layer
Persistence
Layer
(Interface /
Abstract)
Persistence
Layer
Test Double
Fully Unit Tested &
Implemented Class:
Registration
Service
Unit Tests
Registration Service
Correctness
Collaboration
Tests
Collaboration
Tests
Third Party Tool:
SendBlue
(This is a Contract)
Third Party Tool:
SendBlue
(This is a Contract)
SendBlue
API Double
Fully Unit Tested &
Implemented Class:
SendBlue
Adapter
Class:
SendBlue
Adapter
Correctness
Unit Tests
Contract
Tests
Collaboration
Tests
TDD: Integration Testing with Contract & Collaboration Tests
Forgot
Password
Weekly
Newsletter
Direct
Message
Exciting
Event
Reminder!
@jessicamauerhan | http://bit.ly/double-loop
40. @jessicamauerhan | http://bit.ly/double-loop
PdoUserRepositoryTest.php
//Collaboration
public function testEmailExistsPreparesStatement()
{
$sql = "SELECT count(id) FROM users ".
"WHERE email = :email";
$stmt = $this->createMock(PDOStatement::class);
$pdo = this->createMock(PDO::class);
$pdo->expects($this->once())->method('prepare')
->with($sql)->willReturn($stmt);
$repo = new PdoUserRepository($pdo);
$repo->emailExists('test@gmail.com');
}
PdoUserRepository.php
public function emailExists(string $email): bool
{
$sql = "SELECT count(id) FROM users ".
"WHERE email = :email";
$statement = $this->pdo->prepare($sql);
}
41. @jessicamauerhan | http://bit.ly/double-loop
PdoUserRepositoryTest.php
//Collaboration
public function testEmailExistsExecutesStatement()
{
$email = 'jdoe@gmail.com';
$stmt = $this->createMock(PDOStatement::class);
$stmt>expects($this->once())
->method('execute')
->with(['email'=>$email]);
$pdo = this->createMock(PDO::class);
$pdo->method('prepare')->willReturn($statement);
$repo = new PdoUserRepository($pdo);
$repo->emailExists($email);
}
PdoUserRepository.php
public function emailExists(string $email): bool
{
$sql = "SELECT count(id) FROM users ".
"WHERE email = :email";
$statement = $this->pdo->prepare($sql);
}
public function emailExists(string $email): bool
{
$sql = "SELECT count(id) FROM users ".
"WHERE email = :email";
$statement = $this->pdo->prepare($sql);
$statement->execute(['email' => $email]);
}
42. @jessicamauerhan | http://bit.ly/double-loop
PdoUserRepositoryTest.php
//Correctness
public function
testEmailExistsReturnsTrueWhenCountGreaterThanZero()
{
$stmt = $this->createMock(PDOStatement::class);
$stmt->method('fetchColumn')
->willReturn(1);
$pdo = this->createMock(PDO::class);
$pdo->method('prepare')
->willReturn($statement);
$repo = new PdoUserRepository($pdo);
$this->assertTrue(
$repo->emailExists('test@gmail.com')
);
}
PdoUserRepository.php
public function emailExists(string $email): bool
{
$sql = "SELECT count(id) FROM users ".
"WHERE email = :email";
$statement = $this->pdo->prepare($sql);
$statement->execute(['email' => $email]);
}
public function emailExists(string $email): bool
{
$sql = "SELECT count(id) FROM users ".
"WHERE email = :email";
$statement = $this->pdo->prepare($sql);
$statement->execute(['email' => $email]);
$result = $statement->fetchColumn();
return $result > 0;
}
43. @jessicamauerhan | http://bit.ly/double-loop
PdoUserRepositoryTest.php
//Correctness
public function
testEmailExistsReturnsFalseWhenCountZero()
{
$stmt = $this->createMock(PDOStatement::class);
$stmt->method('fetchColumn')
->willReturn(0);
$pdo = this->createMock(PDO::class);
$pdo->method('prepare')
->willReturn($statement);
$repo = new PdoUserRepository($pdo);
$this->assertFalse(
$repo->emailExists('test@gmail.com')
);
}
PdoUserRepository.php
public function emailExists(string $email): bool
{
$sql = "SELECT count(id) FROM users ".
"WHERE email = :email";
$statement = $this->pdo->prepare($sql);
$statement->execute(['email' => $email]);
$result = $statement->fetchColumn();
return $result > 0;
}
44. @jessicamauerhan | http://bit.ly/double-loop
PdoUserRepositoryTest.php
//Contract
public function
testEmailExistsThrowsUnableToCheckEmailWhenPDOExcept
ion()
{
$this->expectException(UnableToCheckEmail::class);
$pdoException = new PdoException();
$pdo = this->createMock(PDO::class);
$pdo->method('prepare')
->willThrow($pdoException);
$repo = new PdoUserRepository($pdo);
$repo->emailExists('test@gmail.com');
}
PdoUserRepository.php
public function emailExists(string $email): bool
{
$sql = "SELECT count(id) FROM users ".
"WHERE email = :email";
$statement = $this->pdo->prepare($sql);
$statement->execute(['email' => $email]);
$result = $statement->fetchColumn();
return $result > 0;
}
public function emailExists(string $email): bool
{
$sql = "SELECT count(id) FROM users ".
"WHERE email = :email";
try {
$statement = $this->pdo->prepare($sql);
$statement->execute(['email' => $email]);
$result = $statement->fetchColumn();
return $result > 0;
} catch (PDOException $PDOException) {
throw new UnableToCheckEmail();
}
}
45. Maintenance
Coverage
Fragility
Duration
Cost
Number of Tests
Isolated Unit Tests
Correctness, Contract & Collaboration
Integration Testing
Multiple Components Integrated
Tested for Correctness & Communication
Behavior Tests
Fully Integrated User Experience
System Tests
load balance, performance, security
GUI
Tests
46. Add
Behavior
Test
Add
Unit
Test
Run Unit
Test
Add/Edit
Some Code
Fail
Run
Behavior
Test Pass*
Fail
Double Loop: TDD & BDD Done Right!
* A test that passes the first time
before code has been written means
the test is inaccurate or poorly
written, not added to the test suite,
or the code/feature already exists.
@jessicamauerhan | http://bit.ly/double-loop
48. Add
Behavior
Test
Add
Unit
Test
Run Unit
Test
Add/Edit
Some Code
Refactor
Run All
Unit Tests
Pass* Fail
Fail
Pass
Run
Behavior
Test Pass*
Fail
Revert Changes
Double Loop: TDD & BDD Done Right!
Feature
Done?
NoYes
* A test that passes the first time
before code has been written means
the test is inaccurate or poorly
written, not added to the test suite,
or the code/feature already exists.
@jessicamauerhan | http://bit.ly/double-loop
50. @jessicamauerhan | http://bit.ly/double-loop
Refactoring
● Improving
non-functional aspects
● Removing duplication
● Renaming
● Simplifying
Unit Tests Always Passing!
● Changes that would
break a unit test
● New functionality that
is not tested
New Unit Tests Fail, Then Pass
Adding Functionality
51. Add
Behavior
Test
Add
Unit
Test
Run Unit
Test
Add/Edit
Some Code
Refactor
Run All
Unit Tests
Run All
Behavior
Tests
Pass* Fail
Fail
Pass
Run
Behavior
Test Pass*
Fail
Fail Pass
Revert Changes
Double Loop: TDD & BDD Done Right!
Feature
Done?
NoYes
* A test that passes the first time
before code has been written means
the test is inaccurate or poorly
written, not added to the test suite,
or the code/feature already exists.
@jessicamauerhan | http://bit.ly/double-loop