Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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 full-size slide

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

    View full-size 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  6. Recommended Reading

    View full-size slide

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

    View full-size slide

  8. 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 full-size slide

  9. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  12. 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 full-size slide

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

    View full-size slide

  14. 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 full-size slide

  15. Creating Test Doubles

    View full-size slide

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

    View full-size slide

  17. Configurable

    View full-size slide

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

    View full-size slide

  19. 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 full-size slide

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

    View full-size slide

  21. 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 full-size slide

  22. 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 full-size slide

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

    View full-size slide

  24. 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 full-size slide

  25. 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 full-size slide

  26. 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 full-size slide

  27. 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 full-size slide

  28. 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 full-size slide

  29. 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 full-size slide

  30. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  33. 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 full-size slide

  34. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  37. 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 full-size slide

  38. 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 full-size slide

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

    View full-size slide

  40. 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 full-size slide

  41. 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 full-size slide

  42. 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 full-size slide

  43. 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 full-size slide

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

    View full-size slide

  45. 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 full-size slide

  46. 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 full-size slide

  47. 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 full-size slide

  48. 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 full-size slide

  49. Test First or Test Last

    View full-size slide

  50. State or Behaviour

    View full-size slide

  51. Classicist vs Mockist

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  54. 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 full-size slide

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

    View full-size slide

  56. 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 full-size slide

  57. 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 full-size slide

  58. 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 full-size slide

  59. Best Practices: Mock
    Ports

    View full-size slide

  60. 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 full-size slide

  61. 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 full-size slide

  62. 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 full-size slide

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

    View full-size slide

  64. 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 full-size slide

  65. 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 full-size slide

  66. 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 full-size slide

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

    View full-size slide

  68. 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 full-size slide

  69. 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 full-size slide

  70. 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 full-size slide

  71. 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 full-size slide

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

    View full-size slide

  73. 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 full-size slide

  74. 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 full-size slide

  75. 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 full-size slide

  76. 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 full-size slide

  77. 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 full-size slide

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

    View full-size slide

  79. 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 full-size slide

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

    View full-size slide