Don’t trust mocked interfaces (use them)

Matthieu Jacquot
5 min readJul 7, 2017

--

What’s worth than no test ? Test giving you confidence where it shouldn’t !

This article is a follow-up of my Hands-On Clean Architecture
The source code is on
github

The Use Case

We’ll keep it simple : we want a function that displays the name of the guy who placed an order

Let’s write checkOrder() that will receive the order ID & an interface that will be used to

  1. get an order by ID (it will contain the user ID)
  2. get user details by ID

If no error occurs, the name of the guy who placed the order will be displayed.

Easy !

I use Golang for the examples but as long as your favorite language supports interfaces, the idea remains the same.

import (
"errors"
"fmt"
)
//User : the guy who placed an order
type User struct {
id int
name string
}
//Order : content on an Order
type order struct {
id int
userID int
}
type orderReader interface {
getOrder(id int) (*order, error)
getUser(id int) (*user, error)
}
func checkOrder(oR orderReader, orderID int) error {
order, err := oR.getOrder(orderID)
if err != nil {
return errors.New("can't get Order")
}
user, err := oR.getUser(order.userID)
if err != nil {
return errors.New("can't get User")
}
fmt.Printf("Order %d belongs to user %s !", order.id, user.name)
return nil
}

Let’s create a mocked interface like so :

type NiceInterface struct{}func (tI NiceInterface) getOrder(id int) (*order, error) {
return &order{}, nil
}
func (tI NiceInterface) getUser(id int) (*user, error) {
return &user{}, nil
}

And test the CheckOrder function like that :

import "testing"func TestCheckOrderUseCase(t *testing.T) {
if err := checkOrder(NiceInterface{}, 10); err != nil {
t.Errorf("an error occured")
}
}

BOOM !

Order 0 belongs to user  !PASS 
coverage: 80.0% of statements

Okay I tested with the order ID = 10, it returned 0 and my user have no name but I don’t connect to a remote DB so my tests run damn fast ! Smart !

Because I’m a bit of a perfectionist, I’ll just improve my mocked interface so the getOrder() method returns me an order with the ID I gave it and the getUser() returns me a user with a name… and also test my error returns so I reach 100% coverage !

… HOLD YOUR HORSES ! (a cool math rock classic btw)

The Evil Interface

What if, one day, your “real” interface returns something like that ?

func (tI evilInterface) getOrder(id int) (*order, error) {
return nil, nil
}

or is it really OK to receive a pointer to an empty Order as in the NiceInterface example ?

The single contract a method passes with you is its signature : hope no more, be paranoid with the rest

Instead of using interfaces as a mean to run your tests in a “bubble”, use them to improve your incoherence detection & handling.

Tests

Let’s try to use these interfaces to see how we can strengthen a little bit our usecase.

As methods will be called in a sequential way and that we’ll try to catch inconsistencies as soon as they happen, we build slices of return values that will all fail.
An exception : the single one that should be able to let the execution flow continue is the index 0 of each slice. This way, we’ll be able to iterate over all the slices, one by one, while the others are all set to 0 and verify that CheckOrder() actually raises an error every time.

NB : in complex cases, we could write a helper function to build a valid return value for each method but we’ll keep this example minimal and assume we know what a valid return value is.

Interface setup

The single perfectly fine test case :

type EvilInterface struct {
GetOrderOutput GetOrderReturn
GetUserOutput GetUserReturn
}
type GetOrderReturn struct {
Order *uc.Order
Err error
}
type GetUserReturn struct {
User *uc.User
Err error
}
func (tI EvilInterface) GetOrder(id int) (*uc.Order, error) {
return tI.GetOrderOutput.Order, tI.GetOrderOutput.Err
}
func (tI EvilInterface) GetUser(id int) (*uc.User, error) {
return tI.GetUserOutput.User, tI.GetUserOutput.Err
}

First test Setup

func TestCheckOrderUseCase(t *testing.T) {
GetOrderReturns := []i.GetOrderReturn{
{&uc.Order{10, 20}, nil},
}
GetUserReturns := []i.GetUserReturn{
{&uc.User{20, "Matth"}, nil},
}
for k, v := range GetOrderReturns {
err := uc.CheckOrder(i.EvilInterface{v, GetUserReturns[0]}, 10)
check(t, "GetOrder", k, err)
}
for k, v := range GetUserReturns {
err := uc.CheckOrder(i.EvilInterface{GetOrderReturns[0], v}, 10)
check(t, "GetUser", k, err)
}
}
func check(t *testing.T, method string, k int, err error) {
if k == 0 && err != nil {
t.Errorf("useCase should pass for #%d of %s", k, method)
} else if k != 0 && err == nil {
t.Errorf("useCase unable to detect incoherence for case #%d of %s", k, method)
}
}

We now just have to add failing cases that should be handled :

  • return an error
  • return a null pointer (w/ no error)
  • return an empty structure (w/ no error)
  • return valid structure but not coherent with the use case

Full test example

func TestCheckOrderUseCase(t *testing.T) {
GetOrderReturns := []i.GetOrderReturn{
{&uc.Order{10, 20}, nil},
{&uc.Order{10, 20}, errors.New("hey")},
{nil, nil},
{&uc.Order{}, nil},
{&uc.Order{10, 0}, nil},
}
GetUserReturns := []i.GetUserReturn{
{&uc.User{20, "Matth"}, nil},
{&uc.User{20, "Matth"}, errors.New("text")},
{nil, nil},
{&uc.User{}, nil},
{&uc.User{10, "m"}, nil},
}
for k, v := range GetOrderReturns {
err := uc.CheckOrder(i.EvilInterface{v, GetUserReturns[0]}, 10)
check(t, "GetOrder", k, err)
}
for k, v := range GetUserReturns {
err := uc.CheckOrder(i.EvilInterface{GetOrderReturns[0], v}, 10)
check(t, "GetUser", k, err)
}
}
func check(t *testing.T, method string, k int, err error) {
if k == 0 && err != nil {
t.Errorf("useCase should pass #%d of %s", k, method)
} else if k != 0 && err == nil {
t.Errorf("useCase unable to detect wrong interface return in case #%d of %s", k, method)
}
}

And the resulting handling :

func CheckOrder(oR orderReader, orderID int) error {
order, err := oR.GetOrder(orderID)
if err != nil || order == nil {
return errors.New("unable to retreive the order")
}
if order.ID != orderID || order.UserID == 0 {
return errors.New("the order returned is wrong")
}
user, err := oR.GetUser(order.UserID)
if err != nil || user == nil {
return errors.New("unable to retreive the user")
}
if order.UserID != user.ID {
return errors.New("the user returned is wrong")
}
fmt.Printf("Order %d belongs to user %s !\n", order.ID, user.Name)
return nil
}

Conclusion

Tell me what you think about that, I’d be very pleased to answer any question if something remains unclear (and improve this article accordingly).

--

--