Mocks Aren't Stubs, Fakes, Dummies or Spies - SymfonyLive London 2014

B423daa9c89538f919aec9f86f767821?s=47 Dave Marshall
September 26, 2014

Mocks Aren't Stubs, Fakes, Dummies or Spies - SymfonyLive London 2014

If you're familiar with testing your PHP code, there's a good chance you've been using mock objects: But are they really mocks? The term mock object is commonly used in the PHP community to describe both Mocks and Stubs, but they do behave differently, and more importantly, they should be used differently.

Test double is used as the general name for objects, procedures or systems used to replace real components, purely for testing purposes. As the title of the talk implies, mock objects aren't the only kind of test doubles out there.

This talk will cover the basic differences between the test doubles you might use and when or what you might utilise them for, including practical examples using several of the most popular PHP test double libraries. We'll then go into a little bit more detail on how the way you approach the design of your software can lead to the use of the different types and the trade-offs between those approaches.

B423daa9c89538f919aec9f86f767821?s=128

Dave Marshall

September 26, 2014
Tweet

Transcript

  1. Mocks Aren't Stubs, Fakes, Dummies or Spies

  2. Mocks Aren't Stubs, Fakes, Dummies or Spies » @davedevelopment »

    @childcare » @thatpodcast
  3. Mocks aren't Stubs “But as often as not I see

    mock objects described poorly.” -- Martin Fowler http://martinfowler.com/articles/ mocksArentStubs.html
  4. Mocks Aren't Stubs, Fakes, Dummies or Spies » Terminology »

    Types of Test Doubles » Testing Behaviour or State » (Bigger) Examples
  5. Terminology -- http://xkcd.com/503/

  6. Terminology

  7. Recommended Reading

  8. Mockito “Mockito is a mocking framework that tastes really good.”

    -- https://code.google.com/p/mockito/
  9. Mockito: FAQ “Is it really a mocking framework?” “There is

    a bit of confusion around the vocabulary. Technically speaking Mockito is a Test Spy framework.” -- https://code.google.com/p/mockito/ wiki/FAQ
  10. Mockito: API » mock() method creates spies List spy =

    mock(List::class); spy.add('one'); verify(spy).add('one'); » spy() method creates real spies List list = new LinkedList(); List spy = spy(list); spy.add('one'); verify(spy).add('one');
  11. System under test (SUT) The thing we are testing $sut

    = new UserService();
  12. Depended-on component (DOC) Something the SUT needs to use $doc

    = new UserRepository(); $sut = new UserService($doc);
  13. Indirect Output Something the SUT sends to one of it's

    DOCs class UserService { public function post(User $user) { $this->repo->add($user); return; } }
  14. Indirect Input Something the SUT receives from one of it's

    DOCs class UserService { public function getUser($id) { return $this->repo->find($id); } }
  15. Test Double A replacement for a real DOC, used to

    enable, ease or improve testing of the SUT $double = new NullUserRepository(); $sut = new UserService($double);
  16. Creating Test Doubles

  17. Hand built

  18. Generated

  19. Generated Doubles: Tools » phpunit/phpunit-mock-objects » phpspec/prophecy » mockery/mockery »

    phake/phake » codeception/aspect-mock » php-vcr/php-vcr » many others...
  20. Configurable

  21. Test Doubles » Dummy » Fake » Stub » Mock

    » Spy
  22. Dummies

  23. Dummies Used whenever we need to pass arguments to a

    constructor or method, where those arguments should not be used while exercising the SUT
  24. Dummies: Values # SUT BCryptPasswordEncoder public function encodePassword($raw, $salt) {

    if ($this->isPasswordTooLong($raw)) { throw new BadCredentialsException( 'Invalid password.' ); } # ... }
  25. Dummies: Values /** @test */ public function it_refuses_a_long_password() { $encoder

    = new BCryptPasswordEncoder(31); $this->setExpectedException( "BadCredentialsException" ); $encoder->encodePassword( str_repeat("a", 4097), "dummy salt that will not be used" ); }
  26. Dummies: Objects # SUT AuthenticationProvider public function __construct(EncoderInterface $encoder) {

    $this->encoder = $encoder; } public function checkAuthentication(User $user, $presentedPassword) { if ("" === $presentedPassword) { throw new BadCredentialsException( 'The presented password cannot be empty. '); } # do something with the encoder }
  27. Dummies: Objects by hand class DummyEncoder implements EncoderInterface { public

    function isPasswordValid($hashedPassword, $plainTextPassword, $salt) { throw new RuntimeException("Not implemented"); } }
  28. Dummies: Objects by hand /** @test */ public function it_throws_on_an_empty_password()

    { $authProvider = new AuthenticationProvider( new DummyEncoder() ); $this->setExpectedException("BadCredentialsException"); $authProvider->checkAuthentication(new User(), ""); }
  29. Dummies: Objects with phpunit /** @test */ public function it_throws_on_an_empty_password()

    { $authProvider = new AuthenticationProvider( $this->getMock("EncoderInterface") ); $this->setExpectedException("BadCredentialsException"); $authProvider->checkAuthentication(new User(), ""); }
  30. Dummies: Objects with mockery /** @test */ public function it_throws_immediately_on_an_empty_password()

    { $authProvider = new AuthenticationProvider( Mockery::mock("EncoderInterface") ); $this->setExpectedException("BadCredentialsException"); $authProvider->checkAuthentication(new User(), ""); }
  31. Fakes

  32. Fakes "Replace a component that the system under test (SUT)

    depends on with a much lighter-weight implementation." -- http://xunitpatterns.com We use a Fake whenever our SUT has a DOC that is unavailable, slow or simply makes testing difficult.
  33. Fakes: In-Memory Database # app/config/config_test.yml doctrine: dbal: driver: pdo_sqlite path:

    :memory: memory: true orm: auto_generate_proxy_classes: true auto_mapping: true
  34. Fakes: Fake Object class PlaintextPasswordEncoder extends BasePasswordEncoder { public function

    encodePassword($raw, $salt) { if ($this->isPasswordTooLong($raw)) { throw new BadCredentialsException( 'Invalid password.' ); } return $this->mergePasswordAndSalt($raw, $salt); } }
  35. Fakes: Fake Object /** @test */ public function it_validates_a_password() {

    $encoder = new PlaintextPasswordEncoder(); $authProvider = new AuthenticationProvider($encoder); $user = new User([ 'password' => 'pass{salt}', 'salt' => 'salt' ]); $this->assertNull( $authProvider->checkAuthentication($user, "pass") ); }
  36. Fakes: Fake Object # config_test.yml framework: test: ~ session: storage_id:

    session.storage.mock_file profiler: collect: false
  37. Fakes: Fake Web Service Web services are often: » Network

    dependant » Hard to control
  38. Fakes: Fake Web Service davedevelopment/fake-recurly @app.route("/subscriptions/<uuid>", methods=["GET"]) def get_subscription(uuid): sub

    = find_subscription_or_404(uuid) plan = Plan.find(sub.planCode) account = Account.find(sub.accountCode) return render_template("subscription.xml", subscription=sub, account=account, plan=plan), 200
  39. Fakes: Using a test double framework? // Create a stub

    for the SomeClass class. $stub = $this->getMock('SomeClass'); // Configure the stub. $stub->method('doSomething') ->will($this->returnCallback('str_rot13'));
  40. Fakes » Usually hand coded » Low coupling between SUT

    and test » May need to expose state for verification » Can get complicated to maintain
  41. Stubs

  42. Stubs Stubs are used to control the indirect inputs to

    the SUT, by providing canned responses to calls made during the test
  43. Stubs: Stub Object /** @test */ function it_gets_a_user() { $employee

    = new Employee(); $repo = $this->getMock('EmployeeRepository'); $repo->method('find') ->with($id = 123); ->willReturn($employee); $sut = new UserService($repo); $this->assertEquals( $employee, $sut->get($id) ); }
  44. Stubs: HTTP calls php-vcr/php-vcr public function it_intercepts_http_calls() { VCR::turnOn(); VCR::insertCassette('example');

    $result = file_get_contents('http://example.com'); $this->assertContains('Example Domain', $result); VCR::eject(); VCR::turnOff(); } First run: 522 ms Subsequent runs: 215 ms
  45. Mocks

  46. Mocks Mocks are used to verify the indirect outputs of

    the SUT
  47. Mocks: Example /** @test */ function it_persists_a_user() { $employee =

    new Employee(); $repo = $this->prophesize('EmployeeRepository'); $repo->add($employee)->shouldBeCalled(); $sut = new UserService($repo->reveal()); $sut->post($employee); $this->getProphet()->checkPredictions(); }
  48. Mocks: Example /** @test */ function it_persists_a_user() { $employee =

    new Employee(); $repo = $this->prophesize('EmployeeRepository'); $repo->add($employee)->shouldBeCalled(); $sut = new UserService($repo->reveal()); $sut->post($employee); }
  49. Mocks » Verify behaviour of the SUT by requiring you

    to provide details of any interaction they should expect » Can act as stubs, returning values » Can lead to overspecification, leading to brittle tests
  50. Mocks: When to use them » When you need to

    verify the behaviour of the SUT, because it's not easy to do so via the final state of the SUT » When you're working outside-in, discovering new interfaces as you develop the SUT
  51. Spies

  52. Spies Spies are used to observe the indirect outputs of

    the SUT
  53. Spies: Example /** @test */ function it_persists_a_user() { $employee =

    new Employee(); $repo = $this->prophesize('EmployeeRepository'); $repo->add($employee)->shouldBeCalled(); $sut = new UserService($repo->reveal()); $sut->post($employee); }
  54. Spies: Example /** @test */ function it_persists_a_user() { $employee =

    new Employee(); $repo = $this->prophesize('EmployeeRepository'); $sut = new UserService($repo->reveal()); $sut->post($employee); $repo->add($employee)->shouldHaveBeenCalled(); }
  55. Spies » Record interactions, allow verification afterwards » More familiar

    workflow: Arrange -> Act -> Assert Given -> When -> Then » Can reveal more intent, by hiding irrelevant calls » Less precise, leading to less fragile tests » Debugging can be harder
  56. Spies: When to use them (instead of mocks) » If

    you would like to » To help communicate the intentions of your test » If you can't predict a value ahead of time » If a mock wouldn't be able to report a failed expectation (edge case)
  57. Test First or Test Last

  58. State or Behaviour

  59. Classicist vs Mockist

  60. Verifiying State “We inspect the state of the SUT after

    it has been exercised and compare it to the expected state.”
  61. Verifying Behaviour “We capture the indirect outputs of the SUT

    as they occur and compare them to the expected behavior.”
  62. Behaviour Verification: Precision “Following Einstein, a specification should be as

    precise as possible, but not more precise” -- Mock Roles, not Objects Find a balance between precision and flexibility. Greater precision leads to over- specified, brittle tests. Allow Queries, expect Commands
  63. Behaviour Verification Mock all the things? 16 Mock objects 18

    method expectations
  64. Best practices: Don't mock values Just create them $email =

    m::mock("Email"); $email->shouldReceive("getDomain")->andReturn("example.com"); $email = Email::fromString("dave@example.com");
  65. Best Practices: Try not to mock concrete classes » It

    hides relationships between objects » A form of over-specification » If describing an interface doesn't seem right, don't use a test double $employee = m::mock("Employee"); $employee->shouldReceive("getEmail")->andReturn("dave@atst.io"); # use object mothers for complicated object graphs etc $employee = ExampleEmployee::withEmail("dave@atst.io");
  66. Best Practices: Only mock types you own » Or try

    to only mock types you trust » Use mocking to derive interfaces for the things your SUT needs » Write adapters for the things you don't trust
  67. Best Practices: Mock Ports

  68. Examples

  69. Example: Stubs and overspecification class SalaryCalculator { private $repo; public

    function __construct(EmployeeRepository $repo) { $this->repo = $repo; } public function calculateTotalSalary($id) { $employee = $this->repo->find($id); return $employee->getSalary() + $employee->getBonus(); } }
  70. Example: Stubs and overspecification /** @test */ public function it_calculates_total_salary()

    { $employee = $this->getMock('Employee'); $employee->expects($this->once()) ->method('getSalary') ->will($this->returnValue(1000)); $employee->expects($this->once()) ->method('getBonus') ->will($this->returnValue(1100)); $repo = $this->getMock('EmployeeRepository'); $repo->expects($this->once()) ->method('find') ->will($this->returnValue($employee)); $salaryCalculator = new SalaryCalculator($repo); $this->assertEquals( 2100, $salaryCalculator->calculateTotalSalary(1) ); }
  71. Example: Stubs and overspecification $employee = $this->getMock('Employee'); # is this

    an interface? $employee->expects($this->once()) # mock or stub? ->method('getSalary') ->will($this->returnValue(1000)); $employee->expects($this->once()) # mock or stub? ->method('getBonus') ->will($this->returnValue(1100));
  72. Example: Stubs and overspecification $repo = $this->getMock('EmployeeRepository'); $repo->expects($this->once()) # mock

    or stub? ->method('find') ->will($this->returnValue($employee));
  73. Example: Stubs and overspecification public function it_calculates_total_salary() { $employee =

    ExampleEmployee::thatGetsPaid(10000, 5000); $repo = $this->getMock('EmployeeRepository'); $repo->method('find') ->willReturn($employee); $calc = new SalaryCalculator($repo); $this->assertEquals( 15000, $calc->calculateTotalSalary(123) ); }
  74. Example: Mocks vs Spies # symfony controller public function forgottenPasswordAction($emailAddress)

    { $user = $this->getDoctrine() ->getRepository("Acme:User") ->findByEmail($emailAddress); $generator = (new RandomLib\Factory) ->getLowStrengthGenerator(); $token = $generator->generateString(32); $this->get('acme.password_reset_token_repository') ->add($token, $userId); $this->sendPasswordResetEmail($user, $token); return $this->render( 'AcmeBundle:Auth:forgotten_password_sent.html.twig', [] ); }
  75. Example: Mocks vs Spies /** @test */ public function it_stores_the_token()

    { $randomStrGenerator = $this->prophesize("RandomStringGenerator"); $randomStrGenerator->generateWithLength(32) ->willReturn($randomString = 'random string'); $repo = $this->prophesize("PasswordResetTokenRepository"); $repo->addToken($randomString, $userId = 123) ->shouldBeCalled(); $generator = new PasswordResetTokenGenerator( $repo->reveal(), $randomStrGenerator->reveal() ); $generator->generateToken($userId); }
  76. Example: Mocks vs Spies interface RandomStringGenerator { function generateWithLength($length); }

    interface PasswordResetTokenRepository { function addToken($token, $userId); }
  77. Example: Mocks vs Spies class PasswordResetTokenGenerator { public function __construct(PasswordResetTokenRepository

    $repo, RandomStringGenerator $generator) { $this->repo = $repo; $this->generator = $generator; } public function generateToken($userId) { $token = $this->generator->generateWithLength(32); $this->repo->addToken($token, $userId); return $token; } }
  78. Example: Mocks vs Spies /** @test */ public function it_stores_the_token()

    { $randomStrGenerator = $this->prophesize("RandomStringGenerator"); $randomStrGenerator->generateWithLength(32) ->willReturn($randomString = 'random string'); $repo = $this->prophesize( "PasswordResetTokenRepository" ); $generator = new PasswordResetTokenGenerator( $repo->reveal(), $randomStrGenerator->reveal() ); $generator->generateToken($userId = 123); $repo->addToken($randomString, $userId) ->shouldHaveBeenCalled(); }
  79. Example: Mocks vs Spies /** @test */ public function it_stores_the_token()

    { $repo = $this->prophesize( "PasswordResetTokenRepository" ); $generator = new PasswordResetTokenGenerator( $repo->reveal() ); $token = $generator->generateToken($userId = 123); $repo->addToken($token, $userId) ->shouldHaveBeenCalled(); }
  80. Example: Mocks vs Spies class PasswordResetTokenGenerator { public function __construct(PasswordResetTokenRepository

    $repo) { $this->repo = $repo; } public function generateToken($userId) { $token = (new RandomLib\Factory) ->getLowStrengthGenerator(); ->generateString(32); $this->repo->addToken($token, $userId); return $token; } }
  81. Example: Strict or Lenient class UserService { public function post(User

    $user) { # stuff $this->entityManager->persist($user); $this->entityManager->flush(); # more stuff } }
  82. Example: Extra Lenient over-specification /** @test */ function it_should_add_the_user_to_the_entity_manager() {

    $entityManager = Mockery::mock("EntityManager") ->shouldIgnoreMissing(); $entityManager->shouldReceive("persist") ->with(Mockery::type("User")) ->once(); $sut->post(new User()); }
  83. Example: Extra Lenient over-specification /** @test */ function it_should_flush_the_entity_manager() {

    $entityManager = Mockery::mock("EntityManager") ->shouldIgnoreMissing(); $entityManager->shouldReceive("flush") ->once(); $sut->post(new User()); }
  84. Example: Lenient - false positive? /** @test */ function it_should_persist_the_new_user()

    { $entityManager = Mockery::mock("EntityManager"); $entityManager->shouldReceive("persist") ->with(Mockery::type("User") ->once(); $entityManager->shouldReceive("flush") ->once(); $sut->post(new User()); }
  85. Example: Strict /** @test */ function it_should_persist_the_new_user() { $entityManager =

    Mockery::mock("EntityManager"); $entityManager->shouldReceive("persist") ->with(Mockery::type("User") ->ordered(); ->once(); $entityManager->shouldReceive("flush") ->ordered() ->once(); $sut->post(new User()); }
  86. Tooling

  87. Mockery 1.0 API? use Mockery\TestDouble as d; $repo = d::dummy("UserRepository");

    $repo = d::stub("UserRepository"); $repo->stub("find")->toReturn(new User()); $repo = d::mock("UserRepository"); $repo->shouldReceive("add"); $repo = d::spy("UserRepository"); $repo->shouldHaveReceived("add");
  88. Questions/Feedback » joind.in/11557 » @davedevelopment » dave@atst.io

  89. Image credits » https://www.flickr.com/photos/ jeepeenyc/219542428 » https://www.flickr.com/photos/ hummergoldcost/5063568078 » https://www.flickr.com/photos/intoxi-

    cation/4075151454 » https://www.flickr.com/photos/val_s/ 3541952533 » https://www.flickr.com/photos/glasgows/ 429710504
  90. Image credits » Spaceballs The Movie » Airplane! » Spies

    Like Us » Jakemans - mollysmixtures.co.uk