$30 off During Our Annual Pro Sale. View Details »

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

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.

Dave Marshall

September 26, 2014
Tweet

More Decks by Dave Marshall

Other Decks in Programming

Transcript

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

    View Slide

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

    View Slide

  3. Mocks aren't Stubs
    “But as often as not I see mock objects
    described poorly.”
    -- Martin Fowler
    http://martinfowler.com/articles/
    mocksArentStubs.html

    View Slide

  4. Mocks Aren't Stubs, Fakes,
    Dummies or Spies
    » Terminology
    » Types of Test Doubles
    » Testing Behaviour or State
    » (Bigger) Examples

    View Slide

  5. Terminology
    -- http://xkcd.com/503/

    View Slide

  6. Terminology

    View Slide

  7. Recommended Reading

    View Slide

  8. Mockito
    “Mockito is a mocking framework that
    tastes really good.”
    -- https://code.google.com/p/mockito/

    View Slide

  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

    View Slide

  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');

    View Slide

  11. System under test (SUT)
    The thing we are testing
    $sut = new UserService();

    View Slide

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

    View Slide

  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;
    }
    }

    View Slide

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

    View Slide

  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);

    View Slide

  16. Creating Test Doubles

    View Slide

  17. Hand built

    View Slide

  18. Generated

    View Slide

  19. Generated Doubles: Tools
    » phpunit/phpunit-mock-objects
    » phpspec/prophecy
    » mockery/mockery
    » phake/phake
    » codeception/aspect-mock
    » php-vcr/php-vcr
    » many others...

    View Slide

  20. Configurable

    View Slide

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

    View Slide

  22. Dummies

    View Slide

  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

    View Slide

  24. Dummies: Values
    # SUT BCryptPasswordEncoder
    public function encodePassword($raw, $salt)
    {
    if ($this->isPasswordTooLong($raw)) {
    throw new BadCredentialsException(
    'Invalid password.'
    );
    }
    # ...
    }

    View Slide

  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"
    );
    }

    View Slide

  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
    }

    View Slide

  27. Dummies: Objects by hand
    class DummyEncoder implements EncoderInterface
    {
    public function isPasswordValid($hashedPassword,
    $plainTextPassword,
    $salt)
    {
    throw new RuntimeException("Not implemented");
    }
    }

    View Slide

  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(), "");
    }

    View Slide

  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(), "");
    }

    View Slide

  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(), "");
    }

    View Slide

  31. Fakes

    View Slide

  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.

    View Slide

  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

    View Slide

  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);
    }
    }

    View Slide

  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")
    );
    }

    View Slide

  36. Fakes: Fake Object
    # config_test.yml
    framework:
    test: ~
    session:
    storage_id: session.storage.mock_file
    profiler:
    collect: false

    View Slide

  37. Fakes: Fake Web Service
    Web services are often:
    » Network dependant
    » Hard to control

    View Slide

  38. Fakes: Fake Web Service
    davedevelopment/fake-recurly
    @app.route("/subscriptions/", 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

    View Slide

  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'));

    View Slide

  40. Fakes
    » Usually hand coded
    » Low coupling between SUT and test
    » May need to expose state for
    verification
    » Can get complicated to maintain

    View Slide

  41. Stubs

    View Slide

  42. Stubs
    Stubs are used to control the indirect
    inputs to the SUT, by providing canned
    responses to calls made during the test

    View Slide

  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)
    );
    }

    View Slide

  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

    View Slide

  45. Mocks

    View Slide

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

    View Slide

  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();
    }

    View Slide

  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);
    }

    View Slide

  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

    View Slide

  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

    View Slide

  51. Spies

    View Slide

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

    View Slide

  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);
    }

    View Slide

  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();
    }

    View Slide

  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

    View Slide

  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)

    View Slide

  57. Test First or Test Last

    View Slide

  58. State or Behaviour

    View Slide

  59. Classicist vs Mockist

    View Slide

  60. Verifiying State
    “We inspect the state of the SUT after it
    has been exercised and compare it to the
    expected state.”

    View Slide

  61. Verifying Behaviour
    “We capture the indirect outputs of the
    SUT as they occur and compare them to
    the expected behavior.”

    View Slide

  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

    View Slide

  63. Behaviour Verification
    Mock all the things?
    16 Mock objects
    18 method expectations

    View Slide

  64. Best practices: Don't mock
    values
    Just create them
    $email = m::mock("Email");
    $email->shouldReceive("getDomain")->andReturn("example.com");
    $email = Email::fromString("[email protected]");

    View Slide

  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("[email protected]");
    # use object mothers for complicated object graphs etc
    $employee = ExampleEmployee::withEmail("[email protected]");

    View Slide

  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

    View Slide

  67. Best Practices: Mock
    Ports

    View Slide

  68. Examples

    View Slide

  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();
    }
    }

    View Slide

  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)
    );
    }

    View Slide

  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));

    View Slide

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

    View Slide

  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)
    );
    }

    View Slide

  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',
    []
    );
    }

    View Slide

  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);
    }

    View Slide

  76. Example: Mocks vs Spies
    interface RandomStringGenerator {
    function generateWithLength($length);
    }
    interface PasswordResetTokenRepository {
    function addToken($token, $userId);
    }

    View Slide

  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;
    }
    }

    View Slide

  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();
    }

    View Slide

  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();
    }

    View Slide

  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;
    }
    }

    View Slide

  81. Example: Strict or Lenient
    class UserService
    {
    public function post(User $user)
    {
    # stuff
    $this->entityManager->persist($user);
    $this->entityManager->flush();
    # more stuff
    }
    }

    View Slide

  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());
    }

    View Slide

  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());
    }

    View Slide

  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());
    }

    View Slide

  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());
    }

    View Slide

  86. Tooling

    View Slide

  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");

    View Slide

  88. Questions/Feedback
    » joind.in/11557
    » @davedevelopment
    » [email protected]

    View Slide

  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

    View Slide

  90. Image credits
    » Spaceballs The Movie
    » Airplane!
    » Spies Like Us
    » Jakemans - mollysmixtures.co.uk

    View Slide