Slide 1

Slide 1 text

Mocks Aren't Stubs, Fakes, Dummies or Spies

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

Terminology

Slide 7

Slide 7 text

Recommended Reading

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

Indirect Output Something the SUT sends to one of it's DOCs class UserService { public function post(User $user) { $this->repo->add($user); return; } }

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

Creating Test Doubles

Slide 17

Slide 17 text

Hand built

Slide 18

Slide 18 text

Generated

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

Configurable

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

Dummies

Slide 23

Slide 23 text

Dummies Used whenever we need to pass arguments to a constructor or method, where those arguments should not be used while exercising the SUT

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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 }

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

Fakes

Slide 32

Slide 32 text

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.

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

Stubs

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

Mocks

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

Spies

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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)

Slide 57

Slide 57 text

Test First or Test Last

Slide 58

Slide 58 text

State or Behaviour

Slide 59

Slide 59 text

Classicist vs Mockist

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

Best Practices: Mock Ports

Slide 68

Slide 68 text

Examples

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

Tooling

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

Questions/Feedback » joind.in/11557 » @davedevelopment » dave@atst.io

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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