DEV Community

Chris James
Chris James

Posted on

Learn Go by writing tests: Mocking

This is the 7th post taken from a WIP project called Learn Go by writing Tests the aim of which is to get a familiarity with Go and learn techniques around TDD

This chapter is about mocking. Even if you don't have a great familiarity with Go, if you want to learn about mocking this post should be helpful.

Mocking

You have been asked to write a program which counts from 3, printing each number on a new line (with a 1 second pause) and when it reaches zero it will print "Go!" and exit.

3
2
1
Go!
Enter fullscreen mode Exit fullscreen mode

We'll tackle this by writing a function called Countdown which we will then put inside a main program so it looks something like this:

package main

func main() {
    Countdown()
}
Enter fullscreen mode Exit fullscreen mode

While this is a pretty trivial program, to test it fully we will need as always to take an iterative, test-driven approach.

What do I mean by iterative? We make sure we take the smallest steps we can to have useful software.

We dont want to spend a long time with code that will theoretically work after some hacking because that's often how developers fall down rabit holes. It's an important skill to be able to slice up requirements as small as you can so you can have working software.

Here's how we can divide our work up and iterate on it

  • Print 3
  • Print 3 to Go!
  • Wait a second between each line

Write the test first

Our software needs to print to stdout and we saw how we could use DI to facilitate testing this in the DI section.

func TestCountdown(t *testing.T) {
    buffer := &bytes.Buffer{}

    Countdown(buffer)

    got := buffer.String()
    want := "3"

    if got != want {
        t.Errorf("got '%s' want '%s'", got, want)
    }
}
Enter fullscreen mode Exit fullscreen mode

If anything like buffer is unfamiliar to you, re-read the previous section.

We know we want our Countdown function to write data somewhere and io.Writer is the de-facto way of capturing that as an interface in Go.

  • In main we will send to os.Stdout so our users see the countdown printed to the terminal
  • In test we will send to bytes.Buffer so our tests can capture what data is being generated

Try and run the test

./countdown_test.go:11:2: undefined: Countdown

Write the minimal amount of code for the test to run and check the failing test output

Define Countdown

func Countdown() {}
Enter fullscreen mode Exit fullscreen mode

Try again

./countdown_test.go:11:11: too many arguments in call to Countdown
    have (*bytes.Buffer)
    want ()
Enter fullscreen mode Exit fullscreen mode

The compiler is telling you what your function signature could be, so update it.

func Countdown(out *bytes.Buffer) {}
Enter fullscreen mode Exit fullscreen mode

countdown_test.go:17: got '' want '3'

Perfect!

Write enough code to make it pass

func Countdown(out *bytes.Buffer) {
    fmt.Fprint(out, "3")
}
Enter fullscreen mode Exit fullscreen mode

We're using fmt.Fprint which takes an io.Writer (like *bytes.Buffer) and sends a string to it. The test should pass.

Refactor

We know that while *bytes.Buffer works, it would be better to use a general purpose interface instead.

func Countdown(out io.Writer) {
    fmt.Fprint(out, "3")
}
Enter fullscreen mode Exit fullscreen mode

Re-run the tests and they should be passing.

To complete matters, let's now wire up our function into a main so we have some working software to reassure ourselves we're making progress.

package main

import (
    "fmt"
    "io"
    "os"
)

func Countdown(out io.Writer) {
    fmt.Fprint(out, "3")
}

func main() {
    Countdown(os.Stdout)
}
Enter fullscreen mode Exit fullscreen mode

Try and run the program and be amazed at your handywork.

Yes this seems trivial but this approach is what I would recommend for any project. Take a thin slice of functionality and make it work end-to-end, backed by tests.

Next we can make it print 2,1 and then "Go!".

Write the test first

By investing in getting the overall plumbing working right, we can iterate on our solution safely and easily. We will no longer need to stop and re-run the program to be confident of it working as all the logic is tested.

