-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy pathVar.go
209 lines (188 loc) · 7.77 KB
/
Var.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
package testcase
import (
"fmt"
)
// Var is a testCase helper structure, that allows easy way to access testCase runtime variables.
// In the future it will be updated to use Go2 type parameters.
//
// Var allows creating testCase variables in a modular way.
// By modular, imagine that you can have commonly used values initialized and then access it from the testCase runtime spec.
// This approach allows an easy dependency injection maintenance at project level for your testing suite.
// It also allows you to have parallel testCase execution where you don't expect side effect from your subject.
//
// e.g.: HTTP JSON API testCase and GraphQL testCase both use the business rule instances.
// Or multiple business rules use the same storage dependency.
//
// The last use-case it allows is to define dependencies for your testCase subject before actually assigning values to it.
// Then you can focus on building up the testing spec and assign values to the variables at the right testing subcontext. With variables, it is easy to forget to assign a value to a variable or forgot to clean up the value of the previous run and then scratch the head during debugging.
// If you forgot to set a value to the variable in testcase, it warns you that this value is not yet defined to the current testing scope.
type Var[V any] struct {
// ID is the testCase spec variable group from where the cached value can be accessed later on.
// ID is Mandatory when you create a variable, else the empty string will be used as the variable group.
ID VarID
// Init is an optional constructor definition that will be used when Var is bonded to a *Spec without constructor function passed to the Let function.
// The goal of this field to initialize a variable that can be reused across different testing suites by bounding the Var to a given testing suite.
//
// Please use #Get if you wish to access a testCase runtime across cached variable value.
// The value returned by this is not subject to any #Before and #Around hook that might mutate the variable value during the testCase runtime.
// Init function doesn't cache the value in the testCase runtime spec but literally just meant to initialize a value for the Var in a given test case.
// Please use it with caution.
Init VarInit[V]
// Before is a hook that will be executed once during the lifetime of tests that uses the Var.
// If the Var is not bound to the Spec at Spec.Context level, the Before Hook will be executed at Var.Get.
Before func(t *T, v Var[V])
// OnLet is an optional Var hook that is executed when the variable being bind to Spec context.
// This hook is ideal to set up tags on the Spec, call Spec.Sequential
// or ensure binding of further dependencies that this variable requires.
//
// In case OnLet is provided, the Var must be explicitly set to a Spec with a Let call
// else accessing the Var value will panic and warn about this.
OnLet func(s *Spec, v Var[V])
// Deps allow you to define a list of Var, that this current Var is depending on.
// If any of the Vars in the dependency list has OnLet set, binding them will be possible by binding our Var.
// Deps make it convenient to describe the dependency graph of our Var without the need to use OnLet + Bind.
// This is especially ideal if we want to use adhoc variable loading in our test,
Deps Vars
}
type VarID string
type Vars []tetcaseVar
type tetcaseVar interface {
isTestcaseVar()
id() VarID
get(t *T) any
bind(s *Spec)
}
func (Var[V]) isTestcaseVar() {}
func (v Var[V]) id() VarID { return v.ID }
type VarInit[V any] func(*T) V
//func CastToVarInit[V any](fn func(testing.TB) V) func(*T) V {
// if fn == nil {
// return nil
// }
// return func(t *T) V { return fn(t) }
//}
const (
varOnLetNotInitialized = `%s Var has Var.OnLet. You must use Var.Let, Var.LetValue to initialize it properly.`
varIDIsIsMissing = `ID for %T is missing. Maybe it's uninitialized?`
)
// Get returns the current cached value of the given Variable
// Get is a thread safe operation.
// When Go2 released, it will replace type casting
func (v Var[V]) Get(t *T) V {
t.Helper()
val, _ := v.get(t).(V)
return val
}
func (v Var[V]) get(t *T) any {
defer t.pauseTimer()()
t.Helper()
if v.ID == "" {
t.Fatalf(varIDIsIsMissing, v)
}
if v.OnLet != nil && !t.hasOnLetHookApplied(v.ID) {
t.Fatalf(varOnLetNotInitialized, v.ID)
}
v.initDeps(t)
v.execBefore(t)
if !t.vars.Knows(v.ID) && v.Init != nil {
t.vars.Let(v.ID, func(t *T) interface{} { return v.Init(t) })
}
rv, ok := t.vars.Get(t, v.ID).(V)
if !ok && t.vars.Get(t, v.ID) != nil {
t.Logf("Incorrect value type for Var.ID: %q", v.ID)
t.Log("If you use .Var type without the .Let helper method")
t.Log("then please make sure that the Var.ID field is unique between your Var instances.")
t.Logf("expected: %T", *new(V))
t.Logf("actual: %T", t.vars.Get(t, v.ID))
t.FailNow()
}
return rv
}
// Set sets a value to a given variable during testCase runtime
// Set is a thread safe operation.
func (v Var[V]) Set(t *T, value V) {
t.Helper()
if v.OnLet != nil && !t.hasOnLetHookApplied(v.ID) {
t.Fatalf(varOnLetNotInitialized, v.ID)
}
t.vars.Set(v.ID, value)
}
// Let allow you to set the variable value to a given spec
func (v Var[V]) Let(s *Spec, blk VarInit[V]) Var[V] {
helper(s.testingTB).Helper()
v.onLet(s)
return let(s, v.ID, blk)
}
func (v Var[V]) onLet(s *Spec) {
helper(s.testingTB).Helper()
if v.OnLet != nil {
v.OnLet(s, v)
s.vars.addOnLetHookSetup(v.ID)
}
if v.Before != nil {
s.Before(v.execBefore)
}
v.letDeps(s)
}
func (v Var[V]) execBefore(t *T) {
t.Helper()
if v.Before != nil && t.vars.tryRegisterVarBefore(v.ID) {
v.Before(t, v)
}
}
// LetValue set the value of the variable to a given block
func (v Var[V]) LetValue(s *Spec, value V) Var[V] {
helper(s.testingTB).Helper()
v.onLet(s)
return letValue[V](s, v.ID, value)
}
// Bind is a syntax sugar shorthand for Var.Let(*Spec, nil),
// where skipping providing a block meant to be explicitly expressed.
func (v Var[V]) Bind(s *Spec) Var[V] {
helper(s.testingTB).Helper()
for _, s := range s.specsFromCurrent() {
if s.vars.Knows(v.ID) {
return v
}
}
return v.Let(s, v.Init)
}
func (v Var[V]) bind(s *Spec) {
helper(s.testingTB).Helper()
_ = v.Bind(s)
}
// EagerLoading allows the variable to be loaded before the action and assertion block is reached.
// This can be useful when you want to have a variable that cause side effect on your system.
// Like it should be present in some sort of attached resource/storage.
//
// For example, you may persist the value in a storage as part of the initialization block,
// and then when the testCase/then block is reached, the entity is already present in the resource.
func (v Var[V]) EagerLoading(s *Spec) Var[V] {
helper(s.testingTB).Helper()
s.Before(func(t *T) { _ = v.Get(t) })
return v
}
// Super will return the inherited Super value of your Var.
// This means that if you declared Var in an outer Spec.Context,
// or your Var has an Var.Init field, then Var.Super will return its content.
// This allows you to incrementally extend with values the inherited value until you reach your testing scope.
// This also allows you to wrap your Super value with a Spy or Stub wrapping layer,
// and pry the interactions with the object while using the original value as a base.
func (v Var[V]) Super(t *T) V {
t.Helper()
isuper, ok := t.vars.LookupSuper(t, v.ID)
if !ok && v.Init != nil {
isuper = any(v.Init(t))
ok = true
t.vars.SetSuper(v.ID, isuper)
}
if !ok {
panic(fmt.Sprintf("no super/previous value decleration found for Var[%T]. Are you sure you defined one already?", *new(V)))
}
return isuper.(V)
}
// Append will append a value[T] to a current value of Var[[]T].
// Append only possible if the value type of Var is a slice type of T.
func Append[V any](t *T, list Var[[]V], vs ...V) {
list.Set(t, append(list.Get(t), vs...))
}