Skip to content

Commit

Permalink
Merge pull request #1 from perrito666/horacio/jsondiff
Browse files Browse the repository at this point in the history
  • Loading branch information
perrito666 authored Nov 19, 2021
2 parents 635a098 + feafe8e commit 1da9ddb
Show file tree
Hide file tree
Showing 6 changed files with 350 additions and 116 deletions.
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,19 @@ func TestMain(m *testing.M) {

Once included, if the update `-u` flag is used when running tests, any snapshot that is no longer in use will be removed. Note: if a single test is run, pruning _will not occur_.

Alternatively `CleanupOrFail` can be used to fail a test run if a snapshot needs cleaning up but the `-u` flag wasn't given (and it's not a single-test run):

```go
func TestMain(m *testing.M) {
if m.Run() == 0 {
if err := abide.CleanupOrFail(); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
}
}
```

## Snapshots

A snapshot is essentially a lock file for an http response. Instead of having to manually compare every aspect of an http response to it's expected value, it can be automatically generated and used for matching in subsequent testing.
Expand Down Expand Up @@ -105,4 +118,4 @@ To write snapshots to a directory other than the default `__snapshot__`, adjust
func init() {
abide.SnapshotDir = "testdata"
}
```
```
35 changes: 35 additions & 0 deletions abide.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ const (
snapshotSeparator = "/* snapshot: "
)

type SnapshotType string

const (
// SnapshotGeneric represents a snapshot whose contents we assume have no known format.
SnapshotGeneric SnapshotType = ""
// SnapshotHTTPRespJSON represents a snapshot whose contents are an HTTP response with content type JSON.
SnapshotHTTPRespJSON SnapshotType = "HTTPContentTypeJSON"
)