func TestCountdown(t *testing.T) {
    buffer := &bytes.Buffer{}

    Countdown(buffer)

    got := buffer.String()
    want := `3
2
1
Go!`

    if got != want {
        t.Errorf("got '%s' want '%s'", got, want)
    }
}
Enter fullscreen mode Exit fullscreen mode

The backtick syntax is another way of creating a string but lets you put things like newlines which is perfect for our test.

Try and run the test

countdown_test.go:21: got '3' want '3
        2
        1
        Go!'
Enter fullscreen mode Exit fullscreen mode

Write enough code to make it pass

func Countdown(out io.Writer) {
    for i := 3; i > 0; i-- {
        fmt.Fprintln(out, i)
    }
    fmt.Fprint(out, "Go!")
}
Enter fullscreen mode Exit fullscreen mode

Use a for loop counting backwards with i-- and use fmt.Fprintln to print to out with our number followed by a newline character. Finally use fmt.Fprint to send "Go!" aftward

Refactor

There's not much to refactor other than refactoring some magic values into named constants.

const finalWord = "Go!"
const countdownStart = 3

func Countdown(out io.Writer) {
    for i := countdownStart; i > 0; i-- {
        fmt.Fprintln(out, i)
    }
    fmt.Fprint(out, finalWord)
}
Enter fullscreen mode Exit fullscreen mode

If you run the program now, you should get the desired output but we dont have it as a dramatic countdown with the 1 second pauses.

Go let's you achieve this with time.Sleep. Try adding it in to our code.

func Countdown(out io.Writer) {
    for i := countdownStart; i > 0; i-- {
        time.Sleep(1 * time.Second)
        fmt.Fprintln(out, i)
    }

    time.Sleep(1 * time.Second)
    fmt.Fprint(out, finalWord)
}
Enter fullscreen mode Exit fullscreen mode

If you run the program it works as we want it to.

Mocking

The tests still pass and the software works as intended but we have some problems:

  • Our tests take 4 seconds to run.
    • Every forward thinking post about software development emphasises the importance of quick feedback loops.
    • Slow tests ruin developer productivity.
    • Imagine if the requirements get more sophisticated warranting more tests. Are we happy with 4s added to the test run for every new test of Countdown?
  • We have not tested an important property of our function.

We have a dependency on Sleeping which we need to extract so we can then control it in our tests.

If we can mock time.Sleep we can use dependency injection to use it instead of a "real" time.Sleep and then we can spy on the calls to make assertions on them.

Write the test first

Let's define our dependency as an interface. This lets us then use a real Sleeper in main and a spy sleeper in our tests. By using an interface our Countdown function is obvlivious to this and adds some flexibility for the caller.

type Sleeper interface {
    Sleep()
}
Enter fullscreen mode Exit fullscreen mode

I made a design decision that our Countdown function would not be responsible
for how long the sleep is. This simplifies our code a little for now at least
and means a user of our function can configure that sleepiness however they
like.

Now we need to make a mock of it for our tests to use.

type SpySleeper struct {
    Calls int
}

func (s *SpySleeper) Sleep() {
    s.Calls++
}
Enter fullscreen mode Exit fullscreen mode

Spies are a kind of mock which can record how a dependency is used. They can record the arguments sent in, how many times, etc. In our case, we're keeping track of how many times Sleep() is called so we can check it in our test.

Update the tests to inject a dependency on our Spy and assert that the sleep has been called 4 times.

func TestCountdown(t *testing.T) {
    buffer := &bytes.Buffer{}
    spySleeper := &SpySleeper{}

    Countdown(buffer, spySleeper)

    got := buffer.String()
    want := `3
2
1
Go!`

    if got != want {
        t.Errorf("got '%s' want '%s'", got, want)
    }

    if spySleeper.Calls != 4 {
        t.Errorf("not enough calls to sleeper, want 4 got %d", spySleeper.Calls)
    }
}
Enter fullscreen mode Exit fullscreen mode

