feat(assert): add marshaling assertion helpers for JSON, YAML and XML

This commit is contained in:
2021-11-06 20:24:01 +00:00
parent edb189f086
commit 3f967b571f
118 changed files with 1248 additions and 17 deletions

5
.gitignore vendored
View File

@@ -1,5 +1,6 @@
bin/*
coverage.out
testdata/*
!testdata/TestExample*
testdata/TestFile*
testdata/TestGet*
testdata/TestSet*

307
assert.go Normal file
View File

@@ -0,0 +1,307 @@
package golden
import (
"bytes"
"encoding/json"
"encoding/xml"
"io"
"testing"
"gopkg.in/yaml.v3"
)
var globalAssert = NewAssert()
// AssertJSONMarshaling asserts that the given "v" value JSON marshals to an
// expected value fetched from a golden file on disk, and then verifies that the
// marshaled result produces a value that is equal to "v" when unmarshaled.
//
// Used for objects that do NOT change when they are marshaled and unmarshaled.
func AssertJSONMarshaling(t *testing.T, v interface{}) {
t.Helper()
globalAssert.JSONMarshaling(t, v)
}
// AssertJSONMarshalingP asserts that the given "v" value JSON marshals to an
// expected value fetched from a golden file on disk, and then verifies that the
// marshaled result produces a value that is equal to "want" when unmarshaled.
//
// Used for objects that change when they are marshaled and unmarshaled.
func AssertJSONMarshalingP(t *testing.T, v, want interface{}) {
t.Helper()
globalAssert.JSONMarshalingP(t, v, want)
}
// AssertXMLMarshaling asserts that the given "v" value XML marshals to an
// expected value fetched from a golden file on disk, and then verifies that the
// marshaled result produces a value that is equal to "v" when unmarshaled.
//
// Used for objects that do NOT change when they are marshaled and unmarshaled.
func AssertXMLMarshaling(t *testing.T, v interface{}) {
t.Helper()
globalAssert.XMLMarshaling(t, v)
}
// AssertXMLMarshalingP asserts that the given "v" value XML marshals to an
// expected value fetched from a golden file on disk, and then verifies that the
// marshaled result produces a value that is equal to "want" when unmarshaled.
//
// Used for objects that change when they are marshaled and unmarshaled.
func AssertXMLMarshalingP(t *testing.T, v, want interface{}) {
t.Helper()
globalAssert.XMLMarshalingP(t, v, want)
}
// AssertYAMLMarshaling asserts that the given "v" value YAML marshals to an
// expected value fetched from a golden file on disk, and then verifies that the
// marshaled result produces a value that is equal to "v" when unmarshaled.
//
// Used for objects that do NOT change when they are marshaled and unmarshaled.
func AssertYAMLMarshaling(t *testing.T, v interface{}) {
t.Helper()
globalAssert.YAMLMarshaling(t, v)
}
// AssertYAMLMarshalingP asserts that the given "v" value YAML marshals to an
// expected value fetched from a golden file on disk, and then verifies that the
// marshaled result produces a value that is equal to "want" when unmarshaled.
//
// Used for objects that change when they are marshaled and unmarshaled.
func AssertYAMLMarshalingP(t *testing.T, v, want interface{}) {
t.Helper()
globalAssert.YAMLMarshalingP(t, v, want)
}
// Assert exposes a series of JSON, YAML, and XML marshaling assertion helpers.
type Assert interface {
// JSONMarshaling asserts that the given "v" value JSON marshals to an
// expected value fetched from a golden file on disk, and then verifies that
// the marshaled result produces a value that is equal to "v" when
// unmarshaled.
//
// Used for objects that do NOT change when they are marshaled and
// unmarshaled.
JSONMarshaling(t *testing.T, v interface{})
// JSONMarshalingP asserts that the given "v" value JSON marshals to an
// expected value fetched from a golden file on disk, and then verifies that
// the marshaled result produces a value that is equal to "want" when
// unmarshaled.
//
// Used for objects that change when they are marshaled and unmarshaled.
JSONMarshalingP(t *testing.T, v interface{}, want interface{})
// XMLMarshaling asserts that the given "v" value XML marshals to an
// expected value fetched from a golden file on disk, and then verifies that
// the marshaled result produces a value that is equal to "v" when
// unmarshaled.
//
// Used for objects that do NOT change when they are marshaled and
// unmarshaled.
XMLMarshaling(t *testing.T, v interface{})
// XMLMarshalingP asserts that the given "v" value XML marshals to an
// expected value fetched from a golden file on disk, and then verifies that
// the marshaled result produces a value that is equal to "want" when
// unmarshaled.
//
// Used for objects that change when they are marshaled and unmarshaled.
XMLMarshalingP(t *testing.T, v interface{}, want interface{})
// YAMLMarshaling asserts that the given "v" value YAML marshals to an
// expected value fetched from a golden file on disk, and then verifies that
// the marshaled result produces a value that is equal to "v" when
// unmarshaled.
//
// Used for objects that do NOT change when they are marshaled and
// unmarshaled.
YAMLMarshaling(t *testing.T, v interface{})
// YAMLMarshalingP asserts that the given "v" value YAML marshals to an
// expected value fetched from a golden file on disk, and then verifies that
// the marshaled result produces a value that is equal to "want" when
// unmarshaled.
//
// Used for objects that change when they are marshaled and unmarshaled.
YAMLMarshalingP(t *testing.T, v interface{}, want interface{})
}
type AssertOption interface {
apply(*asserter)
}
type assertOptionFunc func(*asserter)
func (fn assertOptionFunc) apply(c *asserter) {
fn(c)
}
// WithGolden allows setting a custom *Golden instance when calling NewAssert().
func WithGolden(golden *Golden) AssertOption {
return assertOptionFunc(func(a *asserter) {
a.golden = golden
})
}
// WithNormalizedLineBreaks allows turning off line-break normalization which
// replaces Windows' CRLF (\r\n) and Mac Classic CR (\r) line breaks with Unix's
// LF (\n) line breaks.
func WithNormalizedLineBreaks(value bool) AssertOption {
return assertOptionFunc(func(a *asserter) {
a.normalizeLineBreaks = value
})
}
// NewAssert returns a new Assert which exposes a number of marshaling assertion
// helpers for JSON, YAML and XML.
//
// The default encoders all specify indentation of two spaces, essentially
// enforcing pretty formatting for JSON and XML.
//
// The default decoders for JSON and YAML prohibit unknown fields which are not
// present on the provided struct.
func NewAssert(options ...AssertOption) Assert {
a := &asserter{
golden: globalGolden,
normalizeLineBreaks: true,
}
for _, opt := range options {
opt.apply(a)
}
a.JSONAsserter = NewMarshalAsserter(
a.golden, "JSON",
newJSONEncoder, newJSONDecoder,
a.normalizeLineBreaks,
)
a.XMLAsserter = NewMarshalAsserter(
a.golden, "XML",
newXMLEncoder, newXMLDecoder,
a.normalizeLineBreaks,
)
a.YAMLAsserter = NewMarshalAsserter(
a.golden, "YAML",
newYAMLEncoder, newYAMLDecoder,
a.normalizeLineBreaks,
)
return a
}
// asserter implements the Assert interface.
type asserter struct {
golden *Golden
normalizeLineBreaks bool
JSONAsserter *MarshalAsserter
XMLAsserter *MarshalAsserter
YAMLAsserter *MarshalAsserter
}
func (s *asserter) JSONMarshaling(t *testing.T, v interface{}) {
t.Helper()
s.JSONAsserter.Marshaling(t, v)
}
func (s *asserter) JSONMarshalingP(
t *testing.T,
v interface{},
want interface{},
) {
t.Helper()
s.JSONAsserter.MarshalingP(t, v, want)
}
func (s *asserter) XMLMarshaling(t *testing.T, v interface{}) {
t.Helper()
s.XMLAsserter.Marshaling(t, v)
}
func (s *asserter) XMLMarshalingP(t *testing.T, v, want interface{}) {
t.Helper()
s.XMLAsserter.MarshalingP(t, v, want)
}
func (s *asserter) YAMLMarshaling(t *testing.T, v interface{}) {
t.Helper()
s.YAMLAsserter.Marshaling(t, v)
}
func (s *asserter) YAMLMarshalingP(t *testing.T, v, want interface{}) {
t.Helper()
s.YAMLAsserter.MarshalingP(t, v, want)
}
// newJSONEncoder is the default JSONEncoderFunc used by Assert. It returns a
// *json.Encoder which is set to indent with two spaces.
func newJSONEncoder(w io.Writer) MarshalEncoder {
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
return enc
}
// newJSONDecoder is the default JSONDecoderFunc used by Assert. It returns a
// *json.Decoder which disallows unknown fields.
func newJSONDecoder(r io.Reader) MarshalDecoder {
dec := json.NewDecoder(r)
dec.DisallowUnknownFields()
return dec
}
// newXMLEncoder is the default XMLEncoderFunc used by Assert. It returns a
// *xml.Encoder which is set to indent with two spaces.
func newXMLEncoder(w io.Writer) MarshalEncoder {
enc := xml.NewEncoder(w)
enc.Indent("", " ")
return enc
}
// newXMLDecoder is the default XMLDecoderFunc used by Assert.
func newXMLDecoder(r io.Reader) MarshalDecoder {
return xml.NewDecoder(r)
}
// newYAMLEncoder is the default YAMLEncoderFunc used by Assert. It returns a
// *yaml.Encoder which is set to indent with two spaces.
func newYAMLEncoder(w io.Writer) MarshalEncoder {
enc := yaml.NewEncoder(w)
enc.SetIndent(2)
return enc
}
// newYAMLDecoder is the default YAMLDecoderFunc used by Assert. It returns a
// *yaml.Decoder which disallows unknown fields.
func newYAMLDecoder(r io.Reader) MarshalDecoder {
dec := yaml.NewDecoder(r)
dec.KnownFields(true)
return dec
}
// normalizeLineBreaks replaces Windows CRLF (\r\n) and Classic MacOS CR (\r)
// line-breaks with Unix LF (\n) line breaks.
func normalizeLineBreaks(data []byte) []byte {
// Replace Windows CRLF (\r\n) with Unix LF (\n)
result := bytes.ReplaceAll(data, []byte{13, 10}, []byte{10})
// Replace Classic MacOS CR (\r) with Unix LF (\n)
result = bytes.ReplaceAll(result, []byte{13}, []byte{10})
return result
}

68
assert_example_test.go Normal file
View File

@@ -0,0 +1,68 @@
package golden_test
import (
"testing"
"github.com/jimeh/go-golden"
)
type MyBook struct {
FooBar string `json:"foo_bar,omitempty" yaml:"fooBar,omitempty" xml:"Foo_Bar,omitempty"`
Bar string `json:"-" yaml:"-" xml:"-"`
baz string
}
// TestExampleMyBookMarshaling reads/writes the following golden files:
//
// testdata/TestExampleMyBookMarshaling/marshaled_json.golden
// testdata/TestExampleMyBookMarshaling/marshaled_xml.golden
// testdata/TestExampleMyBookMarshaling/marshaled_yaml.golden
//
func TestExampleMyBookMarshaling(t *testing.T) {
obj := &MyBook{FooBar: "Hello World!"}
golden.AssertJSONMarshaling(t, obj)
golden.AssertYAMLMarshaling(t, obj)
golden.AssertXMLMarshaling(t, obj)
}
// TestExampleMyBookMarshalingP reads/writes the following golden files:
//
// testdata/TestExampleMyBookMarshalingP/marshaled_json.golden
// testdata/TestExampleMyBookMarshalingP/marshaled_xml.golden
// testdata/TestExampleMyBookMarshalingP/marshaled_yaml.golden
//
func TestExampleMyBookMarshalingP(t *testing.T) {
obj := &MyBook{FooBar: "Hello World!", Bar: "Oops", baz: "nope!"}
want := &MyBook{FooBar: "Hello World!"}
golden.AssertJSONMarshalingP(t, obj, want)
golden.AssertYAMLMarshalingP(t, obj, want)
golden.AssertXMLMarshalingP(t, obj, want)
}
// TestExampleMyBookMarshalingTabular reads/writes the following golden files:
//
// testdata/TestExampleMyBookMarshalingTabular/empty/marshaled_json.golden
// testdata/TestExampleMyBookMarshalingTabular/empty/marshaled_xml.golden
// testdata/TestExampleMyBookMarshalingTabular/empty/marshaled_yaml.golden
// testdata/TestExampleMyBookMarshalingTabular/full/marshaled_json.golden
// testdata/TestExampleMyBookMarshalingTabular/full/marshaled_xml.golden
// testdata/TestExampleMyBookMarshalingTabular/full/marshaled_yaml.golden
//
func TestExampleMyBookMarshalingTabular(t *testing.T) {
tests := []struct {
name string
obj *MyBook
}{
{name: "empty", obj: &MyBook{}},
{name: "full", obj: &MyBook{FooBar: "Hello World!"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
golden.AssertJSONMarshaling(t, tt.obj)
golden.AssertYAMLMarshaling(t, tt.obj)
golden.AssertXMLMarshaling(t, tt.obj)
})
}
}

370
assert_test.go Normal file
View File

@@ -0,0 +1,370 @@
package golden
import (
"bytes"
"encoding/xml"
"fmt"
"regexp"
"testing"
"time"
"gopkg.in/yaml.v3"
)
//
// Helpers
//
type Author struct {
FirstName string `json:"first_name" yaml:"first_name" xml:"first_name"`
LastName string `json:"last_name" yaml:"last_name" xml:"last_name"`
}
type Book struct {
ID string `json:"id" yaml:"id" xml:"id"`
Title string `json:"title" yaml:"title" xml:"title"`
Author *Author `json:"author,omitempty" yaml:"author,omitempty" xml:"author,omitempty"`
Year int `json:"year,omitempty" yaml:"year,omitempty" xml:"year,omitempty"`
}
type Article struct {
ID string `json:"id" yaml:"id" xml:"id"`
Title string `json:"title" yaml:"title" xml:"title"`
Author *Author `json:"author" yaml:"author" xml:"author"`
Date *time.Time `json:"date,omitempty" yaml:"date,omitempty" xml:"date,omitempty"`
Rank int `json:"-" yaml:"-" xml:"-"`
order int
}
type Comic struct {
ID string
Name string
Issue string
Ignored string
}
type xmlComic struct {
ID string `xml:"id,attr"`
Name string `xml:",chardata"`
Issue string `xml:"issue,attr"`
}
func (s *Comic) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`{"%s":"%s=%s"}`, s.ID, s.Name, s.Issue)), nil
}
func (s *Comic) UnmarshalJSON(data []byte) error {
m := regexp.MustCompile(`^{\s*"(.*?)":\s*"(.*?)=(.*)"\s*}$`)
matches := m.FindSubmatch(bytes.TrimSpace(data))
if matches == nil {
return nil
}
s.ID = string(matches[1])
s.Name = string(matches[2])
s.Issue = string(matches[3])
return nil
}
func (s *Comic) MarshalYAML() (interface{}, error) {
return map[string]map[string]string{s.ID: {s.Name: s.Issue}}, nil
}
func (s *Comic) UnmarshalYAML(value *yaml.Node) error {
// Horribly hacky code, but it works and specifically only needs to extract
// these specific three values.
if len(value.Content) == 2 {
s.ID = value.Content[0].Value
if len(value.Content[1].Content) == 2 {
s.Name = value.Content[1].Content[0].Value
s.Issue = value.Content[1].Content[1].Value
}
}
return nil
}
func (s *Comic) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
return e.EncodeElement(
&xmlComic{ID: s.ID, Name: s.Name, Issue: s.Issue},
start,
)
}
func (s *Comic) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
x := &xmlComic{}
_ = d.DecodeElement(x, &start)
v := Comic{ID: x.ID, Name: x.Name, Issue: x.Issue}
*s = v
return nil
}
func boolPtr(b bool) *bool {
return &b
}
func intPtr(i int) *int {
return &i
}
func stringPtr(s string) *string {
return &s
}
//
// Test cases
//
var marhalingTestCases = []struct {
name string
v interface{}
}{
{
name: "true bool pointer",
v: boolPtr(true),
},
{
name: "false bool pointer",
v: boolPtr(false),
},
{
name: "int pointer",
v: intPtr(42),
},
{
name: "string pointer",
v: stringPtr("hello world"),
},
{
name: "empty struct",
v: &Book{},
},
{
name: "partial struct",
v: &Book{
ID: "cfda163c-d5c1-44a2-909b-5d2ce3a31979",
Title: "The Traveler",
},
},
{
name: "full struct",
v: &Book{
ID: "cfda163c-d5c1-44a2-909b-5d2ce3a31979",
Title: "The Traveler",
Author: &Author{
FirstName: "John",
LastName: "Twelve Hawks",
},
Year: 2005,
},
},
{
name: "custom marshaling",
v: &Comic{
ID: "2fd5af35-b85e-4f03-8eba-524be28d7a5b",
Name: "Hello World!",
Issue: "Forty Two",
},
},
}
var articleDate = time.Date(
2021, time.October, 27, 23, 30, 34, 0, time.FixedZone("", 1*60*60),
).UTC()
var marshalingPTestCases = []struct {
name string
v interface{}
want interface{}
}{
{
name: "true bool pointer",
v: boolPtr(true),
want: boolPtr(true),
},
{
name: "false bool pointer",
v: boolPtr(false),
want: boolPtr(false),
},
{
name: "int pointer",
v: intPtr(42),
want: intPtr(42),
},
{
name: "string pointer",
v: stringPtr("hello world"),
want: stringPtr("hello world"),
},
{
name: "empty struct",
v: &Article{},
want: &Article{},
},
{
name: "partial struct",
v: &Book{
ID: "10eec54d-e30a-4428-be18-01095d889126",
Title: "Time Travel",
},
want: &Book{
ID: "10eec54d-e30a-4428-be18-01095d889126",
Title: "Time Travel",
},
},
{
name: "full struct",
v: &Article{
ID: "10eec54d-e30a-4428-be18-01095d889126",
Title: "Time Travel",
Author: &Author{
FirstName: "Doc",
LastName: "Brown",
},
Date: &articleDate,
Rank: 8,
order: 16,
},
want: &Article{
ID: "10eec54d-e30a-4428-be18-01095d889126",
Title: "Time Travel",
Author: &Author{
FirstName: "Doc",
LastName: "Brown",
},
Date: &articleDate,
},
},
{
name: "custom marshaling",
v: &Comic{
ID: "2fd5af35-b85e-4f03-8eba-524be28d7a5b",
Name: "Hello World!",
Issue: "Forty Two",
Ignored: "don't pay attention to this :)",
},
want: &Comic{
ID: "2fd5af35-b85e-4f03-8eba-524be28d7a5b",
Name: "Hello World!",
Issue: "Forty Two",
},
},
}
//
// Tests
//
func TestAssertJSONMarshaling(t *testing.T) {
for _, tt := range marhalingTestCases {
t.Run(tt.name, func(t *testing.T) {
AssertJSONMarshaling(t, tt.v)
})
}
}
func TestAssertJSONMarshalingP(t *testing.T) {
for _, tt := range marshalingPTestCases {
t.Run(tt.name, func(t *testing.T) {
AssertJSONMarshalingP(t, tt.v, tt.want)
})
}
}
func TestAssertXMLMarshaling(t *testing.T) {
for _, tt := range marhalingTestCases {
t.Run(tt.name, func(t *testing.T) {
AssertXMLMarshaling(t, tt.v)
})
}
}
func TestAssertXMLMarshalingP(t *testing.T) {
for _, tt := range marshalingPTestCases {
t.Run(tt.name, func(t *testing.T) {
AssertXMLMarshalingP(t, tt.v, tt.want)
})
}
}
func TestAssertYAMLMarshaling(t *testing.T) {
for _, tt := range marhalingTestCases {
t.Run(tt.name, func(t *testing.T) {
AssertYAMLMarshaling(t, tt.v)
})
}
}
func TestAssertYAMLMarshalingP(t *testing.T) {
for _, tt := range marshalingPTestCases {
t.Run(tt.name, func(t *testing.T) {
AssertYAMLMarshalingP(t, tt.v, tt.want)
})
}
}
func TestAssert_JSONMarshaling(t *testing.T) {
for _, tt := range marhalingTestCases {
t.Run(tt.name, func(t *testing.T) {
assert := NewAssert()
assert.JSONMarshaling(t, tt.v)
})
}
}
func TestAssert_JSONMarshalingP(t *testing.T) {
for _, tt := range marshalingPTestCases {
t.Run(tt.name, func(t *testing.T) {
assert := NewAssert()
assert.JSONMarshalingP(t, tt.v, tt.want)
})
}
}
func TestAssert_XMLMarshaling(t *testing.T) {
for _, tt := range marhalingTestCases {
t.Run(tt.name, func(t *testing.T) {
assert := NewAssert()
assert.XMLMarshaling(t, tt.v)
})
}
}
func TestAssert_XMLMarshalingP(t *testing.T) {
for _, tt := range marshalingPTestCases {
t.Run(tt.name, func(t *testing.T) {
assert := NewAssert()
assert.XMLMarshalingP(t, tt.v, tt.want)
})
}
}
func TestAssert_YAMLMarshaling(t *testing.T) {
for _, tt := range marhalingTestCases {
t.Run(tt.name, func(t *testing.T) {
assert := NewAssert()
assert.YAMLMarshaling(t, tt.v)
})
}
}
func TestAssert_YAMLMarshalingP(t *testing.T) {
for _, tt := range marshalingPTestCases {
t.Run(tt.name, func(t *testing.T) {
assert := NewAssert()
assert.YAMLMarshalingP(t, tt.v, tt.want)
})
}
}

1
go.mod
View File

@@ -5,4 +5,5 @@ go 1.15
require (
github.com/jimeh/envctl v0.1.0
github.com/stretchr/testify v1.7.0
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
)

3
go.sum
View File

@@ -10,5 +10,6 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,14 +1,67 @@
// Package golden is yet another package for working with *.golden test files,
// with a focus on simplicity through it's default behavior.
// with a focus on simplicity through it's default behavior, and marshaling
// assertion helpers to validate JSON/YAML/XML format of structs.
//
// Golden file names are based on the name of the test function and any subtest
// names by calling t.Name(). File names are sanitized to ensure they're
// compatible with Linux, macOS and Windows systems regardless of what crazy
// compatible with Linux, macOS and Windows systems regardless of what
// characters might be in a subtest's name.
//
// Usage
// Assertion Helpers
//
// Typical usage should look something like this:
// There are marshaling assertion helpers for JSON, YAML, and XML. Each helper
// operates in two stages:
//
// 1. Marshal the provided object to a byte slice and read the corresponding
// golden file from disk followed by verifying both are identical.
//
// 2. Unmarshal the content of the golden file, verifying that the result is
// identical to the original given object.
//
// Typical usage of a assertion helper would look something like this in a
// tabular test:
//
// type MyStruct struct {
// FooBar string `json:"foo_bar" yaml:"fooBar" xml:"Foo_Bar"`
// }
//
// func TestMyStructMarshaling(t *testing.T) {
// tests := []struct {
// name string
// obj *MyStruct
// }{
// {name: "empty", obj: &MyStruct{}},
// {name: "full", obj: &MyStruct{FooBar: "Hello World!"}},
// }
// for _, tt := range tests {
// t.Run(tt.name, func(t *testing.T) {
// golden.AssertJSONMarshaling(t, tt.obj)
// golden.AssertYAMLMarshaling(t, tt.obj)
// golden.AssertXMLMarshaling(t, tt.obj)
// })
// }
// }
//
// The above example will read from the following golden files:
//
// testdata/TestMyStructMarshaling/empty/marshaled_json.golden
// testdata/TestMyStructMarshaling/empty/marshaled_xml.golden
// testdata/TestMyStructMarshaling/empty/marshaled_yaml.golden
// testdata/TestMyStructMarshaling/full/marshaled_json.golden
// testdata/TestMyStructMarshaling/full/marshaled_xml.golden
// testdata/TestMyStructMarshaling/full/marshaled_yaml.golden
//
// If a corresponding golden file cannot be found on disk, the test will fail.
// To create/update golden files, simply set the GOLDEN_UPDATE environment
// variable to one of "1", "y", "t", "yes", "on", or "true" when running the
// tests.
//
// Golden files should be committed to source control, as it allow tests to fail
// when the marshaling results for an object changes.
//
// Golden Usage
//
// Typical usage would look something like this:
//
// func TestExampleMyStruct(t *testing.T) {
// got, err := json.Marshal(&MyStruct{Foo: "Bar"})
@@ -139,14 +192,14 @@ const (
var DefaultUpdateFunc = EnvUpdateFunc
var global = New()
var globalGolden = New()
// File returns the filename of the golden file for the given *testing.T
// instance as determined by t.Name().
func File(t *testing.T) string {
t.Helper()
return global.File(t)
return globalGolden.File(t)
}
// Get returns the content of the golden file for the given *testing.T instance
@@ -155,7 +208,7 @@ func File(t *testing.T) string {
func Get(t *testing.T) []byte {
t.Helper()
return global.Get(t)
return globalGolden.Get(t)
}
// Set writes given data to the golden file for the given *testing.T instance as
@@ -164,7 +217,7 @@ func Get(t *testing.T) []byte {
func Set(t *testing.T, data []byte) {
t.Helper()
global.Set(t, data)
globalGolden.Set(t, data)
}
// FileP returns the filename of the specifically named golden file for the
@@ -172,7 +225,7 @@ func Set(t *testing.T, data []byte) {
func FileP(t *testing.T, name string) string {
t.Helper()
return global.FileP(t, name)
return globalGolden.FileP(t, name)
}
// GetP returns the content of the specifically named golden file belonging
@@ -184,7 +237,7 @@ func FileP(t *testing.T, name string) string {
func GetP(t *testing.T, name string) []byte {
t.Helper()
return global.GetP(t, name)
return globalGolden.GetP(t, name)
}
// SetP writes given data of the specifically named golden file belonging to
@@ -196,7 +249,7 @@ func GetP(t *testing.T, name string) []byte {
func SetP(t *testing.T, name string, data []byte) {
t.Helper()
global.SetP(t, name, data)
globalGolden.SetP(t, name, data)
}
// Update returns true when golden is set to update golden files. Should be used
@@ -206,7 +259,7 @@ func SetP(t *testing.T, name string, data []byte) {
// environment variable is set to a truthy value. To customize create a custom
// *Golden instance with New() and set a new UpdateFunc value.
func Update() bool {
return global.Update()
return globalGolden.Update()
}
// Golden handles all interactions with golden files. The top-level package

151
marshal_asserter.go Normal file
View File

@@ -0,0 +1,151 @@
package golden
import (
"bytes"
"io"
"reflect"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type (
NewEncoderFunc func(w io.Writer) MarshalEncoder
NewDecoderFunc func(r io.Reader) MarshalDecoder
)
type MarshalEncoder interface {
Encode(v interface{}) error
}
type MarshalDecoder interface {
Decode(v interface{}) error
}
// MarshalAsserter allows building custom marshaling asserters, but providing
// functions which returns new encoder and decoders for the format to be
// asserted.
//
// All the Assert<format>Marshaling helper functions uses MarshalAsserter under
// the hood.
type MarshalAsserter struct {
// Golden is the *Golden instance used to read/write golden files.
Golden *Golden
// Name of the format the MarshalAsserter handles.
Format string
// GoldName is the name of the golden file used when marshaling. This is by
// default set based on Format using NewMarshalAsserter. For example if
// Format is set to "JSON", GoldName will be set to "marshaled_json" by
// default.
GoldName string
// NewEncoderFunc is the function used to create a new encoder for
// marshaling objects.
NewEncoderFunc NewEncoderFunc
// NewDecoderFunc is the function used to create a new decoder for
// unmarshaling objects.
NewDecoderFunc NewDecoderFunc
// NormalizeLineBreaks determines if Windows' CRLF (\r\n) and Mac Classic CR
// (\r) line breaks are replaced with Unix's LF (\n) line breaks. This
// ensure marshaling assertions works cross platform.
NormalizeLineBreaks bool
}
func NewMarshalAsserter(
golden *Golden,
format string,
newEncoderFunc NewEncoderFunc,
newDecoderFunc NewDecoderFunc,
normalizeLineBreaks bool,
) *MarshalAsserter {
if golden == nil {
golden = globalGolden
}
goldName := "marshaled_" + strings.ToLower(sanitizeFilename(format))
return &MarshalAsserter{
Golden: golden,
Format: format,
GoldName: goldName,
NewEncoderFunc: newEncoderFunc,
NewDecoderFunc: newDecoderFunc,
NormalizeLineBreaks: normalizeLineBreaks,
}
}
// Marshaling asserts that the given "v" value marshals via the provided encoder
// to an expected value fetched from a golden file on disk, and then verifies
// that the marshaled result produces a value that is equal to "v" when
// unmarshaled.
//
// Used for objects that do NOT change when they are marshaled and unmarshaled.
func (s *MarshalAsserter) Marshaling(t *testing.T, v interface{}) {
t.Helper()
s.MarshalingP(t, v, v)
}
// MarshalingP asserts that the given "v" value marshals via the provided
// encoder to an expected value fetched from a golden file on disk, and then
// verifies that the marshaled result produces a value that is equal to "want"
// when unmarshaled.
//
// Used for objects that change when they are marshaled and unmarshaled.
func (s *MarshalAsserter) MarshalingP(
t *testing.T,
v interface{},
want interface{},
) {
t.Helper()
if reflect.ValueOf(want).Kind() != reflect.Ptr {
require.FailNowf(t,
"only pointer types can be asserted",
"%T is not a pointer type", want,
)
}
var buf bytes.Buffer
err := s.NewEncoderFunc(&buf).Encode(v)
require.NoErrorf(t, err, "failed to %s marshal %T: %+v", s.Format, v, v)
marshaled := buf.Bytes()
if s.NormalizeLineBreaks {
marshaled = normalizeLineBreaks(marshaled)
}
if s.Golden.Update() {
s.Golden.SetP(t, s.GoldName, marshaled)
}
gold := s.Golden.GetP(t, s.GoldName)
if s.NormalizeLineBreaks {
gold = normalizeLineBreaks(gold)
}
switch strings.ToLower(s.Format) {
case "json":
assert.JSONEq(t, string(gold), string(marshaled))
case "yaml", "yml":
assert.YAMLEq(t, string(gold), string(marshaled))
default:
assert.Equal(t, string(gold), string(marshaled))
}
got := reflect.New(reflect.TypeOf(want).Elem()).Interface()
err = s.NewDecoderFunc(bytes.NewBuffer(gold)).Decode(got)
require.NoErrorf(t, err,
"failed to %s unmarshal %T from %s",
s.Format, got, s.Golden.FileP(t, s.GoldName),
)
assert.Equal(t, want, got,
"unmarshaling from golden file does not match expected object",
)
}

View File

@@ -0,0 +1,3 @@
{
"2fd5af35-b85e-4f03-8eba-524be28d7a5b": "Hello World!=Forty Two"
}

View File

@@ -0,0 +1,4 @@
{
"id": "",
"title": ""
}

View File

@@ -0,0 +1 @@
false

View File

@@ -0,0 +1,9 @@
{
"id": "cfda163c-d5c1-44a2-909b-5d2ce3a31979",
"title": "The Traveler",
"author": {
"first_name": "John",
"last_name": "Twelve Hawks"
},
"year": 2005
}

View File

@@ -0,0 +1 @@
42

View File

@@ -0,0 +1,4 @@
{
"id": "cfda163c-d5c1-44a2-909b-5d2ce3a31979",
"title": "The Traveler"
}

View File

@@ -0,0 +1 @@
"hello world"

View File

@@ -0,0 +1 @@
true

View File

@@ -0,0 +1,3 @@
{
"2fd5af35-b85e-4f03-8eba-524be28d7a5b": "Hello World!=Forty Two"
}

View File

@@ -0,0 +1,5 @@
{
"id": "",
"title": "",
"author": null
}

View File

@@ -0,0 +1 @@
false

View File

@@ -0,0 +1,9 @@
{
"id": "10eec54d-e30a-4428-be18-01095d889126",
"title": "Time Travel",
"author": {
"first_name": "Doc",
"last_name": "Brown"
},
"date": "2021-10-27T22:30:34Z"
}

View File

@@ -0,0 +1 @@
42

View File

@@ -0,0 +1,4 @@
{
"id": "10eec54d-e30a-4428-be18-01095d889126",
"title": "Time Travel"
}

View File

@@ -0,0 +1 @@
"hello world"

View File

@@ -0,0 +1 @@
true

View File

@@ -0,0 +1 @@
<Comic id="2fd5af35-b85e-4f03-8eba-524be28d7a5b" issue="Forty Two">Hello World!</Comic>

View File

@@ -0,0 +1,4 @@
<Book>
<id></id>
<title></title>
</Book>

View File

@@ -0,0 +1 @@
<bool>false</bool>

View File

@@ -0,0 +1,9 @@
<Book>
<id>cfda163c-d5c1-44a2-909b-5d2ce3a31979</id>
<title>The Traveler</title>
<author>
<first_name>John</first_name>
<last_name>Twelve Hawks</last_name>
</author>
<year>2005</year>
</Book>

View File

@@ -0,0 +1 @@
<int>42</int>

View File

@@ -0,0 +1,4 @@
<Book>
<id>cfda163c-d5c1-44a2-909b-5d2ce3a31979</id>
<title>The Traveler</title>
</Book>

View File

@@ -0,0 +1 @@
<string>hello world</string>

View File

@@ -0,0 +1 @@
<bool>true</bool>

View File

@@ -0,0 +1 @@
<Comic id="2fd5af35-b85e-4f03-8eba-524be28d7a5b" issue="Forty Two">Hello World!</Comic>

View File

@@ -0,0 +1,4 @@
<Article>
<id></id>
<title></title>
</Article>

View File

@@ -0,0 +1 @@
<bool>false</bool>

View File

@@ -0,0 +1,9 @@
<Article>
<id>10eec54d-e30a-4428-be18-01095d889126</id>
<title>Time Travel</title>
<author>
<first_name>Doc</first_name>
<last_name>Brown</last_name>
</author>
<date>2021-10-27T22:30:34Z</date>
</Article>

View File

@@ -0,0 +1 @@
<int>42</int>

View File

@@ -0,0 +1,4 @@
<Book>
<id>10eec54d-e30a-4428-be18-01095d889126</id>
<title>Time Travel</title>
</Book>

View File

@@ -0,0 +1 @@
<string>hello world</string>

View File

@@ -0,0 +1 @@
<bool>true</bool>

View File

@@ -0,0 +1,2 @@
2fd5af35-b85e-4f03-8eba-524be28d7a5b:
Hello World!: Forty Two

View File

@@ -0,0 +1,2 @@
id: ""
title: ""

View File

@@ -0,0 +1 @@
false

View File

@@ -0,0 +1,6 @@
id: cfda163c-d5c1-44a2-909b-5d2ce3a31979
title: The Traveler
author:
first_name: John
last_name: Twelve Hawks
year: 2005

View File

@@ -0,0 +1 @@
42

View File

@@ -0,0 +1,2 @@
id: cfda163c-d5c1-44a2-909b-5d2ce3a31979
title: The Traveler

View File

@@ -0,0 +1 @@
hello world

View File

@@ -0,0 +1 @@
true

View File

@@ -0,0 +1,2 @@
2fd5af35-b85e-4f03-8eba-524be28d7a5b:
Hello World!: Forty Two

View File

@@ -0,0 +1,3 @@
id: ""
title: ""
author: null

View File

@@ -0,0 +1 @@
false

View File

@@ -0,0 +1,6 @@
id: 10eec54d-e30a-4428-be18-01095d889126
title: Time Travel
author:
first_name: Doc
last_name: Brown
date: 2021-10-27T22:30:34Z

View File

@@ -0,0 +1 @@
42

View File

@@ -0,0 +1,2 @@
id: 10eec54d-e30a-4428-be18-01095d889126
title: Time Travel

View File

@@ -0,0 +1 @@
hello world

View File

@@ -0,0 +1 @@
true

View File

@@ -0,0 +1,3 @@
{
"2fd5af35-b85e-4f03-8eba-524be28d7a5b": "Hello World!=Forty Two"
}

View File

@@ -0,0 +1,4 @@
{
"id": "",
"title": ""
}

View File

@@ -0,0 +1 @@
false

View File

@@ -0,0 +1,9 @@
{
"id": "cfda163c-d5c1-44a2-909b-5d2ce3a31979",
"title": "The Traveler",
"author": {
"first_name": "John",
"last_name": "Twelve Hawks"
},
"year": 2005
}

View File

@@ -0,0 +1 @@
42

View File

@@ -0,0 +1,4 @@
{
"id": "cfda163c-d5c1-44a2-909b-5d2ce3a31979",
"title": "The Traveler"
}

View File

@@ -0,0 +1 @@
"hello world"

View File

@@ -0,0 +1 @@
true

View File

@@ -0,0 +1,3 @@
{
"2fd5af35-b85e-4f03-8eba-524be28d7a5b": "Hello World!=Forty Two"
}

View File

@@ -0,0 +1,5 @@
{
"id": "",
"title": "",
"author": null
}

View File

@@ -0,0 +1 @@
false

View File

@@ -0,0 +1,9 @@
{
"id": "10eec54d-e30a-4428-be18-01095d889126",
"title": "Time Travel",
"author": {
"first_name": "Doc",
"last_name": "Brown"
},
"date": "2021-10-27T22:30:34Z"
}

View File

@@ -0,0 +1 @@
42

View File

@@ -0,0 +1,4 @@
{
"id": "10eec54d-e30a-4428-be18-01095d889126",
"title": "Time Travel"
}

View File

@@ -0,0 +1 @@
"hello world"

View File

@@ -0,0 +1 @@
true

View File

@@ -0,0 +1 @@
<Comic id="2fd5af35-b85e-4f03-8eba-524be28d7a5b" issue="Forty Two">Hello World!</Comic>

View File

@@ -0,0 +1,4 @@
<Book>
<id></id>
<title></title>
</Book>

View File

@@ -0,0 +1 @@
<bool>false</bool>

View File

@@ -0,0 +1,9 @@
<Book>
<id>cfda163c-d5c1-44a2-909b-5d2ce3a31979</id>
<title>The Traveler</title>
<author>
<first_name>John</first_name>
<last_name>Twelve Hawks</last_name>
</author>
<year>2005</year>
</Book>

View File

@@ -0,0 +1 @@
<int>42</int>

View File

@@ -0,0 +1,4 @@
<Book>
<id>cfda163c-d5c1-44a2-909b-5d2ce3a31979</id>
<title>The Traveler</title>
</Book>

View File

@@ -0,0 +1 @@
<string>hello world</string>

View File

@@ -0,0 +1 @@
<bool>true</bool>

View File

@@ -0,0 +1 @@
<Comic id="2fd5af35-b85e-4f03-8eba-524be28d7a5b" issue="Forty Two">Hello World!</Comic>

View File

@@ -0,0 +1,4 @@
<Article>
<id></id>
<title></title>
</Article>

View File

@@ -0,0 +1 @@
<bool>false</bool>

View File

@@ -0,0 +1,9 @@
<Article>
<id>10eec54d-e30a-4428-be18-01095d889126</id>
<title>Time Travel</title>
<author>
<first_name>Doc</first_name>
<last_name>Brown</last_name>
</author>
<date>2021-10-27T22:30:34Z</date>
</Article>

View File

@@ -0,0 +1 @@
<int>42</int>

View File

@@ -0,0 +1,4 @@
<Book>
<id>10eec54d-e30a-4428-be18-01095d889126</id>
<title>Time Travel</title>
</Book>

View File

@@ -0,0 +1 @@
<string>hello world</string>

View File

@@ -0,0 +1 @@
<bool>true</bool>

View File

@@ -0,0 +1,2 @@
2fd5af35-b85e-4f03-8eba-524be28d7a5b:
Hello World!: Forty Two

View File

@@ -0,0 +1,2 @@
id: ""
title: ""

View File

@@ -0,0 +1 @@
false

View File

@@ -0,0 +1,6 @@
id: cfda163c-d5c1-44a2-909b-5d2ce3a31979
title: The Traveler
author:
first_name: John
last_name: Twelve Hawks
year: 2005

View File

@@ -0,0 +1 @@
42

View File

@@ -0,0 +1,2 @@
id: cfda163c-d5c1-44a2-909b-5d2ce3a31979
title: The Traveler

View File

@@ -0,0 +1 @@
hello world

View File

@@ -0,0 +1 @@
true

View File

@@ -0,0 +1,2 @@
2fd5af35-b85e-4f03-8eba-524be28d7a5b:
Hello World!: Forty Two

View File

@@ -0,0 +1,3 @@
id: ""
title: ""
author: null

View File

@@ -0,0 +1 @@
false

Some files were not shown because too many files have changed in this diff Show More