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. 3.

    Mocks aren't Stubs “But as often as not I see

    mock objects described poorly.” -- Martin Fowler http://martinfowler.com/articles/ mocksArentStubs.html
  2. 4.

    Mocks Aren't Stubs, Fakes, Dummies or Spies » Terminology »

    Types of Test Doubles » Testing Behaviour or State » (Bigger) Examples
  3. 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
  4. 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');
  5. 12.

    Depended-on component (DOC) Something the SUT needs to use $doc

    = new UserRepository(); $sut = new UserService($doc);
  6. 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; } }
  7. 14.

    Indirect Input Something the SUT receives from one of it's

    DOCs class UserService { public function getUser($id) { return $this->repo->find($id); } }
  8. 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);
  9. 18.
  10. 19.

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

    phake/phake » codeception/aspect-mock » php-vcr/php-vcr » many others...
  11. 22.
  12. 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
  13. 24.

    Dummies: Values # SUT BCryptPasswordEncoder public function encodePassword($raw, $salt) {

    if ($this->isPasswordTooLong($raw)) { throw new BadCredentialsException( 'Invalid password.' ); } # ... }
  14. 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" ); }
  15. 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 }
  16. 27.

    Dummies: Objects by hand class DummyEncoder implements EncoderInterface { public

    function isPasswordValid($hashedPassword, $plainTextPassword, $salt) { throw new RuntimeException("Not implemented"); } }
  17. 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(), ""); }
  18. 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(), ""); }
  19. 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(), ""); }
  20. 31.
  21. 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.
  22. 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
  23. 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); } }
  24. 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") ); }
  25. 36.

    Fakes: Fake Object # config_test.yml framework: test: ~ session: storage_id:

    session.storage.mock_file profiler: collect: false
  26. 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
  27. 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'));
  28. 40.

    Fakes » Usually hand coded » Low coupling between SUT

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

    Stubs Stubs are used to control the indirect inputs to

    the SUT, by providing canned responses to calls made during the test
  31. 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) ); }
  32. 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
  33. 45.
  34. 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(); }
  35. 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); }
  36. 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
  37. 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
  38. 51.
  39. 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); }
  40. 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(); }
  41. 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
  42. 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)
  43. 60.

    Verifiying State “We inspect the state of the SUT after

    it has been exercised and compare it to the expected state.”
  44. 61.

    Verifying Behaviour “We capture the indirect outputs of the SUT

    as they occur and compare them to the expected behavior.”
  45. 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
  46. 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");
  47. 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");
  48. 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
  49. 68.
  50. 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(); } }
  51. 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) ); }
  52. 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));
  53. 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) ); }
  54. 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', [] ); }
  55. 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); }
  56. 76.

    Example: Mocks vs Spies interface RandomStringGenerator { function generateWithLength($length); }

    interface PasswordResetTokenRepository { function addToken($token, $userId); }
  57. 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; } }
  58. 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(); }
  59. 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(); }
  60. 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; } }
  61. 81.

    Example: Strict or Lenient class UserService { public function post(User

    $user) { # stuff $this->entityManager->persist($user); $this->entityManager->flush(); # more stuff } }
  62. 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()); }
  63. 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()); }
  64. 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()); }
  65. 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()); }
  66. 86.
  67. 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");
  68. 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
  69. 90.

    Image credits » Spaceballs The Movie » Airplane! » Spies

    Like Us » Jakemans - mollysmixtures.co.uk