Imagine a world without mocks

Post on 09-Jan-2017

3468 Views

Category:

Software

0 Downloads

Preview:

Click to see full reader

Transcript

Imagine a world without mocks

@KenScambler Scala Developer at

NOOOPE

Me

14 years

5 years

5 years

when possible

when bored

when forced

http://techblog.realestate.com.au/to-kill-a-mockingtest/

Q: What’s so bad about mocks & stubs?

A: The problem they solve is “how to test poorly designed code”

How did we get here?

UserRepoUserService

User getUser(UserId)

DB

AuthServiceboolean authUser(UserId)

Collaborators galore!

Record selectUser(SQL)

UserRepoUserService

User getUser(UserId)

DB

AuthServiceboolean authUser(UserId)

Record selectUser(SQL)

How to separate UserService?

http://martinfowler.com/articles/mocksArentStubs.html

Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test.

UserServiceUser getUser(UserId)

If you ask me “authUser(1234)”, I’ll say “true”

Stub

UserRepoDB

Record selectUser(SQL)

UserServiceUser getUser(UserId)

Stub

UserRepoRecord selectUser(SQL)

UserService’s input is now deterministic!

If you ask me “authUser(1234)”, I’ll say “true”

DB

http://martinfowler.com/articles/mocksArentStubs.html

Mocks are objects pre-programmed with expectations which form a specification of the calls they are expected to receive.

UserServiceUser getUser(UserId)

I expect “selectUser()” to be called once

Mock

If you ask me “authUser(1234)”, I’ll say “true”

UserServiceUser getUser(UserId)

I expect “selectUser()” to be called once

Mock

UserService’s output is now deterministic!

If you ask me “authUser(1234)”, I’ll say “true”

UserServiceUser getUser(UserId)

MockDeterministic output

Deterministic input

= Fairly sane test

So far so good.

But wait! There’s a cost…

1. Coupled to brittle implementation details.

UserServiceUser getUser(UserId)

Mock

What if an equivalent method is called instead? If you ask me

“authUser(1234)”, I’ll say “true”

authLocalUser(1234)

Stub

UserServiceUser getUser(UserId)

Ah shit.

authLocalUser(1234)

UserServiceUser getUser(UserId)

Honestly, no one’s asked me that before.authLocalUser(1234)

UserServiceUser getUser(UserId)

I’m just a sock.

authLocalUser(1234)

2.Somewhat misses the point of the test

UserServiceUser getUser(UserId)

authLocalUser(1234)

I expect “selectUser()” to be called once

Still no idea.

UserServiceUser getUser(UserId)

IT DIDN’T GET CALLED!!! NOTHING HAPPENED!!!!!

UserServiceUser getUser(UserId)

IT DIDN’T GET CALLED!!! NOTHING HAPPENED!!!!!

Real problem:The stub configuration was out of date.

Case study #1Clumsy input

