-
Notifications
You must be signed in to change notification settings - Fork 3
/
tf.go
274 lines (247 loc) · 6.89 KB
/
tf.go
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
// Package tf is a microframework for parametrized testing of functions.
//
// I wrote this because I was tired of creating a []struct{} fixture for most of
// my tests. I knew there had to be an easier and more reliable way.
//
// It offers a simple and intuitive syntax for tests by wrapping the function:
//
// // Remainder returns the quotient and remainder from dividing two integers.
// func Remainder(a, b int) (int, int) {
// return a / b, a % b
// }
//
// func TestRemainder(t *testing.T) {
// Remainder := tf.Function(t, Remainder)
//
// Remainder(10, 3).Returns(3, 1)
// Remainder(10, 2).Returns(5, 0)
// Remainder(17, 7).Returns(2, 3)
// }
//
// Assertions are performed with github.com/stretchr/testify/assert. If an
// assertion fails it will point to the correct line so you do not need to
// explicitly label tests.
//
// The above test will output (in verbose mode):
//
// === RUN TestRemainder
// --- PASS: TestRemainder (0.00s)
// === RUN TestRemainder/Remainder#1
// --- PASS: TestRemainder/Remainder#1 (0.00s)
// === RUN TestRemainder/Remainder#2
// --- PASS: TestRemainder/Remainder#2 (0.00s)
// === RUN TestRemainder/Remainder#3
// --- PASS: TestRemainder/Remainder#3 (0.00s)
// PASS
//
package tf
import (
"fmt"
"reflect"
"runtime"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
type (
// F wrapper around a func which handles testing instance, agrs and reveals
// function name
F struct {
t *testing.T
fn interface{}
args []interface{}
fnArgsIn []reflect.Type
fnArgsOut []reflect.Type
fnName string
}
handleFunc func(t *testing.T, expected []interface{}, actual []interface{})
)
var (
funcMap = map[string]int{}
)
// Returns matches if expected result matches actual
//
// Remainder := tf.Function(t, func(a,b int) int { return a + b })
// Remainder(1, 2).Returns(3)
//
func (f *F) Returns(expected ...interface{}) {
f.runFunc(func(t *testing.T, expected []interface{}, actual []interface{}) {
assert.Equal(t, expected, actual)
}, expected...)
}
// Errors check if function returns errors and match expectation you provided
//
// BasicErrorer := tf.Function(t, func() error { return errors.New("some error") } )
// BasicErrorer().Errors()
//
// You also can provide strings to match message
//
// AdvancedErrorer := tf.Function(t, func() error { return errors.New("some error") } )
// AdvancedErrorer().Errors("some error")
//
// Or you may provide your custom error type to check it bumps correctly
//
// custom := MyCustomError{errors.New("some error")}
// TypeErrorer := tf.Function(t, func() error { return custom } )
// TypeErrorer().Errors(custom)
//
func (f *F) Errors(args ...interface{}) {
f.runFunc(func(t *testing.T, expected []interface{}, actual []interface{}) {
if len(actual) == 0 {
assert.Fail(t, "function don't return anything")
return
}
// Grab last argument it should be an error
last := actual[len(actual)-1]
err, ok := last.(error)
if !ok {
assert.Fail(t, "last argument is not an error")
return
}
assert.Error(t, err)
if args == nil {
return
}
// Checking what args we are provided
firstArg := args[0]
switch firstArg.(type) {
case string:
// Check error message
assert.Equal(t, firstArg.(string), err.Error())
case error:
assert.Equal(t, firstArg.(error).Error(), err.Error())
assert.IsType(t, firstArg, last)
default:
assert.Fail(t, "unknown providen expectation")
}
})
}
func (f *F) runFunc(h handleFunc, expected ...interface{}) {
if _, ok := funcMap[f.fnName]; !ok {
funcMap[f.fnName] = 0
}
funcMap[f.fnName]++
f.t.Run(fmt.Sprintf("%s#%d", f.fnName, funcMap[f.fnName]), func(t *testing.T) {
// Casting calling arguments
argsIn := make([]reflect.Value, len(f.args))
for idx, arg := range f.args {
if arg == nil {
argsIn[idx] = reflect.Zero(f.fnArgsIn[idx])
} else {
argsIn[idx] = reflect.ValueOf(arg).Convert(f.fnArgsIn[idx])
}
}
returns := make([]interface{}, len(f.fnArgsOut))
for idx, r := range reflect.ValueOf(f.fn).Call(argsIn) {
returns[idx] = r.Interface()
}
for idx, e := range expected {
if e == nil {
expected[idx] = reflect.Zero(f.fnArgsOut[idx]).Interface()
} else {
expected[idx] = reflect.ValueOf(e).Convert(f.fnArgsOut[idx]).Interface()
}
}
h(t, expected, returns)
})
}
// True matches is function returns true as a result
//
// func Switch() bool {
// return true
// }
//
// func TestSwitch(t *testing.T) {
// Switch := tf.Function(t, Switch)
//
// Switch().True()
// }
//
func (f *F) True() {
f.Returns(true)
}
// False matches is function returns false as a result
//
// func Switch() bool {
// return false
// }
//
// func TestSwitch(t *testing.T) {
// Switch := tf.Function(t, Switch)
//
// Switch().False()
// }
//
func (f *F) False() {
f.Returns(false)
}
func getFunctionName(fn interface{}) string {
name := runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name()
parts := strings.Split(name, ".")
return parts[len(parts)-1]
}
func getFunctionArgs(fn interface{}) []reflect.Type {
ref := reflect.ValueOf(fn).Type()
argsCount := ref.NumIn()
args := make([]reflect.Type, argsCount)
for i := 0; i < argsCount; i++ {
args[i] = ref.In(i)
}
return args
}
func getFunctionReturns(fn interface{}) []reflect.Type {
ref := reflect.ValueOf(fn).Type()
argsCount := ref.NumOut()
args := make([]reflect.Type, argsCount)
for i := 0; i < argsCount; i++ {
args[i] = ref.Out(i)
}
return args
}
// Function wraps fn into F testing type and returns back function to which you
// can use as a regular function in e.g:
//
// // Remainder returns the quotient and remainder from dividing two integers.
// func Remainder(a, b int) (int, int) {
// return a / b, a % b
// }
//
// func TestRemainder(t *testing.T) {
// Remainder := tf.Function(t, Remainder)
//
// Remainder(10, 3).Returns(3, 1)
// Remainder(10, 2).Returns(5, 0)
// Remainder(17, 7).Returns(2, 3)
// }
//
func Function(t *testing.T, fn interface{}) func(args ...interface{}) *F {
return NamedFunction(t, getFunctionName(fn), fn)
}
// NamedFunction works the same way as Function but allows a custom name for the
// function to be set.
//
// This is especially important when the function is anonymous, or it can be
// used for grouping tests:
//
// Sum := tf.NamedFunction(t, "Sum1", Item.Add)
//
// Sum(Item{1.3, 4.5}, 3.4).Returns(9.2)
// Sum(Item{1.3, 4.6}, 3.5).Returns(9.4)
//
// Sum = tf.NamedFunction(t, "Sum2", Item.Add)
//
// Sum(Item{1.3, 14.5}, 3.4).Returns(19.2)
// Sum(Item{21.3, 4.6}, 3.5).Returns(29.4)
//
func NamedFunction(t *testing.T, fnName string, fn interface{}) func(args ...interface{}) *F {
return func(args ...interface{}) *F {
return &F{
t: t,
fn: fn,
args: args,
fnArgsIn: getFunctionArgs(fn),
fnArgsOut: getFunctionReturns(fn),
fnName: fnName,
}
}
}