Mocks and how to avoid them



Mocks and how to avoid them

0 0


lightning

slides and notes for lightning talk

On Github benhyland / lightning

Mocks and how to avoid them

(high-speed!)

Why are we here?

  • Mocking is usually awful, let's understand why
  • There are better alternatives, so let's learn about them
  • Mocking is helpful sometimes, let's mention some of those times

Mocking is awful

public void testCreditCardIsCharged() {

    paymentProcessor = new PaymentProcessor(mockCreditCardServer);

    when(mockCreditCardServer.isServerAvailable()).thenReturn(true);
    when(mockCreditCardServer.beginTransaction()).thenReturn(mockTransactionManager);
    when(mockTransactionManager.getTransaction()).thenReturn(transaction);
    when(mockCreditCardServer.pay(transaction, creditCard, 500)).thenReturn(mockPayment);
    when(mockPayment.isOverMaxBalance()).thenReturn(false);

    paymentProcessor.processPayment(creditCard, Money.dollars(500));

    verify(mockCreditCardServer).pay(transaction, creditCard, 500);
}
					

Mock tests are not good value

  • Cost to write is fairly low, but cost of maintenance and poor design is high
  • Bugs found and protection against regression is low
  • Mocks test wiring or coupling rather than something more useful
  • Mocking input data increases fragility and discourages writing humane apis

Separate effects from calculations

public void publishMessage(InternalMessage message, Session session,
                           MessagePublisher publisher) {

    publisher.transformAndPublish(message, session);
}
					
public void publishMessage(InternalMessage message, Session session,
                           MessageRenderer renderer, MessagePublisher publisher) {

    RenderedMessage toPublish = renderer.transform(message);
    publisher.publish(toPublish, session);
}
					

Separate effects from calculations

public Order acceptInstruction(Instruction instruction, Account account,
                               PaymentProcessor processor) {

    if(account.canAfford(instruction.value())) {
        processor.debit(account.id(), instruction.value());
        return Order.buildFrom(instruction);
    }
    return null;
}
					
public Pair<Order, Charge> acceptInstruction(Instruction instruction, Account account) {

    if(account.canAfford(instruction.value())) {
        Charge debit = new Charge(account.id(), instruction.value());
        Order order = Order.buildFrom(instruction);
        return Pair.of(order, debit);
    }
    return null;
}
					

Test at an appropriate level

A unit doesn't necessarily mean a class.

If several classes are tightly coupled, it's OK to unit test them together.

If a class packages independent functions, it's OK to test them separately.

Reduce coupling by exposing effects in the interface between units.

Test the glue at integration or acceptance level.

When to mock

Mocking is appropriate if you are testing an interaction which is genuinely an effect, at the edge of your component.

public void deliverMessage(Message message, Route route) {
    if(route.accept(message)) {
        auditLog.auditDeliverySuccess(message.id());
    }
    else {
        auditLog.auditDeliveryFailure(message.id());
    }
}
					

Mock tests can sometimes be preferable to no tests, especially in legacy code while you aren't sure of all the requirements for a particular program.

Summary

  • factor your code to separate calculations and effects
  • define what counts as an effect by looking at component boundaries
  • use mock tests for effects
  • use input/output tests for calculations, and avoid mocking input data
  • appropriate units can sometimes be bigger than a single class
  • test glue code logic at integration or acceptance level

Mocks, by their very nature, are coupled to mechanisms instead of outcomes. Mocks, or the setup code that builds them, have deep knowledge of the inner workings of several different classes.

That knowledge is the very definition of high-coupling.

- Bob Martin