Try and run the test

too many arguments in call to Countdown
    have (*bytes.Buffer, Sleeper)
    want (io.Writer)
Enter fullscreen mode Exit fullscreen mode

Write the minimal amount of code for the test to run and check the failing test output

We need to update Countdown to accept our Sleeper

func Countdown(out io.Writer, sleeper Sleeper) {
    for i := countdownStart; i > 0; i-- {
        time.Sleep(1 * time.Second)
        fmt.Fprintln(out, i)
    }

    time.Sleep(1 * time.Second)
    fmt.Fprint(out, finalWord)
}
Enter fullscreen mode Exit fullscreen mode

If you try again, your main will no longer compile for the same reason

./main.go:26:11: not enough arguments in call to Countdown
    have (*os.File)
    want (io.Writer, Sleeper)
Enter fullscreen mode Exit fullscreen mode

Let's create a real sleeper which implements the interface we need

type ConfigurableSleeper struct {
    duration time.Duration
}

func (o *ConfigurableSleeper) Sleep() {
    time.Sleep(o.duration)
}
Enter fullscreen mode Exit fullscreen mode

I decided to make a little extra effort and make it so our real sleeper is
configurable but you could just as easily not bother and hard-code it for
1 second.

We can then use it in our real application like so

func main() {
    sleeper := &ConfigurableSleeper{1 * time.Second}
    Countdown(os.Stdout, sleeper)
}
Enter fullscreen mode Exit fullscreen mode

Write enough code to make it pass

The test is now compiling but not passing because we're still calling the time.Sleep rather than the injected in dependency. Let's fix that.

func Countdown(out io.Writer, sleeper Sleeper) {
    for i := countdownStart; i > 0; i-- {
        sleeper.sleep()
        fmt.Fprintln(out, i)
    }

    sleeper.sleep()
    fmt.Fprint(out, finalWord)
}
Enter fullscreen mode Exit fullscreen mode

The test should pass and no longer taking 4 seconds.

Still some problems

There's still another important property we haven't tested.

Countdown should sleep before the first print and then after each one until the last, e.g:

  • Sleep
  • Print N
  • Sleep
  • Print N-1
  • Sleep
  • etc

Our latest change only asserts that it has slept 4 times, but those sleeps could occur out of sequence

When writing tests if you're not confident that your tests are giving you sufficient confidence, just break it! (make sure you have commited your changes to source control first though). Change the code to the following

func Countdown(out io.Writer, sleeper Sleeper) {
    for i := countdownStart; i > 0; i-- {
        sleeper.Sleep()
    }

    for i := countdownStart; i > 0; i-- {
        fmt.Fprintln(out, i)
    }

    sleeper.Sleep()
    fmt.Fprint(out, finalWord)
}
Enter fullscreen mode Exit fullscreen mode

If you run your tests they should still be passing even though the implementation is wrong.

Let's use spying again with a new test to check the order of operations is correct.

We have two different dependencies and we want to record all of their operations into one list. So we'll create one spy for them both.

type CountdownOperationsSpy struct {
    Calls []string
}

func (s *CountdownOperationsSpy) Sleep() {
    s.Calls = append(s.Calls, sleep)
}

func (s *CountdownOperationsSpy) Write(p []byte) (n int, err error) {
    s.Calls = append(s.Calls, write)
    return
}

const write = "write"
const sleep = "sleep"
Enter fullscreen mode Exit fullscreen mode

Our CountdownOperationsSpy implements both io.Writer and Sleeper, recording every call into one slice. In this test we're only concerned about the order of operations, so just recording them as list of named operations is sufficient.

We can now add a sub-test into our test suite.

