What should you test with your unit tests? Some people will say that unit behaviour is best tested through it's outcomes. But what if communication between units itself is more important than the results of it? This session will introduce you to two different ways of unit-testing and show you a way to assert your object behaviours through their communications.
2. ”
– Reverse Focus on the reverse mortgages
“One of the most common mistakes people
make is to fixate on the goal or expected
outcome while ignoring their underlying
behaviours.”
3. @everzet
• BDD Practice Manager
• Software Engineer
• Creator of Behat, Mink,
Prophecy, PhpSpec2
• Contributor to Symfony2,
Doctrine2, Composer
4. This talk is about
• Test-driven development with and without mocks
• Introducing and making sense of different types of
doubles
• OOP as a messaging paradigm
• Software design as a response to messaging
observations
• Code
6. Money multiplication test from the TDD book
public void testMultiplication()
{
Dollar five = new Dollar(5);
Dollar product = five.times(2);
!
assertEquals(10, product.amount);
!
product = five.times(3);
!
assertEquals(15, product.amount);
}
7. Money multiplication test in PHP
public function testMultiplication()
{
$five = new Dollar(5);
$product = $five->times(2);
$this->assertEquals(10, $product->getAmount());
$product = $five->times(3);
$this->assertEquals(15, $product->getAmount());
}
8. Event dispatching test
public function testEventIsDispatchedDuringRegistration()
{
$dispatcher = new EventDispatcher();
$repository = new UserRepository();
$manager = new UserManager($repository, $dispatcher);
!
$timesDispatched = 0;
$dispatcher->addListener(
'userIsRegistered',
function() use($timesDispatched) {
$timesDispatched += 1;
}
);
!
$user = User::signup('ever.zet@gmail.com');
$manager->registerUser($user);
$this->assertSame(1, $timesDispatched);
}
9. Event dispatching test
public function testEventIsDispatchedDuringRegistration()
{
$dispatcher = new EventDispatcher();
$repository = new UserRepository();
$manager = new UserManager($repository, $dispatcher);
!
$timesDispatched = 0;
$dispatcher->addListener(
'userIsRegistered',
function() use($timesDispatched) {
$timesDispatched += 1;
}
);
!
$user = User::signup('ever.zet@gmail.com');
$manager->registerUser($user);
$this->assertSame(1, $timesDispatched);
}
10.
11.
12. ”
– Ralph Waldo Emerson
“Life is a journey, not a destination.”
18. ”
– Alan Kay, father of OOP
“OOP to me means only messaging, local
retention and protection and hiding of state-
process, and extreme late-binding of all things.”
19. Interfaces
interface LoginMessenger {
public function askForCard();
public function askForPin();
}
interface InputMessenger {
public function askForAccount();
public function askForAmount();
}
interface WithdrawalMessenger {
public function tellNoMoney();
public function tellMachineEmpty();
}
23. 1. Dummy
class System {
private $authorizer;
public function __construct(Authorizer $authorizer) {
$this->authorizer = $authorizer;
}
public function getLoginCount() {
return 0;
}
}
24. 1. Dummy
class System {
private $authorizer;
public function __construct(Authorizer $authorizer) {
$this->authorizer = $authorizer;
}
public function getLoginCount() {
return 0;
}
}
!
public function testNewlyCreatedSystemHasNoLoggedInUsers() {
$auth = $this->prophesize(Authorizer::class);
$system = new System($auth->reveal());
$this->assertSame(0, $system->getLoginCount());
}
25. 1. Dummy
class System {
private $authorizer;
public function __construct(Authorizer $authorizer) {
$this->authorizer = $authorizer;
}
public function getLoginCount() {
return 0;
}
}
!
public function testNewlyCreatedSystemHasNoLoggedInUsers() {
$auth = $this->prophesize(Authorizer::class);
$system = new System($auth->reveal());
$this->assertSame(0, $system->getLoginCount());
}
27. 2. Stub
class System {
// ...
!
public function logIn($username, $password) {
if ($this->authorizer->authorize($username, $password)) {
$this->loginCount++;
}
}
!
public function getLoginCount() {
return $this->loginCount;
}
}
28. 2. Stub
class System {
// ...
!
public function logIn($username, $password) {
if ($this->authorizer->authorize($username, $password)) {
$this->loginCount++;
}
}
!
public function getLoginCount() {
return $this->loginCount;
}
}
!
public function testCountsSuccessfullyAuthorizedLogIns() {
$auth = $this->prophesize(Authorizer::class);
$system = new System($auth->reveal());
!
$auth->authorize('everzet', '123')->willReturn(true);
!
$system->logIn('everzet', ‘123’);
!
$this->assertSame(1, $system->getLoginCount());
}
29. 2. Stub
class System {
// ...
!
public function logIn($username, $password) {
if ($this->authorizer->authorize($username, $password)) {
$this->loginCount++;
}
}
!
public function getLoginCount() {
return $this->loginCount;
}
}
!
public function testCountsSuccessfullyAuthorizedLogIns() {
$auth = $this->prophesize(Authorizer::class);
$system = new System($auth->reveal());
!
$auth->authorize('everzet', '123')->willReturn(true);
!
$system->logIn('everzet', ‘123’);
!
$this->assertSame(1, $system->getLoginCount());
}
30. 2. Stub
class System {
// ...
!
public function logIn($username, $password) {
if ($this->authorizer->authorize($username, $password)) {
$this->loginCount++;
}
}
!
public function getLoginCount() {
return $this->loginCount;
}
}
!
public function testCountsSuccessfullyAuthorizedLogIns() {
$auth = $this->prophesize(Authorizer::class);
$system = new System($auth->reveal());
!
$auth->authorize('everzet', '123')->willReturn(true);
!
$system->logIn(‘_md', ‘321’);
!
$this->assertSame(1, $system->getLoginCount());
}
32. 3. Spy
class System {
// ...
!
public function logIn($username, $password) {
if ($this->authorizer->authorize($username, $password)) {
$this->loginCount++;
$this->lastLoginTimer->recordLogin($username);
}
}
}
33. 3. Spy
class System {
// ...
!
public function logIn($username, $password) {
if ($this->authorizer->authorize($username, $password)) {
$this->loginCount++;
$this->lastLoginTimer->recordLogin($username);
}
}
}
!
public function testCountsSuccessfullyAuthorizedLogIns() {
$auth = $this->prophesize(Authorizer::class);
$timer = $this->prophesize(LoginTimer::class);
$system = new System($auth->reveal(), $timer->reveal());
$auth->authorize('everzet', '123')->willReturn(true);
!
$system->login('everzet', '123');
!
$timer->recordLogin('everzet')->shouldHaveBeenCalled();
}
34. 3. Spy
class System {
// ...
!
public function logIn($username, $password) {
if ($this->authorizer->authorize($username, $password)) {
$this->loginCount++;
$this->lastLoginTimer->recordLogin($username);
}
}
}
!
public function testCountsSuccessfullyAuthorizedLogIns() {
$auth = $this->prophesize(Authorizer::class);
$timer = $this->prophesize(LoginTimer::class);
$system = new System($auth->reveal(), $timer->reveal());
$auth->authorize('everzet', '123')->willReturn(true);
!
$system->login('everzet', '123');
!
$timer->recordLogin('everzet')->shouldHaveBeenCalled();
}
39. Find the message
public function testEventIsDispatchedDuringRegistration()
{
$dispatcher = new EventDispatcher();
$repository = new UserRepository();
$manager = new UserManager($repository, $dispatcher);
!
$timesDispatched = 0;
$dispatcher->addListener(
'userIsRegistered',
function() use($timesDispatched) {
$timesDispatched += 1;
}
);
!
$user = User::signup('ever.zet@gmail.com');
$manager->registerUser($user);
$this->assertSame(1, $timesDispatched);
}
40. Communication over state
public function testEventIsDispatchedDuringRegistration()
{
$repository = $this->prophesize(UserRepository::class);
$dispatcher = $this->prophesize(EventDispatcher::class);
$manager = new UserManager(
$repository->reveal(),
$dispatcher->reveal()
);
$user = User::signup('ever.zet@gmail.com');
$manager->registerUser($user);
!
$dispatcher->dispatch('userIsRegistered', Argument::any())
->shouldHaveBeenCalled();
}
41. Exposed communication
public function testEventIsDispatchedDuringRegistration()
{
$repository = $this->prophesize(UserRepository::class);
$dispatcher = $this->prophesize(EventDispatcher::class);
$manager = new UserManager(
$repository->reveal(),
$dispatcher->reveal()
);
$user = User::signup('ever.zet@gmail.com');
$manager->registerUser($user);
!
$dispatcher->dispatch('userIsRegistered', Argument::any())
->shouldHaveBeenCalled();
}
65. Browser
class Browser {
public function __construct(BrowserDriver $driver) {
$this->driver = $driver;
}
public function goto($url) {
$this->driver->boot();
$this->driver->visit($url);
}
}
66. Browser drivers
interface BrowserDriver {
public function boot();
public function visit($url);
}
!
interface HeadlessBrowserDriver extends BrowserDriver {}
!
class SeleniumDriver implements BrowserDriver {
public function boot() {
$this->selenium->startBrowser($this->browser);
}
!
public function visit($url) {
$this->selenium->visitUrl($url);
}
}
!
class GuzzleDriver implements HeadlessBrowserDriver {
public function boot() {}
!
public function visit($url) {
$this->guzzle->openUrl($url);
}
}
67. Headless driver test
public function testVisitingProvidedUrl() {
$url = 'http://en.wikipedia.org';
$driver = $this->prophesize(HeadlessBrowserDriver::class);
!
$driver->visit($url)->shouldBeCalled();
!
$browser = new Browser($driver->reveal());
$browser->goto($url);
!
$this->getProphecy()->checkPredictions();
}
68. Failing headless driver test
public function testVisitingProvidedUrl() {
$url = 'http://en.wikipedia.org';
$driver = $this->prophesize(HeadlessBrowserDriver::class);
!
$driver->visit($url)->shouldBeCalled();
!
$browser = new Browser($driver->reveal());
$browser->goto($url);
!
$this->getProphecy()->checkPredictions();
}
70. Headless driver implementation
class GuzzleDriver implements HeadlessBrowserDriver {
!
public function boot() {}
!
public function visit($url) {
$this->guzzle->openUrl($url);
}
}
71. Headless driver simple behaviour
class GuzzleDriver implements HeadlessBrowserDriver {
!
public function boot() {}
!
public function visit($url) {
$this->guzzle->openUrl($url);
}
}
72. Headless driver that knows about booting
class GuzzleDriver implements HeadlessBrowserDriver {
!
public function boot() {
$this->allowDoActions = true;
}
public function visit($url) {
if ($this->allowDoActions)
$this->guzzle->openUrl($url);
}
}
74. Adapter layer between BrowserDriver and HeadlessBrowserDriver
interface HeadlessBrowserDriver {
public function visit($url);
}
!
class GuzzleDriver implements HeadlessBrowserDriver {
public function visit($url) {
$this->guzzle->openUrl($url);
}
}
!
final class HeadlessBrowserAdapter implements BrowserDriver {
private $headlessDriver, $allowDoAction = false;
!
public function __construct(HeadlessBrowserDriver $headlessDriver) {
$this->headlessDriver = $headlessDriver;
}
!
public function boot() {
$this->allowDoActions = true;
}
!
public function visit($url) {
if ($this->allowDoActions)
$this->headlessDriver->visit($url);
}
}
75. Dirty adapter layer between BrowserDriver and HeadlessBrowserDriver
interface HeadlessBrowserDriver {
public function visit($url);
}
!
class GuzzleDriver implements HeadlessBrowserDriver {
public function visit($url) {
$this->guzzle->openUrl($url);
}
}
!
final class HeadlessBrowserAdapter implements BrowserDriver {
private $headlessDriver, $allowDoAction = false;
!
public function __construct(HeadlessBrowserDriver $headlessDriver) {
$this->headlessDriver = $headlessDriver;
}
!
public function boot() {
$this->allowDoActions = true;
}
!
public function visit($url) {
if ($this->allowDoActions)
$this->headlessDriver->visit($url);
}
}
76. Single adapter layer between BrowserDriver and HeadlessBrowserDriver
interface HeadlessBrowserDriver {
public function visit($url);
}
!
class GuzzleDriver implements HeadlessBrowserDriver {
public function visit($url) {
$this->guzzle->openUrl($url);
}
}
!
final class HeadlessBrowserAdapter implements BrowserDriver {
private $headlessDriver, $allowDoAction = false;
!
public function __construct(HeadlessBrowserDriver $headlessDriver) {
$this->headlessDriver = $headlessDriver;
}
!
public function boot() {
$this->allowDoActions = true;
}
!
public function visit($url) {
if ($this->allowDoActions)
$this->headlessDriver->visit($url);
}
}
78. ATM messenger interface
interface Messenger {
public function askForCard();
public function askForPin();
public function askForAccount();
public function askForAmount();
public function tellNoMoney();
public function tellMachineEmpty();
}
79. City ATM login test
public function testAtmAsksForCardAndPinDuringLogin() {
$messenger = $this->prophesize(Messenger::class);
!
$messenger->askForCard()->shouldBeCalled();
$messenger->askForPin()->shouldBeCalled();
!
$atm = new CityAtm($messenger->reveal());
$atm->login();
!
$this->getProphet()->checkPredictions();
}
80. City ATM login test
public function testAtmAsksForCardAndPinDuringLogin() {
$messenger = $this->prophesize(Messenger::class);
!
$messenger->askForCard()->shouldBeCalled();
$messenger->askForPin()->shouldBeCalled();
!
$atm = new CityAtm($messenger->reveal());
$atm->login();
!
$this->getProphet()->checkPredictions();
}
82. City ATM login test
public function testAtmAsksForCardAndPinDuringLogin() {
$messenger = $this->prophesize(LoginMessenger::class);
!
$messenger->askForCard()->shouldBeCalled();
$messenger->askForPin()->shouldBeCalled();
!
$atm = new CityAtm($messenger->reveal());
$atm->login();
!
$this->getProphet()->checkPredictions();
}
83. ATM messenger interface(s)
interface LoginMessenger {
public function askForCard();
public function askForPin();
}
!
interface InputMessenger {
public function askForAccount();
public function askForAmount();
}
!
interface WithdrawalMessenger {
public function tellNoMoney();
public function tellMachineEmpty();
}
!
interface Messenger extends LoginMessenger,
InputMessenger,
WithdrawalMessenger
91. Job repository & Doctrine implementation of it
interface
JobRepository
{
public
function
findJobByName($name);
}
!
class
DoctrineJobRepository
extends
EntityRepository
implements
JobRepository
{
!
public
function
findJobByName($name)
{
return
$this-‐>findOneBy(['name'
=>
$name]);
}
}
92. Job repository & Doctrine implementation of it
interface
JobRepository
{
public
function
findJobByName($name);
}
!
class
DoctrineJobRepository
extends
EntityRepository
implements
JobRepository
{
!
public
function
findJobByName($name)
{
return
$this-‐>findOneBy(['name'
=>
$name]);
}
}
93. Job repository & Doctrine implementation of it
interface
JobRepository
{
public
function
findJobByName($name);
}
!
class
DoctrineJobRepository
extends
EntityRepository
implements
JobRepository
{
!
public
function
findJobByName($name)
{
return
$this-‐>findOneBy(['name'
=>
$name]);
}
}
96. Recap:
1. State-focused TDD is not the only way to TDD
2. Messaging is far more important concept of OOP than
the state
97. Recap:
1. State-focused TDD is not the only way to TDD
2. Messaging is far more important concept of OOP than
the state
3. By focusing on messaging, you expose messaging
problems
98. Recap:
1. State-focused TDD is not the only way to TDD
2. Messaging is far more important concept of OOP than
the state
3. By focusing on messaging, you expose messaging
problems
4. By exposing messaging problems, you could discover
most of the SOLID principles violation before they
happen
99. Recap:
1. State-focused TDD is not the only way to TDD
2. Messaging is far more important concept of OOP than the
state
3. By focusing on messaging, you expose messaging
problems
4. By exposing messaging problems, you could discover
most of the SOLID principles violation before they happen
5. Prophecy is awesome