gregoryv/asserter

View on GitHub
assert.go

Summary

Maintainability
A
3 hrs
Test Coverage
package asserter

import (
    "bytes"
    "io"
    "io/ioutil"
    "net/http"
    "reflect"
    "strconv"
    "strings"

    "github.com/sergi/go-diff/diffmatchpatch"
)

type T interface {
    Helper()
    Error(...interface{})
    Errorf(string, ...interface{})
    Fatal(...interface{})
    Fatalf(string, ...interface{})
    Fail()
    FailNow()
    Log(...interface{})
    Logf(string, ...interface{})
}

type Asserter interface {
    Assert(...bool) Testar
    Equals(got, exp interface{}) T
    NotEqual(a, b interface{}) T
    Contains(body, exp interface{}) T
    ResponseFrom(http.Handler) *HttpResponse
    Errors() (ok, bad AssertErrFunc)
    Mixed() (ok, bad MixedErrFunc)
}

// Testar combines testing.T with and Asserter
type Testar interface {
    T
    Asserter
}

func Wrap(t T) *WrappedT {
    return &WrappedT{t}
}

// New returns a WrappedT.Assert func for online assertions.
func New(t T) AssertFunc {
    return Wrap(t).Assert
}

type AssertFunc func(expr ...bool) Testar

type WrappedT struct {
    T
}

func (w *WrappedT) Assert(expr ...bool) Testar {
    if len(expr) > 1 {
        w.T.Helper()
        w.T.Fatal("Only 0 or 1 bool expressions are allowed")
    }
    if len(expr) == 0 || !expr[0] {
        return w
    }
    return &noopT{}
}

func (w *WrappedT) Helper() {
    /* Cannot use the asserter as helper */
}

func (w *WrappedT) Error(args ...interface{}) {
    w.T.Helper()
    w.T.Error(args...)
}

func (w *WrappedT) Errorf(format string, args ...interface{}) {
    w.T.Helper()
    w.T.Errorf(format, args...)
}

func (w *WrappedT) Fatal(args ...interface{}) {
    w.T.Helper()
    w.T.Fatal(args...)
}

func (w *WrappedT) Fatalf(format string, args ...interface{}) {
    w.T.Helper()
    w.T.Fatalf(format, args...)
}

func (w *WrappedT) Fail() {
    w.T.Helper()
    w.T.Fail()
}

func (w *WrappedT) FailNow() {
    w.T.Helper()
    w.T.FailNow()
}
func (w *WrappedT) Log(args ...interface{}) {
    w.T.Helper()
    w.T.Log(args...)
}

func (w *WrappedT) Logf(format string, args ...interface{}) {
    w.T.Helper()
    w.T.Logf(format, args...)
}

// Helpers

// Equals fails with error if got is different from exp.
// Returns T for e.g. logging extra or calling Fatal if needed.
func (w *WrappedT) Equals(got, exp interface{}) T {
    w.T.Helper()
    if !reflect.DeepEqual(got, exp) {
        got := string(toBytes(w.T, got, "got"))
        exp := string(toBytes(w.T, exp, "exp"))

        if strings.Contains(got, "\n") || strings.Contains(exp, "\n") {
            w.Errorf("\n%s", diff(string(got), string(exp)))
            return w.T
        }
        w.Errorf("\ngot: %v\nexp: %v", got, exp)
    }
    return w.T
}

func (w *WrappedT) NotEqual(a, b interface{}) T {
    w.T.Helper()

    A := string(toBytes(w.T, a, "a"))
    B := string(toBytes(w.T, b, "b"))

    if A == B {
        w.Error("a and b are equal")
        return w.T
    }
    return &noopT{}
}

func diff(got, exp string) string {
    dmp := diffmatchpatch.New()
    diffs := dmp.DiffMain(exp, got, false)
    return dmp.DiffPrettyText(diffs)
}

// Contains checks the body for the given expression.
// The body can be various types.
func (w *WrappedT) Contains(body, exp interface{}) T {
    w.T.Helper()
    b := toBytes(w.T, body, "body")
    e := toBytes(w.T, exp, "exp")

    if bytes.Index(b, e) == -1 {
        format := "%q does not contain %q"
        if bytes.Index(b, []byte("\n")) > -1 {
            format = "%s\ndoes not contain\n%s"
        }
        w.Errorf(format, string(b), string(e))
    }
    return w.T
}

// ----------------------------------------

type AssertErrFunc func(error) T

func NewErrors(t T) (ok, bad AssertErrFunc) {
    t.Helper()
    return Wrap(t).Errors()
}

