Don’t trust mocked interfaces (use them)
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
- get an order by ID (it will contain the user ID)
- 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).