Sunday, January 31, 2010

Making your manual mocks resistant to interface changes

Making your manual mocks resistant to interface changes

Suppose that you'd like to unit test the code below:

 public class BookService {
    private Books books;
    private Patrons patrons;
    
    public void borrow(String patronCode, String callNo) {
        Book b = books.get(callNo);
        Patron p = patrons.get(patronCode);
        if (b.isOnLoan()) {
            throw new RuntimeException("already on loan");
        }
        b.addBorrowRecords(new BorrowRecord(p, new Date()));
    }
}

public interface Books {
    Book get(String callNo);
}

public interface Patrons {
    Patron get(String patronCode);
}

You'll need to mock the two DAO objects: Books and Patrons. Usually I will hand-create these mock objects instead of using mock frameworks (in order to access the fields of the test case in a mocked method. For the other reasons, see Uncle Bob's article). So, the test may look like:

 public class BookServiceTest extends TestCase {
    private Book b123 = new Book("123", "Java programming");
    private Patron kent = new Patron("001", "Kent");

    public void testBorrow() throws Exception {
        BookService bs = new BookService();
        bs.setBooks(new Books() {
            @Override
            public Book get(String callNo) {
                return callNo.equals("123") ? b123 : null;
            }
        });
        bs.setPatrons(new Patrons() {
            @Override
            public Patron get(String patronCode) {
                return patronCode.equals("001") ? kent : null;
            }
        });
        bs.borrow("001", "123");
        List<BorrowRecord> records = b123.getBorrowRecords();
        assertEquals(records.size(), 1);
        BorrowRecord record = records.get(0);
        assertEquals(record.getPatron().getName(), "Kent");
    }
}

The problem is that if you later add a method to the DAO interfaces such as:

public interface Books {
    Book get(String callNo);
    void add(Book b);
}

Your unit test code will break because you aren't implementing the add() method. To avoid this problem,  the idea is to first create an (abstract) mock class implementing only the needed methods such as get(). Then use an automatic way to further create a subclass that provide all the dummy methods. To do the latter, one can use cglib. Here is an example:

public class BookServiceTest extends TestCase {
    private Book b123 = new Book("123", "Java programming");
    private Patron kent = new Patron("001", "Kent");

     public abstract class MockedBooks implements Books {
        @Override
        public Book get(String callNo) {
            return callNo.equals("123") ? b123 : null;
        }
    }
    public void testBorrow() throws Exception {
        BookService bs = new BookService();
        bs.setBooks(mock(MockedBooks.class));
        bs.setPatrons(new Patrons() {
            @Override
            public Patron get(String patronCode) {
                return patronCode.equals("001") ? kent : null;
            }
        });
        bs.borrow("001", "123");
        List<BorrowRecord> records = b123.getBorrowRecords();
        assertEquals(records.size(), 1);
        BorrowRecord record = records.get(0);
        assertEquals(record.getPatron().getName(), "Kent");
    }
    @SuppressWarnings("unchecked")
    private <T> T mock(Class<T> c) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(c);
        enhancer.setCallback(NoOp.INSTANCE);
        //Because MockedBooks is a non-static inner class, need to provide the outer instance
        return (T) enhancer.create(new Class[] { getClass() },
                new Object[] { this });
    }
}

 To make the code reusable in multiple test cases, just extract it into a base class:

public class ChangeResistantMockTest extends TestCase {
    @SuppressWarnings("unchecked")
    public <T> T mock(Class<T> c) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(c);
        enhancer.setCallback(NoOp.INSTANCE);
        return (T) enhancer.create(new Class[] { getClass() },
                new Object[] { this });
    }
}

public class BookServiceTest extends ChangeResistantMockTest {
    private Book b123 = new Book("123", "Java programming");
    private Patron kent = new Patron("001", "Kent");

     public abstract class MockedBooks implements Books {
        @Override
        public Book get(String callNo) {
            return callNo.equals("123") ? b123 : null;
        }
    }
&
nbsp;   public void testBorrow() throws Exception {
        BookService bs = new BookService();
        bs.setBooks(mock(MockedBooks.class));
        ...
    }
}

Acknowledgement: I got this idea from the Scala mailing list.

No comments:

Post a Comment