func init() {
// Get arguments
args = getArguments()
Expand All @@ -50,6 +59,32 @@ func Cleanup() error {
return allSnapshots.save()
}

// CleanupOrFail is an optional method which will behave like
// Cleanup() if the `-u` flag was given, but which returns an error if
// `-u` was not given and there were things to clean up.
func CleanupOrFail() error {
if args.singleRun {
return nil
}
if args.shouldUpdate {
return Cleanup()
}

failed := 0
for _, s := range allSnapshots {
if !s.evaluated {
failed++
fmt.Fprintf(os.Stderr, "Unused snapshot `%s`\n", s.id)
}
}

if failed > 0 {
return fmt.Errorf("%d unused snapshots", failed)
}

return nil
}

// snapshotID represents the unique identifier for a snapshot.
type snapshotID string

Expand Down
57 changes: 55 additions & 2 deletions abide_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package abide

import (
"fmt"
"os"
"reflect"
"testing"
Expand All @@ -22,7 +23,7 @@ func testingSnapshot(id, value string) *snapshot {
func testingSnapshots(count int) snapshots {
s := make(snapshots, count)
for i := 0; i < count; i++ {
id := string(i)
id := string(rune(i))
s[snapshotID(id)] = testingSnapshot(id, id)
}
return s
Expand All @@ -34,6 +35,8 @@ func TestCleanup(t *testing.T) {
_ = testingSnapshot("1", "A")

// If shouldUpdate = false, the snapshot must remain.
args.shouldUpdate = false
args.singleRun = false
err := Cleanup()
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -69,13 +72,63 @@ func TestCleanup(t *testing.T) {
}
}

func TestCleanupOrFail(t *testing.T) {
defer testingCleanup()

_ = testingSnapshot("1", "A")

args.shouldUpdate = false
args.singleRun = true
// singleRun means no cleanup
err := CleanupOrFail()
if err != nil {
t.Fatal(err)
}

// shouldUpdate=false and singleRun=false -> CleanupOrFail fails
args.singleRun = false
err = CleanupOrFail()
if fmt.Sprint(err) != "1 unused snapshots" {
t.Fatalf("expected `1 unused snapshots`, got %v", err)
}

err = loadSnapshots()
if err != nil {
t.Fatal(err)
}

snapshot := getSnapshot("1")
if snapshot == nil {
t.Fatal("Expected snapshot[1] to exist.")
}

// If shouldUpdate = true and singleRun = false, the snapshot must be removed.
args.shouldUpdate = true
args.singleRun = false
err = CleanupOrFail()
if err != nil {
t.Fatal(err)
}

// call private reloadSnapshots to repeat once-executing function
err = reloadSnapshots()
if err != nil {
t.Fatal(err)
}

snapshot = getSnapshot("1")
if snapshot != nil {
t.Fatal("Expected snapshot[1] to be removed.")
}
}

func TestCleanupUpdate(t *testing.T) {
defer testingCleanup()

// this snapshot is updated, should be evaluated, and not removed
_ = testingSnapshot("1", "A")
t2 := &testing.T{}
createOrUpdateSnapshot(t2, "1", "B")
createOrUpdateSnapshot(t2, "1", "B", SnapshotGeneric)

// this snapshot is never evaluated, and should be removed
_ = testingSnapshot("2", "B")
Expand Down
122 changes: 12 additions & 110 deletions assert.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
package abide

import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httputil"
"strings"
"testing"

"github.com/beme/abide/internal"
"github.com/sergi/go-diff/diffmatchpatch"
)

Expand All @@ -22,108 +18,7 @@ type Assertable interface {
// Assert asserts the value of an object with implements Assertable.
func Assert(t *testing.T, id string, a Assertable) {
data := a.String()
createOrUpdateSnapshot(t, id, data)
}

// AssertHTTPResponse asserts the value of an http.Response.
func AssertHTTPResponse(t *testing.T, id string, w *http.Response) {
body, err := httputil.DumpResponse(w, true)
if err != nil {
t.Fatal(err)
}

assertHTTP(t, id, body, contentTypeIsJSON(w.Header.Get("Content-Type")))
}

// AssertHTTPRequestOut asserts the value of an http.Request.
// Intended for use when testing outgoing client requests
// See https://golang.org/pkg/net/http/httputil/#DumpRequestOut for more
func AssertHTTPRequestOut(t *testing.T, id string, r *http.Request) {
body, err := httputil.DumpRequestOut(r, true)
if err != nil {
t.Fatal(err)
}

assertHTTP(t, id, body, contentTypeIsJSON(r.Header.Get("Content-Type")))
}

// AssertHTTPRequest asserts the value of an http.Request.
// Intended for use when testing incoming client requests
// See https://golang.org/pkg/net/http/httputil/#DumpRequest for more
func AssertHTTPRequest(t *testing.T, id string, r *http.Request) {
body, err := httputil.DumpRequest(r, true)
if err != nil {
t.Fatal(err)
}

assertHTTP(t, id, body, contentTypeIsJSON(r.Header.Get("Content-Type")))
}

func assertHTTP(t *testing.T, id string, body []byte, isJSON bool) {
config, err := getConfig()
if err != nil {
t.Fatal(err)
}

data := string(body)
lines := strings.Split(strings.TrimSpace(data), "\n")

if config != nil {
// empty line identifies the end of the HTTP header
for i, line := range lines {
if line == "" {
break
}

headerItem := strings.Split(line, ":")
if def, ok := config.Defaults[headerItem[0]]; ok {
lines[i] = fmt.Sprintf("%s: %s", headerItem[0], def)
}
}
}

// If the response body is JSON, indent.
if isJSON {
jsonStr := lines[len(lines)-1]

var jsonIface map[string]interface{}
err = json.Unmarshal([]byte(jsonStr), &jsonIface)
if err != nil {
t.Fatal(err)
}

// Clean/update json based on config.
if config != nil {
for k, v := range config.Defaults {
jsonIface = internal.UpdateKeyValuesInMap(k, v, jsonIface)
}
}

out, err := json.MarshalIndent(jsonIface, "", " ")
if err != nil {
t.Fatal(err)
}
lines[len(lines)-1] = string(out)
}

data = strings.Join(lines, "\n")
createOrUpdateSnapshot(t, id, data)
}

func contentTypeIsJSON(contentType string) bool {
contentTypeParts := strings.Split(contentType, ";")
firstPart := contentTypeParts[0]

isPlainJSON := firstPart == "application/json"
if isPlainJSON {
return isPlainJSON
}

isVendor := strings.HasPrefix(firstPart, "application/vnd.")

isJSON := strings.HasSuffix(firstPart, "+json")

return isVendor && isJSON
createOrUpdateSnapshot(t, id, data, SnapshotGeneric)
}

// AssertReader asserts the value of an io.Reader.
Expand All @@ -133,10 +28,10 @@ func AssertReader(t *testing.T, id string, r io.Reader) {
t.Fatal(err)
}

createOrUpdateSnapshot(t, id, string(data))
createOrUpdateSnapshot(t, id, string(data), SnapshotGeneric)
}

func createOrUpdateSnapshot(t *testing.T, id, data string) {
func createOrUpdateSnapshot(t *testing.T, id, data string, format SnapshotType) {
var err error
snapshot := getSnapshot(snapshotID(id))

Expand All @@ -156,7 +51,14 @@ func createOrUpdateSnapshot(t *testing.T, id, data string) {
}

snapshot.evaluated = true
diff := compareResults(t, snapshot.value, strings.TrimSpace(data))
var diff string
switch format {
case SnapshotHTTPRespJSON:
diff = compareResultsHTTPRequestJSON(t, snapshot.value, strings.TrimSpace(data))
default:
diff = compareResults(t, snapshot.value, strings.TrimSpace(data))
}

if diff != "" {
if snapshot != nil && args.shouldUpdate {
fmt.Printf("Updating snapshot `%s`\n", id)
Expand All @@ -176,7 +78,7 @@ func compareResults(t *testing.T, existing, new string) string {
dmp := diffmatchpatch.New()
dmp.PatchMargin = 20
allDiffs := dmp.DiffMain(existing, new, false)
nonEqualDiffs := []diffmatchpatch.Diff{}
var nonEqualDiffs []diffmatchpatch.Diff
for _, diff := range allDiffs {
if diff.Type != diffmatchpatch.DiffEqual {
nonEqualDiffs = append(nonEqualDiffs, diff)
Expand Down
Loading

0 comments on commit 1da9ddb

Please sign in to comment.