I stumbled upon this video, and boy it is so amazing! (if you ignore the annoying audience asking non-stop some annoying questions). This is clearly one of the most inspiring videos I have ever watched. So I must take some notes down and spread the idea as well.
I deeply believe that it is actually easy to make things complicated, on the contrary, it is hard to make things simple and elegant. I found some resonance from the speaker.
Why the speaker thinks that using stubs and mocks are actually harmful?
Stubs v.s. Mocks
There is a great essay by Martin Fowler about this topic, go read it before you continue if you haven’t done so.
Case study 1: Clumsy Input
Here is a bloated configuration interface.
public interface Config {
// Database stuff
String getDBHost();
int getDBPort();
int getMaxThreads();
int getConnectionTimeout();
// Potato settings
String getDefaultPotatoVarieties();
int getMaxPotatoes();
double getPotatoShinniness();
// Sacrificial settings
int getBSGoadCount();
int getBSChickenCount();
int getBSSheepCount();
}
Here is a class that uses the configuration interface.
public class PotatoService {
public PotatoService(Config config) {
this.potatoVariety = config.getDefaultPotatoVarieties();
this.maxPotatoes = config.getMaxPotatoes();
}
public Salad makePotatoSalad() {...}
}
Now we want to test the makePotatoSalad
function in PotatoService
. One can easily think of using a Config
stub.
public class PotatoServiceTest {
Config config = Mock(Config.class);
@Before
public void before() {
// This is a stub
when(config.getDefaultPotatoVarieties()).thenReturn("pontiac");
when(config.getMaxPotatoes()).thenReturn(33);
}
@Test
public void testMakingSalad() {
PotatoService service = new PotatoService(config);
Assert.equals(service.makePotatoSalad(), ...);
}
}
There are several observations.
First, we stub out the config because it is hard to create and contains stuff that the SUT(System Under Test) doesn’t care. So it is better to break up the fat interface into smaller ones. But this is not the end of the story.
Stare at the PotatoService
class a bit harder we will find that it actually needs only two things: potatoVariety
and maxPotatoes
. Does it matter where they come from? Why does this class needs to know about the Config
at all?
We can rewrite the PotatoService
as follows:
public class PotatoService {
public PotatoService(String variety, int max) {
this.potatoVariety = variety;
this.maxPotatoes = max;
}
public Salad makePotatoSalad() {...}
}
How to pass in the variety
and max
is someone else’s business, probably some factory or DI framework. And here is the test:
public class PotatoServiceTest {
@Test
public void testMakingSalad() {
// No stubs anymore!
PotatoService service = new PotatoService("pontiac", 33);
Assert.equals(service.makePotatoSalad(), ...);
}
}
Case study 2: Unnecessary Mutable State
In this example, a Customer
can buy from some VendingMachine
from his Wallet
.
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();
}
The tests look like this:
public class CustomerTest {
Wallet wallet = mock(Wallet.class);
VendingMachine vendingMachine = mock(VendingMachine.class);
@Before
public void before() {
// These are stubs
when(wallet.removeCoins(3)).thenReturn(3);
when(vendingMachine.collectCan()).thenReturn(new CokeCan());
}
@Test
public void testBuyDrink() {
Customer c = new Customer(wallet, vendingMachine);
c.buyDrink();
// These are mocks
verify(wallet).removeCoins(3);
verify(vendingMachine).insertCoins(3);
verify(vendingMachine).collectCan();
}
}
Here the SUT is separated, but the stubs and mocks are being too tied to the implementation details. These tests are testing behaviours rather than the final states. What we really care is after all these, what are the final state of the wallet, the vendingMachine and the customer. The wallet and vendingMachine must have the correct amount of money in the end, and the customer gets a can of drink. We care less about what functions are called in what order and such.
The true reason we need the stubs and mocks to help us is that these classes are mutable, then change. So we need to catch the trace of how they changed over time. But if they immutable. then we don’t need to trace anymore, we can just see its values. This concept should be very familiar to you if you have some functional programming background. So these classes can be rewritten as follows:
public interface Wallet {
int getAmount();
Wallet removeCoins(int amount);
}
public interface VendingMachine {
Optional<Can> getCanInTray();
int getStoredCash();
VendingMachine insertCoins(int amount);
VendingMachine collectCan();
}
public interface Customer {
Wallet getWallet();
List<Can> getCansHeld();
Pair<VendingMachine, Customer> buyDrink(VendingMachine vendingMachine);
}
Then the tests becomes this:
public class CustomerTest {
@Test
public void 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.getCanInTray().size());
Assert.equals(33, vm2.getStoredCash());
}
}
Case study 3: Essential effects
This example shows how to finally deal with the side effects. Sending emails, writing databases and such. We cannot or don’t want to test them directly, so using mocks can be helpful here. Like the following example:
public interface EmailSender {
void sendEmail(String address, Email email);
}
public class SpecialOffers {
private final EmailSender sender;
void sendSpecialOffers(Customer customer) {
if (!customer.isUnsubscribed()) {
String content = "Something...";
sender.sendEmail(c.getEmailAddress(), new Email(content));
}
}
}
When we test, we don’t really want to send out emails, so we are going to mock the email sender.
public class SpecialOfferTest {
EmailSender sender = mock(EmailSender.class);
public testSendEmail() {
SpeicalOffers offers = new SpeicalOffers(sender);
offers.sendSpecialOffers(new Customer(false, "Bob", "foo@bar.com"));
// mock
verify(sender).send("foo@bar.com", new Email("Something..."));
}
}
So what we really want to test is the intent of sending an email. Can we separate out the intent by itself?
public interface SendEmailIntent {
String getAddress();
Email getEmail();
}
public interface interpreter {
void interpret(SendEmailIntent i);
}
public class SpecialOffers {
Optional<SendEmailIntent> sendSpecialOffers(Customer c) {
if (!c.isUnsubscribed()) {
String content = "Something...";
return Optional.of(new SendEmailIntent(c.getEmailAddress(), new Email(content)));
} else {
return Optional.empty();
}
}
}
Now the tests become:
public class SpecialOfferTest {
@Test
public void testSendEmail() {
SpecialOffers offers = new SpeicalOffers();
SendEmailIntent intent = offers.sendEmail(new Customer(false, "Bob", "foo@bar.com")).get();
Assert.equals("foo@bar.com", intent.getAddress());
Assert.equals("Something...", intent.getEmail().getText());
}
}
These examples really opened my eye as to how you can leverage some of the good parts from FP into OO programming.
Share this post
Twitter
Google+
Facebook
Reddit
LinkedIn
StumbleUpon
Email