Skip to content

Commit

Permalink
Merge pull request quii#67 from hackeryarn/sleeper-tests
Browse files Browse the repository at this point in the history
Sleeper tests
  • Loading branch information
quii authored May 21, 2018
2 parents 29d6865 + 4daae70 commit 24d8d9d
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 22 deletions.
104 changes: 96 additions & 8 deletions mocking.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,22 +315,18 @@ If you try again, your `main` will no longer compile for the same reason
Let's create a _real_ sleeper which implements the interface we need

```go
type ConfigurableSleeper struct {
duration time.Duration
}
type DefaultSleeper struct {}

func (o *ConfigurableSleeper) Sleep() {
time.Sleep(o.duration)
func (d *DefaultSleeper) Sleep() {
time.Sleep(1 * time.Second)
}
```

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

```go
func main() {
sleeper := &ConfigurableSleeper{1 * time.Second}
sleeper := &DefaultSleeper{}
Countdown(os.Stdout, sleeper)
}
```
Expand Down Expand Up @@ -481,6 +477,98 @@ Go!`

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

## Extending Sleeper to be configurable

A nice feature would be for the `Sleeper` to be configurable.

### Write the test first

Let's first create a new type for `ConfigurableSleeper` that accepts what we need for configuration and testing.

```go
type ConfigurableSleeper struct {
duration time.Duration
sleep func(time.Duration)
}
```

We are using `duration` to configure the time slept and `sleep` as a way to pass in a sleep function. The signature of `sleep` is the same as for `time.Sleep` allowing us to use `time.Sleep` in our real implementation and a spy in our tests.

```go
type SpyTime struct {
durationSlept time.Duration
}

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

With our spy in place, we can create a new test for the configurable sleeper.

```go
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)
}
}
```

There should be nothing new in this test and it is setup very similar to the previous mock tests.

### Try and run the test
```
sleeper.Sleep undefined (type ConfigurableSleeper has no field or method Sleep, but does have sleep)
```

You should see a very clear error message indicating that we do not have a `Sleep` method created on our `ConfigurableSleeper`.

### Write the minimal amount of code for the test to run and check failing test output
```go
func (c *ConfigurableSleeper) Sleep() {
}
```

With our new `Sleep` function implemented we have a failing test.

```
countdown_test.go:56: should have slept for 5s but slept for 0s
```

### Write enough code to make it pass

All we need to do now is implement the `Sleep` function for `ConfigurableSleeper`.

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

With this change all of the test should be passing again.

### Cleanup and refactor

The last thing we need to do is to actually use our `ConfigurableSleeper` in the main function.

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

If we run the tests and the program manually, we can see that all the behavior remains the same.

Since we are using the `ConfigurableSleeper`, it is safe to delete the `DefaultSleeper` implementation. Wrapping up our program.

## 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](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself).
Expand Down
12 changes: 5 additions & 7 deletions mocking/v3/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,12 @@ type Sleeper interface {
Sleep()
}

// ConfigurableSleeper is an implementation of Sleeper with a defined delay
type ConfigurableSleeper struct {
Duration time.Duration
}
// DefaultSleeper is an implementation of Sleeper with a predefined delay
type DefaultSleeper struct{}

// Sleep will pause execution for the defined Duration
func (o *ConfigurableSleeper) Sleep() {
time.Sleep(o.Duration)
func (d *DefaultSleeper) Sleep() {
time.Sleep(1 * time.Second)
}

const finalWord = "Go!"
Expand All @@ -37,6 +35,6 @@ func Countdown(out io.Writer, sleeper Sleeper) {
}

func main() {
sleeper := &ConfigurableSleeper{1 * time.Second}
sleeper := &DefaultSleeper{}
Countdown(os.Stdout, sleeper)
}
12 changes: 5 additions & 7 deletions mocking/v4/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,12 @@ type Sleeper interface {
Sleep()
}

// ConfigurableSleeper is an implementation of Sleeper with a defined delay
type ConfigurableSleeper struct {
Duration time.Duration
}
// DefaultSleeper is an implementation of Sleeper with a predefined delay
type DefaultSleeper struct{}

// Sleep will pause execution for the defined Duration
func (o *ConfigurableSleeper) Sleep() {
time.Sleep(o.Duration)
func (d *DefaultSleeper) Sleep() {
time.Sleep(1 * time.Second)
}

const finalWord = "Go!"
Expand All @@ -38,6 +36,6 @@ func Countdown(out io.Writer, sleeper Sleeper) {
}

func main() {
sleeper := &ConfigurableSleeper{1 * time.Second}
sleeper := &DefaultSleeper{}
Countdown(os.Stdout, sleeper)
}
82 changes: 82 additions & 0 deletions mocking/v5/countdown_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package main

import (
"bytes"
"reflect"
"testing"
"time"
)

func TestCountdown(t *testing.T) {

t.Run("prints 5 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)
}
})
}

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)
}
}

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"

type SpyTime struct {
durationSlept time.Duration
}

func (s *SpyTime) Sleep(duration time.Duration) {
s.durationSlept = duration
}
44 changes: 44 additions & 0 deletions mocking/v5/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package main

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

// Sleeper allows you to put delays
type Sleeper interface {
Sleep()
}

// ConfigurableSleeper is an implementation of Sleeper with a defined delay
type ConfigurableSleeper struct {
duration time.Duration
sleep func(time.Duration)
}

// Sleep will pause execution for the defined Duration
func (c *ConfigurableSleeper) Sleep() {
c.sleep(c.duration)
}

const finalWord = "Go!"
const countdownStart = 3

// Countdown prints a countdown from 5 to out with a delay between count provided by Sleeper
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)
}

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

0 comments on commit 24d8d9d

Please sign in to comment.