Home Go: Mocking
Post
Cancel

Go: Mocking

Mocking in Go

Mocking is a technique that uses mock objects to test code with external dependencies when writing tests in Go. External dependencies refer to the use of external resources or external APIs, such as file systems, databases, and web services.

External Dependencies

These external dependencies can cause problems when writing tests. For example, a test environment may not have access to external resources or may need to use arbitrary data at test time. External API calls can also cause issues with longer test execution times.

To solve this problem, mocking provides a way to replace external dependencies using fake objects. This fake object has the same interface as the real object, so it can be called and interacted with in the same way as the real object. Mocking creates fake objects that mimic real external dependencies at test time, and you can use them to test your code.

Mocking requires an understanding of dependency injections.

Code Usage

Say we need a program that counts down from 3 and shouts Go!

We would want this function to accomplish three tasks:

  • Print 3
  • Print 3, 2, 1 and Go!
  • Wait a second between each line

TDD

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func TestCountdown(t *testing.T) {
	buffer := &bytes.Buffer{}

	Countdown(buffer)

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

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

Write the test first, as we are using TDD afterall.

1
2
3
4
5
6
7
8
func Countdown(out io.Writer) {
	for i := countdownStart; i > 0; i-- {
		fmt.Fprintln(out, i)
		time.Sleep(1 * time.Second)
	}

	fmt.Fprint(out, finalWord)
}

Now with the function and test ready, it simply checks if we print out all the required components right. However, there are a few problems that we are failing to see.

First of all, this will make it very slow to test. Because of the sleep requirements, the test will wait 3 seconds and eventually lead to reduced development efficiency, and if the test gradually expands, it may take longer than just 3 seconds.

Another big chunk we are missing is that, the test coverage is very low. We do not check if the function has properly sleeped in between. So what we know up until now:

  • Countdown has a dependency on Sleep

The amazing thing about mockign is that if you mock time.Sleep, you can use dependency injection to create Spy on the calls.

Spies

1
2
3
4
5
6
7
8
9
10
11
type Sleeper interface {
	Sleep()
}

type SpySleeper struct {
	Calls int
}

func (s *SpySleeper) Sleep() {
	s.Calls++
}

In our context, spies are a kind of mock that records how dependencies are used. Here, spy is specified so that the number of times sleep is called can be tracked. This way, actual time sleep is performed when counting down in real applications, but in tests, only the number of sleep calls is tracked using mocks, and more efficient and faster tests are pursued.

Additional testing

In addition, we want to check whether the sleep and write operations are called upon alternately, in the proper order.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
func TestCountdown(t *testing.T) {

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

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

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

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

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

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

So we add a test, creating an array that maintains this order.

Lastly, we also want to see if the sleeping time is configurable so we add another test for that.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type ConfigurableSleeper struct {
	duration time.Duration // configure the time slept
	sleep    func(time.Duration) // pass in a sleep function
}

func (c *ConfigurableSleeper) Sleep() {
	c.sleep(c.duration)
}

type SpyTime struct {
	durationSlept time.Duration
}

func (s *SpyTime) Sleep(duration time.Duration) {
	s.durationSlept = duration
}

With the sleeper and duration defined above, we can create a new test for the configurable sleeper.

1
2
3
4
5
6
7
8
9
10
11
func TestConfigurableSleeper(t *testing.T) {
	sleepTime := 5 * time.Second

	spyTime := &SpyTime{}
	sleeper := ConfigurableSleeper{sleepTime, spyTime.Sleep}
	sleeper.Sleep()

	if spyTime.durationSlept != sleepTime {
		t.Errorf("should have slept for %v but slept for %v", sleepTime, spyTime.durationSlept)
	}
}

We see here once again, that the main application runs with the default time sleeper that actually adds the pause in between the scripts.

1
2
3
4
func main() {
	sleeper := &ConfigurableSleeper{1 * time.Second, time.Sleep}
	Countdown(os.Stdout, sleeper)
}

However, the main takeaway here is that by using mock, you can see that the core behavior remains unchanged the entire time while maintaining the efficiency of the many tests.

Strengths of mocking

The advantages of using mocking are,

  • Test coverage can be increased even up to parts of the function that are not covered.
  • Test are available for functions that cause system failure when called by using mocks.
  • Quickly and efficient tests are available even for external resources that need to be established, such as establishing a db connection.

Drawbacks

However if mocking is abused, the test code becomes unmanageable, difficult to understand, as it is not a real object and unreliable.

Using more than 5 mock objects are often referenced as being bad practice.

Finale

Go supports mocking to make mocks more automated and convenient to use. This post was influenced by Learn Go with tests.

This post is licensed under CC BY 4.0 by the author.

Go: Dependency Injection

Go: Select for concurrency