Skip to content

Commit 32d5677

Browse files
authored
Merge pull request #37 from JosephWoodward/scrubber-support
Initial support for scrubbers via a fluent API
2 parents e60d9ca + 1305c95 commit 32d5677

10 files changed

Lines changed: 210 additions & 30 deletions

approvals.go

Lines changed: 100 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"io"
99
"os"
1010
"reflect"
11+
"regexp"
1112
"strings"
1213

1314
"github.com/approvals/go-approval-tests/reporters"
@@ -32,13 +33,49 @@ type Failable interface {
3233
}
3334

3435
// VerifyWithExtension Example:
35-
// VerifyWithExtension(t, strings.NewReader("Hello"), ".txt")
36-
func VerifyWithExtension(t Failable, reader io.Reader, extWithDot string) {
36+
// VerifyWithExtension(t, strings.NewReader("Hello"), ".json")
37+
// Deprecated: Please use Verify with the Options() fluent syntax.
38+
func VerifyWithExtension(t Failable, reader io.Reader, extWithDot string, opts ...verifyOptions) {
3739
t.Helper()
40+
Verify(t, reader, alwaysOption(opts).WithExtension(extWithDot))
41+
}
42+
43+
// Verify Example:
44+
// Verify(t, strings.NewReader("Hello"))
45+
func Verify(t Failable, reader io.Reader, opts ...verifyOptions) {
46+
t.Helper()
47+
48+
if len(opts) > 1 {
49+
panic("Please use fluent syntax for options, see documentation for more information")
50+
}
51+
52+
var extWithDot string
53+
if len(opts) == 0 || opts[0].extWithDot == "" {
54+
extWithDot = ".txt"
55+
} else {
56+
extWithDot = opts[0].extWithDot
57+
}
58+
3859
namer := getApprovalName(t)
3960

61+
if len(opts) > 0 {
62+
b, err := io.ReadAll(reader)
63+
if err != nil {
64+
panic(err)
65+
}
66+
67+
result := string(b)
68+
for _, o := range opts {
69+
for _, sb := range o.scrubbers {
70+
result = sb(result)
71+
}
72+
}
73+
74+
reader = strings.NewReader(result)
75+
}
76+
4077
reporter := getReporter()
41-
var err = namer.compare(namer.getApprovalFile(extWithDot), namer.getReceivedFile(extWithDot), reader)
78+
err := namer.compare(namer.getApprovalFile(extWithDot), namer.getReceivedFile(extWithDot), reader)
4279
if err != nil {
4380
reporter.Report(namer.getApprovalFile(extWithDot), namer.getReceivedFile(extWithDot))
4481
t.Log("Failed Approval: received does not match approved.")
@@ -48,24 +85,17 @@ func VerifyWithExtension(t Failable, reader io.Reader, extWithDot string) {
4885
}
4986
}
5087

51-
// Verify Example:
52-
// Verify(t, strings.NewReader("Hello"))
53-
func Verify(t Failable, reader io.Reader) {
54-
t.Helper()
55-
VerifyWithExtension(t, reader, ".txt")
56-
}
57-
5888
// VerifyString stores the passed string into the received file and confirms
5989
// that it matches the approved local file. On failure, it will launch a reporter.
60-
func VerifyString(t Failable, s string) {
90+
func VerifyString(t Failable, s string, opts ...verifyOptions) {
6191
t.Helper()
6292
reader := strings.NewReader(s)
63-
Verify(t, reader)
93+
Verify(t, reader, opts...)
6494
}
6595

6696
// VerifyXMLStruct Example:
6797
// VerifyXMLStruct(t, xml)
68-
func VerifyXMLStruct(t Failable, obj interface{}) {
98+
func VerifyXMLStruct(t Failable, obj interface{}, opts ...verifyOptions) {
6999
t.Helper()
70100
xmlContent, err := xml.MarshalIndent(obj, "", " ")
71101
if err != nil {
@@ -74,15 +104,15 @@ func VerifyXMLStruct(t Failable, obj interface{}) {
74104
tip = "when using anonymous types be sure to include\n XMLName xml.Name `xml:\"Your_Name_Here\"`\n"
75105
}
76106
message := fmt.Sprintf("error while pretty printing XML\n%verror:\n %v\nXML:\n %v\n", tip, err, obj)
77-
VerifyWithExtension(t, strings.NewReader(message), ".xml")
107+
Verify(t, strings.NewReader(message), alwaysOption(opts).WithExtension(".xml"))
78108
} else {
79-
VerifyWithExtension(t, bytes.NewReader(xmlContent), ".xml")
109+
Verify(t, bytes.NewReader(xmlContent), alwaysOption(opts).WithExtension(".xml"))
80110
}
81111
}
82112

83113
// VerifyXMLBytes Example:
84114
// VerifyXMLBytes(t, []byte("<Test/>"))
85-
func VerifyXMLBytes(t Failable, bs []byte) {
115+
func VerifyXMLBytes(t Failable, bs []byte, opts ...verifyOptions) {
86116
t.Helper()
87117
type node struct {
88118
Attr []xml.Attr
@@ -95,65 +125,66 @@ func VerifyXMLBytes(t Failable, bs []byte) {
95125
err := xml.Unmarshal(bs, &x)
96126
if err != nil {
97127
message := fmt.Sprintf("error while parsing XML\nerror:\n %s\nXML:\n %s\n", err, string(bs))
98-
VerifyWithExtension(t, strings.NewReader(message), ".xml")
128+
Verify(t, strings.NewReader(message), alwaysOption(opts).WithExtension(".xml"))
99129
} else {
100-
VerifyXMLStruct(t, x)
130+
VerifyXMLStruct(t, x, opts...)
101131
}
102132
}
103133

104134
// VerifyJSONStruct Example:
105135
// VerifyJSONStruct(t, json)
106-
func VerifyJSONStruct(t Failable, obj interface{}) {
136+
func VerifyJSONStruct(t Failable, obj interface{}, opts ...verifyOptions) {
107137
t.Helper()
138+
108139
jsonb, err := json.MarshalIndent(obj, "", " ")
109140
if err != nil {
110141
message := fmt.Sprintf("error while pretty printing JSON\nerror:\n %s\nJSON:\n %s\n", err, obj)
111-
VerifyWithExtension(t, strings.NewReader(message), ".json")
142+
Verify(t, strings.NewReader(message), alwaysOption(opts).WithExtension(".json"))
112143
} else {
113-
VerifyWithExtension(t, bytes.NewReader(jsonb), ".json")
144+
Verify(t, bytes.NewReader(jsonb), alwaysOption(opts).WithExtension(".json"))
114145
}
115146
}
116147

117148
// VerifyJSONBytes Example:
118149
// VerifyJSONBytes(t, []byte("{ \"Greeting\": \"Hello\" }"))
119-
func VerifyJSONBytes(t Failable, bs []byte) {
150+
func VerifyJSONBytes(t Failable, bs []byte, opts ...verifyOptions) {
120151
t.Helper()
121152
var obj map[string]interface{}
122153
err := json.Unmarshal(bs, &obj)
123154
if err != nil {
124155
message := fmt.Sprintf("error while parsing JSON\nerror:\n %s\nJSON:\n %s\n", err, string(bs))
125-
VerifyWithExtension(t, strings.NewReader(message), ".json")
156+
Verify(t, strings.NewReader(message), alwaysOption(opts).WithExtension(".json"))
126157
} else {
127-
VerifyJSONStruct(t, obj)
158+
VerifyJSONStruct(t, obj, opts...)
128159
}
129160
}
130161

131162
// VerifyMap Example:
132163
// VerifyMap(t, map[string][string] { "dog": "bark" })
133-
func VerifyMap(t Failable, m interface{}) {
164+
func VerifyMap(t Failable, m interface{}, opts ...verifyOptions) {
134165
t.Helper()
135166
outputText := utils.PrintMap(m)
136-
VerifyString(t, outputText)
167+
VerifyString(t, outputText, opts...)
137168
}
138169

139170
// VerifyArray Example:
140171
// VerifyArray(t, []string{"dog", "cat"})
141-
func VerifyArray(t Failable, array interface{}) {
172+
func VerifyArray(t Failable, array interface{}, opts ...verifyOptions) {
142173
t.Helper()
143174
outputText := utils.PrintArray(array)
144-
VerifyString(t, outputText)
175+
VerifyString(t, outputText, opts...)
145176
}
146177

147178
// VerifyAll Example:
148179
// VerifyAll(t, "uppercase", []string("dog", "cat"}, func(x interface{}) string { return strings.ToUpper(x.(string)) })
149-
func VerifyAll(t Failable, header string, collection interface{}, transform func(interface{}) string) {
180+
func VerifyAll(t Failable, header string, collection interface{}, transform func(interface{}) string, opts ...verifyOptions) {
150181
t.Helper()
151182
if len(header) != 0 {
152183
header = fmt.Sprintf("%s\n\n\n", header)
153184
}
154185

155186
outputText := header + strings.Join(utils.MapToString(collection, transform), "\n")
156-
VerifyString(t, outputText)
187+
VerifyString(t, outputText, opts...)
157188
}
158189

159190
type reporterCloser struct {
@@ -232,3 +263,42 @@ func getReporter() reporters.Reporter {
232263
func UseFolder(f string) {
233264
defaultFolder = f
234265
}
266+
267+
type scrubber func(s string) string
268+
269+
// verifyOptions can be accessed via the approvals.Options() API enabling configuration of scrubbers
270+
type verifyOptions struct {
271+
scrubbers []scrubber
272+
extWithDot string
273+
}
274+
275+
// Options enables providing individual Verify functions with customisations such as scrubbers
276+
func Options() verifyOptions {
277+
return verifyOptions{}
278+
}
279+
280+
// WithRegexScrubber allows you to 'scrub' dynamic data such as timestamps within your test input
281+
// and replace it with a static placeholder
282+
func (v verifyOptions) WithRegexScrubber(scrubber *regexp.Regexp, replacer string) verifyOptions {
283+
v.scrubbers = append(v.scrubbers, func(s string) string {
284+
return scrubber.ReplaceAllString(s, replacer)
285+
})
286+
return v
287+
}
288+
289+
// WithExtension overrides the default file extension (.txt) for approval files.
290+
func (v verifyOptions) WithExtension(extension string) verifyOptions {
291+
v.extWithDot = extension
292+
return v
293+
}
294+
295+
func alwaysOption(opts []verifyOptions) verifyOptions {
296+
var v verifyOptions
297+
if len(opts) == 0 {
298+
v = Options()
299+
} else {
300+
v = opts[0]
301+
}
302+
303+
return v
304+
}

scrubber_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package approvals_test
2+
3+
import (
4+
"fmt"
5+
"regexp"
6+
"strings"
7+
"testing"
8+
"time"
9+
10+
approvals "github.com/approvals/go-approval-tests"
11+
)
12+
13+
func TestVerifyDoesNotAcceptSeveralVerifyOptions(t *testing.T) {
14+
scrubber1, _ := regexp.Compile("\\d{10}$")
15+
opts1 := approvals.Options().WithRegexScrubber(scrubber1, "<time>")
16+
opts2 := approvals.Options().WithRegexScrubber(scrubber1, "<time>")
17+
18+
m := strings.NewReader("Hello World")
19+
20+
defer func() { _ = recover() }()
21+
22+
approvals.Verify(t, m, opts1, opts2)
23+
t.Errorf("Panic expected")
24+
}
25+
26+
func TestVerifyMapWithRegexScrubber(t *testing.T) {
27+
scrubber, _ := regexp.Compile("\\d{10}$")
28+
opts := approvals.Options().WithRegexScrubber(scrubber, "<time>")
29+
30+
m := map[string]string{
31+
"dog": "bark",
32+
"cat": "meow",
33+
"time": fmt.Sprint(time.Now().Unix()),
34+
}
35+
approvals.VerifyMap(t, m, opts)
36+
}
37+
38+
func TestVerifyArrayWithRegexScrubber(t *testing.T) {
39+
scrubber, _ := regexp.Compile("cat")
40+
opts := approvals.Options().WithRegexScrubber(scrubber, "person")
41+
42+
xs := []string{"dog", "cat", "bird"}
43+
approvals.VerifyArray(t, xs, opts)
44+
}
45+
46+
func TestVerifyJSONBytesWithRegexScrubber(t *testing.T) {
47+
scrubber, _ := regexp.Compile("Hello")
48+
opts := approvals.Options().WithRegexScrubber(scrubber, "Hi")
49+
50+
jb := []byte("{ \"Greeting\": \"Hello\" }")
51+
approvals.VerifyJSONBytes(t, jb, opts)
52+
}
53+
54+
func TestVerifyXMLBytesWithRegexScrubber(t *testing.T) {
55+
scrubber, _ := regexp.Compile("Hello")
56+
opts := approvals.Options().WithRegexScrubber(scrubber, "Hi")
57+
58+
xmlb := []byte("<Test><Title>Hello World!</Title><Name>Peter Pan</Name><Age>100</Age></Test>")
59+
approvals.VerifyXMLBytes(t, xmlb, opts)
60+
}
61+
62+
func TestVerifyStringWithRegexScrubber(t *testing.T) {
63+
scrubber, _ := regexp.Compile("\\d{10}$")
64+
opts := approvals.Options().WithRegexScrubber(scrubber, "<now>")
65+
66+
s := fmt.Sprintf("The time is %v", time.Now().Unix())
67+
approvals.VerifyString(t, s, opts)
68+
}
69+
70+
func TestVerifyStringWithMultipleScrubbers(t *testing.T) {
71+
scrubber1, _ := regexp.Compile("\\d{10}$")
72+
scrubber2, _ := regexp.Compile("time")
73+
74+
opts := approvals.Options().
75+
WithRegexScrubber(scrubber1, "<now>").
76+
WithRegexScrubber(scrubber2, "<future>")
77+
78+
s := fmt.Sprintf("The time is %v", time.Now().Unix())
79+
approvals.VerifyString(t, s, opts)
80+
}
81+
82+
func TestVerifyAllWithRegexScrubber(t *testing.T) {
83+
scrubber, _ := regexp.Compile("Llewellyn")
84+
opts := approvals.Options().WithRegexScrubber(scrubber, "Walken")
85+
86+
xs := []string{"Christopher", "Llewellyn"}
87+
approvals.VerifyAll(t, "uppercase", xs, func(x interface{}) string { return fmt.Sprintf("%s => %s", x, strings.ToUpper(x.(string))) }, opts)
88+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
uppercase
2+
3+
4+
Christopher => CHRISTOPHER
5+
Walken => LLEWELLYN
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[0]=dog
2+
[1]=person
3+
[2]=bird
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Hello World
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"Greeting": "Hi"
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[cat]=meow
2+
[dog]=bark
3+
[time]=<time>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The <future> is <now>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The time is <now>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<Test>
2+
<Title>Hi World!</Title>
3+
<Name>Peter Pan</Name>
4+
<Age>100</Age>
5+
</Test>

0 commit comments

Comments
 (0)