├── go.mod ├── .gitignore ├── cpu └── cpu.go ├── gpu └── gpu.go ├── main.go ├── memory └── memory.go ├── LICENSE ├── computer ├── macbook.go └── macbook_test.go ├── go.sum └── README.md /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jurabek/table-mock-sample 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/stretchr/objx v0.3.0 // indirect 7 | github.com/stretchr/testify v1.7.0 8 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 9 | ) 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /cpu/cpu.go: -------------------------------------------------------------------------------- 1 | package cpu 2 | 3 | import ( 4 | "log" 5 | "math/rand" 6 | "time" 7 | ) 8 | 9 | type CPU interface { 10 | Usage() int // percentage of CPU usage 11 | } 12 | 13 | type Intel struct { 14 | } 15 | 16 | func (i *Intel) Usage() int { 17 | rand.Seed(time.Now().UnixNano()) 18 | usage := rand.Intn(100) 19 | log.Printf("CPU Rand usage: %v", usage) 20 | return usage 21 | } 22 | -------------------------------------------------------------------------------- /gpu/gpu.go: -------------------------------------------------------------------------------- 1 | package gpu 2 | 3 | import ( 4 | "log" 5 | "math/rand" 6 | "time" 7 | ) 8 | 9 | type GPU interface { 10 | Usage() int // percentage of the GPU 11 | } 12 | 13 | type Nvidia struct { 14 | } 15 | 16 | func (g *Nvidia) Usage() int { 17 | rand.Seed(time.Now().UnixNano()) 18 | usage := rand.Intn(100) 19 | log.Printf("GPU Rand usage: %v", usage) 20 | return usage 21 | } 22 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/jurabek/table-mock-sample/computer" 5 | "github.com/jurabek/table-mock-sample/cpu" 6 | "github.com/jurabek/table-mock-sample/gpu" 7 | "github.com/jurabek/table-mock-sample/memory" 8 | ) 9 | 10 | func main() { 11 | intel := &cpu.Intel{} 12 | nvidia := &gpu.Nvidia{} 13 | samsung := &memory.Samsung{} 14 | _ = computer.NewMacBook(intel, nvidia, samsung) 15 | } 16 | -------------------------------------------------------------------------------- /memory/memory.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "log" 5 | "math/rand" 6 | "time" 7 | ) 8 | 9 | type RAM interface { 10 | FreeMemory() int 11 | } 12 | 13 | type Samsung struct { 14 | } 15 | 16 | func (s *Samsung) FreeMemory() int { 17 | rand.Seed(time.Now().UnixNano()) 18 | max := 1024 * 8 19 | min := 1024 * 3 20 | usage := rand.Intn(max-min) + min 21 | log.Printf("Rand usage of RAM: %v", usage) 22 | return max - usage 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jurabek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /computer/macbook.go: -------------------------------------------------------------------------------- 1 | package computer 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/jurabek/table-mock-sample/cpu" 7 | "github.com/jurabek/table-mock-sample/gpu" 8 | "github.com/jurabek/table-mock-sample/memory" 9 | ) 10 | 11 | var CpuUtilizationError = errors.New("cpu utilized more than expected") 12 | var MemoryUsageError = errors.New("not enough memory") 13 | var GpuUsageError = errors.New("gpu usage error") 14 | 15 | type Diagnoseable interface { 16 | Diagnose(cpuThreshold, gpuThreshold, memoryThreshold int) error 17 | } 18 | 19 | type MacBook struct { 20 | cpu cpu.CPU 21 | gpu gpu.GPU 22 | memory memory.RAM 23 | } 24 | 25 | func NewMacBook(cpu cpu.CPU, gpu gpu.GPU, memory memory.RAM) *MacBook { 26 | return &MacBook{ 27 | cpu: cpu, 28 | gpu: gpu, 29 | memory: memory, 30 | } 31 | } 32 | 33 | func (m *MacBook) Diagnose(cpuThreshold, gpuThreshold, memoryThreshold int) error { 34 | if (m.cpu.Usage()) > cpuThreshold { 35 | return CpuUtilizationError 36 | } 37 | 38 | if m.gpu.Usage() > gpuThreshold { 39 | return GpuUsageError 40 | } 41 | 42 | if m.memory.FreeMemory() <= memoryThreshold { 43 | return MemoryUsageError 44 | } 45 | 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 8 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 9 | github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As= 10 | github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 11 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 12 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 13 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 14 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 15 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 16 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 17 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 18 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # golang-table-tests-with-testify-mock 2 | This repo shows how to test and mock multiple dependencies using table-driven tests with testify-mock in Golang 3 | 4 | 5 | ## Problem 6 | In unit testing and mocking world we need to mock multiple dependencies for specific test cases, (e.g) imagine we have a `struct MacBook` and it depends on `CPU`, `GPU` and `RAM` and we have a `Diagnose` method that returns an error if some of the dependencies `used resources` hits the threshold. 7 | ``` 8 | type MacBook struct { 9 | cpu cpu.CPU 10 | gpu gpu.GPU 11 | memory memory.RAM 12 | } 13 | 14 | func (m *MacBook) Diagnose(cpuThreshold, gpuThreshold, memoryThreshold int) error { 15 | if (m.cpu.Usage()) > cpuThreshold { 16 | return CpuUtilizationError 17 | } 18 | 19 | if m.gpu.Usage() > gpuThreshold { 20 | return GpuUsageError 21 | } 22 | 23 | if m.memory.FreeMemory() <= memoryThreshold { 24 | return MemoryUsageError 25 | } 26 | 27 | return nil 28 | } 29 | ``` 30 | example above we have 3 if conditions, in order to test and get happy result that returns no error, we should have to write at least 4 unit tests or more and mock all dependencies. 31 | 32 | ## Solution 33 | ``` 34 | func TestMacbookDiagnose(t *testing.T) { 35 | type depFields struct { 36 | cpu *mockedCPU 37 | gpu *mockedGPU 38 | memory *mockedRAM 39 | } 40 | 41 | type args struct { 42 | cpuThreshold, gpuThreshold, memoryThreshold int 43 | } 44 | 45 | tests := []struct { 46 | name string 47 | in *args 48 | out error 49 | 50 | on func(*depFields) 51 | assert func(*depFields) 52 | }{ 53 | 54 | // Test cases... 55 | 56 | { 57 | name: "when all thresholds not hit return nil", 58 | in: &args{50, 90, 1000}, 59 | out: nil, 60 | on: func(df *depFields) { 61 | df.cpu.On("Usage").Return(40) // 40% CPU usage less than cpuThreshold 62 | df.gpu.On("Usage").Return(50) // 50% gpu usage less than gpuThreshold 63 | df.memory.On("FreeMemory").Return(2000) // 2000 MB free memory left so it is larger than 1000 mb threshold 64 | }, 65 | assert: func(t *testing.T, df *depFields) { 66 | df.cpu.AssertNumberOfCalls(t, "Usage", 1) 67 | df.gpu.AssertNumberOfCalls(t, "Usage", 1) 68 | df.memory.AssertNumberOfCalls(t, "FreeMemory", 1) 69 | }, 70 | }, 71 | } 72 | ``` 73 | basically in the table-driven testing we will define slice of structs that represents test cases, in the example above we have struct that contains callback functions `on: func(*depFields)` and `assert: func(*testing.T, *depFields)` when we call those methods `type depFields struct{}` will be passed into it in order to `mock` or `assert` 74 | ``` 75 | for _, tt := range tests { 76 | t.Run(tt.name, func(t *testing.T) { 77 | // arrange 78 | f := &depFields{ 79 | &mockedCPU{}, 80 | &mockedGPU{}, 81 | &mockedRAM{}, 82 | } 83 | mb := NewMacBook(f.cpu, f.gpu, f.memory) 84 | if tt.on != nil { 85 | tt.on(f) 86 | } 87 | // act 88 | err := mb.Diagnose(tt.in.cpuThreshold, tt.in.gpuThreshold, tt.in.memoryThreshold) 89 | 90 | // assert 91 | if err != tt.out { 92 | t.Errorf("got %v, want %v", err, tt.out) 93 | } 94 | if tt.assert != nil { 95 | tt.assert(t, f) 96 | } 97 | }) 98 | } 99 | ``` 100 | 101 | example above we are iterating all test cases and running sub tests, on each case we creating `f := &depFields{}` of mocks and passing it onto `on` function to prepare mocks, at the end of the test we also checking `assertion` of mock calls, this helps us to assert number of calls that mocked methods are called inside the `Diagnose` method. 102 | 103 | ## Links to read 104 | [Table-Driven tests in Go](https://github.com/golang/go/wiki/TableDrivenTests) 105 | 106 | [Testify Mocking](https://github.com/stretchr/testify#mock-package) 107 | -------------------------------------------------------------------------------- /computer/macbook_test.go: -------------------------------------------------------------------------------- 1 | package computer 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/mock" 7 | ) 8 | 9 | type mockedCPU struct { 10 | mock.Mock 11 | } 12 | 13 | func (m *mockedCPU) Usage() int { 14 | args := m.Called() 15 | return args.Get(0).(int) 16 | } 17 | 18 | type mockedGPU struct { 19 | mock.Mock 20 | } 21 | 22 | func (m *mockedGPU) Usage() int { 23 | args := m.Called() 24 | return args.Get(0).(int) 25 | } 26 | 27 | type mockedRAM struct { 28 | mock.Mock 29 | } 30 | 31 | func (m *mockedRAM) FreeMemory() int { 32 | args := m.Called() 33 | return args.Get(0).(int) 34 | } 35 | 36 | func TestMacbook(t *testing.T) { 37 | type depFields struct { 38 | cpu *mockedCPU 39 | gpu *mockedGPU 40 | memory *mockedRAM 41 | } 42 | 43 | type args struct { 44 | cpuThreshold, gpuThreshold, memoryThreshold int 45 | } 46 | 47 | tests := []struct { 48 | name string 49 | in *args 50 | out error 51 | 52 | on func(*depFields) 53 | assert func(*testing.T, *depFields) 54 | }{ 55 | { 56 | name: "when CPU usage larger than CPU threshold diagnose return CpuUtilizationError", 57 | in: &args{50, 60, 1000}, 58 | out: CpuUtilizationError, 59 | on: func(df *depFields) { 60 | df.cpu.On("Usage").Return(60) // 60% CPU usage 61 | }, 62 | assert: func(t *testing.T, df *depFields) { 63 | df.cpu.AssertNumberOfCalls(t, "Usage", 1) 64 | }, 65 | }, 66 | { 67 | name: "when GPU usage larger than GPU threshold diagnose return GpuUsageError", 68 | in: &args{50, 90, 1000}, 69 | out: GpuUsageError, 70 | on: func(df *depFields) { 71 | df.cpu.On("Usage").Return(40) // 40% CPU usage less than cpuThreshold 72 | df.gpu.On("Usage").Return(95) // 95% gpu usage larger than gpuThreshold 73 | }, 74 | assert: func(t *testing.T, df *depFields) { 75 | df.cpu.AssertNumberOfCalls(t, "Usage", 1) 76 | df.gpu.AssertNumberOfCalls(t, "Usage", 1) 77 | }, 78 | }, 79 | 80 | { 81 | name: "when Free memory less than memory threshold diagnose return MemoryUsageError", 82 | in: &args{50, 90, 1000}, 83 | out: MemoryUsageError, 84 | on: func(df *depFields) { 85 | df.cpu.On("Usage").Return(40) // 40% CPU usage less than cpuThreshold 86 | df.gpu.On("Usage").Return(50) // 50% gpu usage less than gpuThreshold 87 | df.memory.On("FreeMemory").Return(900) // 900 MB free memory left so it is less than 1000 mb threshold 88 | }, 89 | assert: func(t *testing.T, df *depFields) { 90 | df.cpu.AssertNumberOfCalls(t, "Usage", 1) 91 | df.gpu.AssertNumberOfCalls(t, "Usage", 1) 92 | df.memory.AssertNumberOfCalls(t, "FreeMemory", 1) 93 | }, 94 | }, 95 | 96 | { 97 | name: "when all thresholds not hit return nil", 98 | in: &args{50, 90, 1000}, 99 | out: nil, 100 | on: func(df *depFields) { 101 | df.cpu.On("Usage").Return(40) // 40% CPU usage less than cpuThreshold 102 | df.gpu.On("Usage").Return(50) // 50% gpu usage less than gpuThreshold 103 | df.memory.On("FreeMemory").Return(2000) // 2000 MB free memory left so it is larger than 1000 mb threshold 104 | }, 105 | assert: func(t *testing.T, df *depFields) { 106 | df.cpu.AssertNumberOfCalls(t, "Usage", 1) 107 | df.gpu.AssertNumberOfCalls(t, "Usage", 1) 108 | df.memory.AssertNumberOfCalls(t, "FreeMemory", 1) 109 | }, 110 | }, 111 | } 112 | 113 | for _, tt := range tests { 114 | t.Run(tt.name, func(t *testing.T) { 115 | // arrange 116 | f := &depFields{ 117 | &mockedCPU{}, 118 | &mockedGPU{}, 119 | &mockedRAM{}, 120 | } 121 | mb := NewMacBook(f.cpu, f.gpu, f.memory) 122 | if tt.on != nil { 123 | tt.on(f) 124 | } 125 | // act 126 | err := mb.Diagnose(tt.in.cpuThreshold, tt.in.gpuThreshold, tt.in.memoryThreshold) 127 | 128 | // assert 129 | if err != tt.out { 130 | t.Errorf("got %v, want %v", err, tt.out) 131 | } 132 | if tt.assert != nil { 133 | tt.assert(t, f) 134 | } 135 | }) 136 | } 137 | } 138 | --------------------------------------------------------------------------------