public interface Config { // Database stuff String getDatabaseHost(); int getDatabasePort(); int getMaxThreads(); int getConnectionTimeout(); // Potato settings String getDefaultPotatoVariety(); int getMaxPotatoes(); double getPotatoShininess();

// Sacrificial settings int getBloodSacrificeGoatCount(); int getBloodSacrificeChickenCount(); int getBloodSacrificeSheepCount(); }

public class PotatoService {

public PotatoService(Config config) { this.potatoVariety = config.getPotatoVariety(); this.maxPotatoes = config.getMaxPotatoes(); }

public Salad makePotatoSalad() {...} }

public class PotatoServiceTest {

Config config = mock(Config.class)

@Before public void before() {

when(config.getDefaultPotatoVariety()) .thenReturn(“pontiac”);

when(config.getMaxPotatoes()) .thenReturn(33); }

public testMakeSalad() { PotatoService service = new PotatoService(); Assert.equalTo(service.makeSalad(), ...); } }

public class PotatoServiceTest {

Config config = mock(Config.class)

@Before public void before() {

when(config.getDefaultPotatoVariety()) .thenReturn(“pontiac”);

when(config.getMaxPotatoes()) .thenReturn(33); }

public testMakeSalad() { PotatoService service = new PotatoService(); Assert.equalTo(service.makeSalad(), ...); } }

Stub

Looks ok. But what is the stub trying to tell us?

No-ones ever going to need all those things at once.

public interface Config { // Database stuff String getDatabaseHost(); int getDatabasePort(); int getMaxThreads(); int getConnectionTimeout(); // Potato settings String getDefaultPotatoVariety(); int getMaxPotatoes(); double getPotatoShininess();

// Sacrificial settings int getBloodSacrificeGoatCount(); int getBloodSacrificeChickenCount(); int getBloodSacrificeSheepCount(); }

That’s better!

public interface DatabaseConfig { String getDatabaseHost(); int getDatabasePort(); int getMaxThreads(); int getConnectionTimeout();}

public interface PotatoConfig { String getDefaultPotatoVariety(); int getMaxPotatoes(); double getPotatoShininess();}

public interface SacrificialConfig { int getBloodSacrificeGoatCount(); int getBloodSacrificeChickenCount(); int getBloodSacrificeSheepCount();}

public class PotatoService {

public PotatoService(PotatoConfig config) { this.potatoVariety = config.getPotatoVariety(); this.maxPotatoes = config.getMaxPotatoes(); }

public Salad makePotatoSalad() {...} }

Don’t you just need the two fields? Does it matter where they come from?

public class PotatoService {

public PotatoService(String variety, int max) { this.potatoVariety = variety; this.maxPotatoes = max; }

public Salad makePotatoSalad() {...} }

The application wiring can be someone else’s business.

public class PotatoServiceTest {

public testMakeSalad() { PotatoService service = new PotatoService(“pontiac”, 33);

Assert.equalTo(service.makeSalad(), ...); } }

- More modular- More reusable- Simpler- Less code- Stubs are gone

Case study #2Unnecessary mutable

state

removeCoins()

insertCoins()

collectCan()

public interface Wallet { int removeCoins(int amount); int getAmount(); }

public interface VendingMachine { void insertCoins(int amount); Can collectCan(); int getStoredCash();}

public interface Customer { void buyDrink();}

public class CustomerTest {

Wallet wallet = mock(Wallet.class); VendingMachine machine = mock(VendingMachine.class);

@Before public void before() { when(wallet.removeCoins(3)).thenReturn(3); when(vendingMachine.collectCan()) .thenReturn(new CokeCan()); }

public testBuyDrink() { Customer c = new Customer(); c.buyDrink();

verify(wallet).removeCoins(3); verify(vendingMachine).insertCoins(3); verify(vendingMachine).collectCan(); }}

public class CustomerTest {

Wallet wallet = mock(Wallet.class); VendingMachine machine = mock(VendingMachine.class);

@Before public void before() { when(wallet.removeCoins(3)).thenReturn(3); when(vendingMachine.collectCan()) .thenReturn(new CokeCan()); }

public testBuyDrink() { Customer c = new Customer(); c.buyDrink();

verify(wallet).removeCoins(3); verify(vendingMachine).insertCoins(3); verify(vendingMachine).collectCan(); }}

Stub

Mock

The class under test is separated now!

But what are the mocks telling us?

public interface Wallet { int removeCoins(int amount); int getAmount(); }

public interface VendingMachine { void insertCoins(int amount); Can collectCan(); int getStoredCash();}

public interface Customer { void buyDrink();}

Surely we care about the resulting state, not the in-betweeny verbs.

If the state is just immutable values, we don’t have to force isolation

public interface Wallet { int removeCoins(int amount); int getAmount(); }

public interface VendingMachine { void insertCoins(int amount); Can collectCan(); int getStoredCash();}

public interface Customer { void buyDrink();}

public interface Wallet { int getAmount(); Wallet removeCoins(int amount);}

public interface VendingMachine { Optional<Can> getCanInTray(); int getStoredCash(); List<Can> getCansInMachine(); VendingMachine insertCoins(int amount); VendingMachine collectCan();}

public interface Customer { Wallet getWallet(); List<Can> getCansHeld(); Pair<VendingMachine, Customer> buyDrink(VendingMachine vm);}

public interface Wallet { int getAmount(); Wallet removeCoins(int amount);}

public interface VendingMachine { Optional<Can> getCanInTray(); int getStoredCash(); List<Can> getCansInMachine(); VendingMachine insertCoins(int amount); VendingMachine collectCan();}

public interface Customer { Wallet getWallet(); List<Can> getCansHeld(); Pair<VendingMachine, Customer> buyDrink(VendingMachine vm);}

Immutable state

public interface Wallet { int getAmount(); Wallet removeCoins(int amount);}

public interface VendingMachine { Optional<Can> getCanInTray(); int getStoredCash(); List<Can> getCansInMachine(); VendingMachine insertCoins(int amount); VendingMachine collectCan();}

public interface Customer { Wallet getWallet(); List<Can> getCansHeld(); Pair<VendingMachine, Customer> buyDrink(VendingMachine vm);}

“Actions” just return new copies

public class CustomerTest {

public testBuyDrink() { Customer c = new Customer(new Wallet(23)); VendingMachine vm = new VendingMachine(10,30);

Pair<VendingMachine, Customer> result = c.buyDrink(vm); Customer c2 = result.second(); VendingMachine vm2 = result.first();

Assert.equals(20, c2.getWallet().getAmount()); Assert.equals(9, vm2.getCansInMachine().size()); Assert.equals(33, vm2.getStoredCash()); }}

- Less moving parts- More reusable- Simpler- Easier - Mocks & Stubs are

gone

“But then it’s an integration test!”

1. Immutable data structures are just

values.

2. We have no business peeking at

a method’s tools; only its results,

effects

3. Pure functions are already

deterministic

Case study #3Essential effects

public interface EmailSender { void sendEmail(String addr, Email email);}

public class SpecialOffers { private final EmailSender sender;

void sendSpecialOffers(Customer c) { if (!c.isUnsubscribed()) { String content = "Hi " + c.getName() + "!"; sender.sendEmail(c.getEmailAddr(), new Email(content)) } }}

public class SpecialOffersTest {

EmailSender sender = mock(EmailSender.class)

public testSendEmail() { SpecialOffers offers = new SpecialOffers(sender);

offers.sendSpecialOffers( new Customer(false, “Bob”, “foo@foo.com”));

verify(sender).send(“foo@foo.com”, new Email(“Hi, Bob!”)); }}

public class SpecialOffersTest {

EmailSender sender = mock(EmailSender.class)

public testSendEmail() { SpecialOffers offers = new SpecialOffers(sender);

offers.sendSpecialOffers( new Customer(false, “Bob”, “foo@foo.com”));

verify(sender).send(“foo@foo.com”, new Email(“Hi, Bob!”)); }}

Mock

Ok, so it tests we send an email.

But what is the mock trying to tell us?

public interface EmailSender { void sendEmail(String addr, Email email);}

public class SpecialOffers { private final EmailSender sender;

void sendSpecialOffers(Customer c) { if (!c.isUnsubscribed()) { String content = "Hi " + c.getName() + "!"; sender.sendEmail(c.getEmailAddr(), new Email(content)) } }} I only care about the intent to

send an email, not the actual sending. Can the intent be its own thing?

public interface SendEmailIntent { String getAddress(); Email getEmail();}

public interface Interpreter { void interpret(SendEmailIntent intent);}

public class SpecialOffers {

Optional<SendEmailIntent> sendSpecialOffers( Customer c) { if (!c.isUnsubscribed()) { String content = "Hi " + c.getName() + "!"; return Optional.of(new SendEmailIntent( c.getEmailAddr(), new Email(content))); } else { return Optional.empty(); } }}

We can have an interpreter elsewhere.

public class SpecialOffersTest {

public testSendEmail() { SpecialOffers offers = new SpecialOffers();

SendEmailIntent intent = offers.sendSpecialOffers( new Customer(false, “Bob”, “foo@foo.com”)).get();

Assert.equals(intent.getAddress(), “foo@foo.com”); Assert.equals(intent.getEmail().getText(), “Hi, Bob!”); }}

- Separated intent from execution

- More reusable- Simpler- Easier - Mocks are gone

Mocks kill TDD.

TDD = design methodology

Test-first encourages you to design code well enough to

test…

…and no further.

Mocks & stubs set a

looooow bar

This totally guts TDD’s value for design.

Conclusion:Side effects are the real killer

All I do is make the input deterministic. If the input is already just immutable values, then you don’t need me.

Stub Mock

If you’re just using me because stuff is hard to create, you need to get back and design harder!

Stub Mock

I make output deterministic, by recording method calls instead of allowing effects.

Stub Mock

Sometimes, this means that I test a pointless web of lies, that doesn’t touch the code’s reason for existence.

Stub Mock

Other times, I am really testing the intent of the code, which can be pulled out as its own structure. This separates the concern of choosing the next thing.

Stub Mock

Stub Mock

If you are using immutable types and pure functions, then you’re home and hosed.

Forget about • “collaborators”• “Tell don’t ask”• Avoiding static

methods• Avoiding “new”.

top related