t.Run("sleep after every print", func(t *testing.T) {
    spySleepPrinter := &CountdownOperationsSpy{}
    Countdown(spySleepPrinter, spySleepPrinter)

    want := []string{
        sleep,
        write,
        sleep,
        write,
        sleep,
        write,
        sleep,
        write,
    }

    if !reflect.DeepEqual(want, spySleepPrinter.Calls) {
        t.Errorf("wanted calls %v got %v", want, spySleepPrinter.Calls)
    }
})
Enter fullscreen mode Exit fullscreen mode

This test should now fail. Revert it back and the new test should pass.

We now have two tests spying on the Sleeper so we can now refactor our test so one is testing what is being printed and the other one is ensuring we're sleeping in between the prints. Finally we can delete our first spy as it's not used anymore.

func TestCountdown(t *testing.T) {

    t.Run("prints 3 to Go!", func(t *testing.T) {
        buffer := &bytes.Buffer{}
        Countdown(buffer, &CountdownOperationsSpy{})

        got := buffer.String()
        want := `3
2
1
Go!`

        if got != want {
            t.Errorf("got '%s' want '%s'", got, want)
        }
    })

    t.Run("sleep after every print", func(t *testing.T) {
        spySleepPrinter := &CountdownOperationsSpy{}
        Countdown(spySleepPrinter, spySleepPrinter)

        want := []string{
            sleep,
            write,
            sleep,
            write,
            sleep,
            write,
            sleep,
            write,
        }

        if !reflect.DeepEqual(want, spySleepPrinter.Calls) {
            t.Errorf("wanted calls %v got %v", want, spySleepPrinter.Calls)
        }
    })
}
Enter fullscreen mode Exit fullscreen mode

We now have our function and its 2 important properties properly tested.

But isn't mocking evil?

You may have heard mocking is evil. Just like anything in software development it can be used for evil, just like DRY.

People normally get in to a bad state when they don't listen to their tests and are not respecting the refactoring stage.

If your mocking code is becoming complicated or you are having to mock out lots of things to test something, you should listen to that bad feeling and think about your code. Usually it is a sign of

  • The thing you are testing is having to do too many things.
    • Break the module apart so it does less
  • Its dependencies are too fine-grained
    • Think about how you can consolidate some of these dependencies into one meaningful module.
  • Your test is too concerned with implementation details
    • Favour testing expected behaviour rather than the implementation

Normally a lot of mocking points to bad abstraction in your code.

What people see here is a weakness in TDD but it is actually a strength, more often than not poor test code is a result of bad design or put more nicely, well-designed code is easy to test.

But mocks and tests are still making my life hard!

Ever run into this situation?

  • You want to do some refactoring
  • To do this you end up changing lots of tests
  • You question TDD and make a post on Medium titled "Mocking considered harmful"

This is usually a sign of you testing too much implementation detail. Try to make it so your tests are testing useful behaviour unless the implementation is really important to how the system runs.

It is sometimes hard to know what level to test exactly but here are some thought processes and rules I try to follow

  • The definition of refactoring is that the code changes but the behaviour stays the same. If you have decided to do some refactoring in theory you should be able to do make the commit without any test changes. So when writing a test ask yourself
    • Am i testing the behaviour I want or the implementation details?
    • If i were to refactor this code, would I have to make lots of changes to the tests?
  • Although Go lets you test private functions, I would avoid it as private functions are to do with implementation.
  • I feel like if a test is working with more than 3 mocks then it is a red flag - time for a rethink on the design
  • Use spies with caution. Spies let you see the insides of the algorithm you are writing which can be very useful but that means a tighter coupling between your test code and the implementation. Be sure you actually care about these details if you're going to spy on them

As always, rules in software development aren't really rules and there can be exceptions. Uncle Bob's article of "When to mock" has some excellent pointers.

Wrapping up

More on TDD approach

  • When faced with less trivial examples, break the problem down into "thin vertical slices". Try to get to a point where you have working software backed by tests as soon as you can, to avoid getting in rabbit holes and taking a "big bang" approach.
  • Once you have some working software it should be easier to iterate with small steps until you arrive at the software you need.