func (w *WrappedT) Errors() (ok, bad AssertErrFunc) {
    w.T.Helper()
    return w.Ok, w.Bad
}

func (w *WrappedT) Ok(err error) T {
    w.T.Helper()
    if err != nil {
        w.T.Error(err)
        return w
    }
    return &noopT{}
}

func (w *WrappedT) Bad(err error) T {
    w.T.Helper()
    if err == nil {
        w.T.Error("expected error")
        return w
    }
    return &noopT{}
}

// ------------

func NewFatalErrors(t T) (ok, bad AssertErrFunc) {
    t.Helper()
    return Wrap(t).FatalErrors()
}

func (w *WrappedT) FatalErrors() (ok, bad AssertErrFunc) {
    w.T.Helper()
    return w.MustOk, w.MustBad
}

func (w *WrappedT) MustOk(err error) T {
    w.T.Helper()
    if err != nil {
        w.T.Fatal(err)
        return w
    }
    return &noopT{}
}

func (w *WrappedT) MustBad(err error) T {
    w.T.Helper()
    if err == nil {
        w.T.Fatal("expected error")
        return w
    }
    return &noopT{}
}

// ----------------------------------------

type MixedErrFunc func(interface{}, error) T

func NewMixed(t T) (ok, bad MixedErrFunc) {
    t.Helper()
    return Wrap(t).Mixed()
}

func (w *WrappedT) Mixed() (ok, bad MixedErrFunc) {
    w.T.Helper()
    return w.MixOk, w.MixBad
}

func (w *WrappedT) MixOk(any interface{}, err error) T {
    w.T.Helper()
    if err != nil {
        w.T.Error(err)
        return w
    }
    return &noopT{}
}

func (w *WrappedT) MixBad(any interface{}, err error) T {
    w.T.Helper()
    if err == nil {
        w.T.Error("expected error")
        return w
    }
    return &noopT{}
}

// ---------

func NewFatalMixed(t T) (ok, bad MixedErrFunc) {
    t.Helper()
    return Wrap(t).FatalMixed()
}

func (w *WrappedT) FatalMixed() (ok, bad MixedErrFunc) {
    w.T.Helper()
    return w.MustMixOk, w.MustMixBad
}

func (w *WrappedT) MustMixOk(any interface{}, err error) T {
    w.T.Helper()
    if err != nil {
        w.T.Fatal(err)
        return w
    }
    return &noopT{}
}

func (w *WrappedT) MustMixBad(any interface{}, err error) T {
    w.T.Helper()
    if err == nil {
        w.T.Fatal("should fail")
        return w
    }
    return &noopT{}
}

// ----------------------------------------

func (w *WrappedT) ResponseFrom(h http.Handler) *HttpResponse {
    return &HttpResponse{w.T, h}
}

func toBytes(t T, v interface{}, name string) (b []byte) {
    switch v := v.(type) {
    case []byte:
        return v
    case string:
        return []byte(v)
    case int:
        return []byte(strconv.Itoa(v))
    case io.Reader:
        return bytesOrError(v)
    }
    t.Fatalf("%s must be io.Reader, []byte, string or int", name)
    return
}

func bytesOrError(r io.Reader) []byte {
    body, err := ioutil.ReadAll(r)
    if err != nil {
        return []byte(err.Error())
    }
    return body
}

type noopT struct{}

func (t *noopT) Assert(...bool) Testar            { return t }
func (t *noopT) Helper()                          {}
func (t *noopT) Error(...interface{})             {}
func (t *noopT) Errorf(string, ...interface{})    {}
func (t *noopT) Fatal(...interface{})             {}
func (t *noopT) Fatalf(string, ...interface{})    {}
func (t *noopT) Fail()                            {}
func (t *noopT) FailNow()                         {}
func (t *noopT) Log(...interface{})               {}
func (t *noopT) Logf(string, ...interface{})      {}
func (t *noopT) Equals(got, exp interface{}) T    { return t }
func (t *noopT) NotEqual(a, b interface{}) T      { return t }
func (t *noopT) Contains(body, exp interface{}) T { return t }
func (t *noopT) ResponseFrom(h http.Handler) *HttpResponse {
    return &HttpResponse{t, h}
}
func (t *noopT) Errors() (ok, bad AssertErrFunc) {
    return func(error) T { return t },
        func(error) T { return t }
}

func (t *noopT) Mixed() (ok, bad MixedErrFunc) {
    return func(interface{}, error) T { return t },
        func(interface{}, error) T { return t }
}