Test behaviour, not implementation

Quite often I see unit tests that seem very interested in a class’s inner workings. This not only misses the point of a unit test, but makes the class harder to refactor, since the corresponding unit test will have to change more often as well.

Suppose you have a BookService class, responsible for retrieving and storing books:

public class BookService 
 
    private BookRepository bookRepository;
 
    public BookService(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }
 
    public List<Book> getAllBooks() {
        return bookRepository.getAllBooks();
    }
 
    public void createBook(Book book) {
        bookRepository.createBook(book);
    }
}

Pretty straightforward, right? Now, suppose we write a unit test to test the getAllBooks method, using Mockito to stub things out. Often I encounter something like this:
 private Book lord_of_the_rings = new Book("Lord of the rings");
 
 @Test
    public void reallyInterestedInWhatThisServiceIsDoing() {
        BookRepository bookRepository = mock(BookRepository.class);
        when(bookRepository.getAllBooks()).thenReturn(Collections.singletonList(lord_of_the_rings));
        BookService bookService = new BookService(bookRepository);
        
        assertThat(bookService.getAllBooks(), containsInAnyOrder(lord_of_the_rings));
        verify(bookRepository, times(1)).getAllBooks();
    }

Notice the verify call at the end of the method. This makes assumptions about the class’s inner workings. Don’t do this. A unit test should check if a class implements its contract. Instead, verify behaviour only:
@Test
    public void shouldGetAllBooks() {
        BookRepository bookRepository = mock(BookRepository.class);
        when(bookRepository.getAllBooks()).thenReturn(Collections.singletonList(lord_of_the_rings));
        BookService bookService = new BookService(bookRepository);
        
        assertThat(bookService.getAllBooks(), containsInAnyOrder(lord_of_the_rings));
    }

Of course you need a collaborator to get this class going, hence the BookRepository mock object and the recorded behaviour using when.

Verfiy behaviour only also applies to methods that mutate the system (a “command”), like the createBook method in the example class. Of course, such methods have a void result type (if you build your system CQRS-wise, of course). Only difference is, the output comes from the bottom this time (pun intended):

 @Test
    public void insertBook() {
        BookRepository bookRepository = mock(BookRepository.class);
        BookService bookService = new BookService(bookRepository);
        Book the_dark_tower = new Book("The Dark Tower");
        bookService.createBook(the_dark_tower);
        
        verify(bookRepository, times(1)).createBook(the_dark_tower);
    }

In some cases, a command also has a result type, for example, the result of the mutation. For example, the createBook method could have returned its Book parameter enriched with an id, for example, as a result of storing the book in the repository. In that case, you could verify output at both sides, although assertion the return value only would still be preferred.

Most of the time, however, encountering such a method is probably a side effect anti-pattern and should be refactored into two separate methods.

Leave a Reply

Your email address will not be published. Required fields are marked *