Mocking

  • Without mocking important areas of your code will be untested. In our case we would not be able to test that our code paused between each print but there are countless other examples. Calling a service that can fail? Wanting to test your system in a particular state? It is very hard to test these scenarios without mocking.
  • Without mocks you may have to set up databases and other third parties things just to test simple business rules. You're likely to have slow tests, resulting in slow feedback loops.
  • By having to spin up a database or a webservice to test something you're likely to have fragile tests due to the unreliability of such services.

Once a developer learns about mocking it becomes very easy to over-test every single facet of a system in terms of the way it works rather than what it does. Always be mindful about the value of your tests and what impact they would have in future refactoring.

In this post about mocking we have only covered Spies which are a kind of mock. There are different kind of mocks. Uncle Bob explains the types in a very easy to read article. In later chapters we will need to write code that depends on others for data, which is where we will show Stubs in action.

Oldest comments (5)

Collapse
 
onecuteliz profile image
Elizabeth Jenerson

Yes, Chris this IS helpful. I have a little more to learn w TDD but I’m in a good place due to this post. 👍🏾

Collapse
 
biros profile image
Boris Jamot ✊ /

I'm very confused with mocking in Go.

I've been struggling on that topic for a few hours now and what you nicely shows here in your post is not acceptable for me, except for an "hello world!" app.

I can't imagine writing the mocks for each dependency by myself, and above all, I can't imagine modifying my code to replace my dependencies by my interface.

It must be possible with reflection, no?

If you know a way to do that properly like with doubles in PHPUnit, or with mocks in Java/Mockito, then I'm interested!

Anyway, thanks for this great article!

Collapse
 
quii profile image
Chris James

your post is not acceptable for me, except for an "hello world!" app.

Well what can I say, I and many others take this approach for large applications and it's fine.

Writing mocks, especially if you have an IDE that lets you auto implement interfaces at a keystroke is very trivial.

You can also do embedding if you wish to only mock a part of an interface

type MyStub struct {
    MyInterface
}

//todo: impl just the method you need

I'm not sure using reflection is any more "proper". In my experience mockito and the like give you a superficial "easiness" but then run into the usual problems with reflection which are a lack of clarity, type-safety and speed.

Debugging null pointer exceptions with my oh-so-easy mocks in mockito have wasted too much of my life already.

There are auto mocking libraries out there for Go of course, but I steer clear of them. I think GoMock is used by some.

Think about why you need to set up so many mocks. Are you over-testing things? Is the design of your system right?

Collapse
 
biros profile image
Boris Jamot ✊ /

You may be pointing out something interesting: maybe I am over-testing things, sometimes.
I love this concept in Go, where you craft many things by yourself.
This is achieved thanks to the great standard library.
That's why no framework emerged, unlike Laravel in PHP or Spring in Java.

In PHP, I was used to unit test every single part of my code, by mocking the sub-dependencies of my functions, and by spying calls.

By the time, it always become a kind of hell to maintain.

Maybe in Go, I could be a little less exhaustive in unit testing, and rely more on the strong typing. I mean, testing the inputs & outputs of my web APIs should be enough most of the time. But in some cases, I still need to check that data are written in db or that a log in sent to stdout because we have dashboards that consume it and this is critical for the business.

Thank you for your answer, it helped me to better understand what I really need.

Thread Thread
 
quii profile image
Chris James

Static typing certainly removes a lot of the tests you would need in a dynamic language but you still benefit from tests

I allude to this in the book in various chapters about how you should favour testing behaviour over implementation detail. It's a myth that unit testing means "tests on classes/functions/whatever" You write unit tests on behaviour, that could be a class which has a number of internal collaborators. You dont have to test them. In fact by testing them you may harm your efforts if you wish to change the implementation (refactoring)

i plan to do a talk/blog on this subject in more detail... sometime when i get the time.