correct

module
v0.0.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 18, 2024 License: MPL-2.0

README

correct

Mission: to cleanly express the inputs and outputs that prove a piece of software as correct.

Correct is a collection of assertion libraries for go, intended to be used together. We try not to strictly enforce that, though - most of correct should be customizeable either by changing some options or by using only some sub-packages of correct.

Quick start

$ go get git.sr.ht/~nelsam/correct@latest

Simple assertions are pretty obvious:

func TestFoo(t *testing.T) {
    val := somepkg.SomeFn()
    it.Must(t, val, match.Equal(anExpectedValue))
}

Motivation

An assertion library has three responsibilities:

  1. Compare business values using logic that is equivalent to business logic.
    • if a value would fail a business logic comparison, it must also fail the assertion.
  2. Provide context about why a value failed a check.
    • Output must be clear about which value came from business logic, which value was expected, and why the check failed.
  3. On failure, mark the test as failed and provide a way to stop test execution.
    • If I assert that foo is a slice of length 3, then performing foo[2] on the next line must not panic the test.

Existing libraries perform all of these with various levels of success, but I have never found a library that performs them all well. And over time, I've come to the conclusion that the unix philosophy is a good way of building a package - each package should "do one thing and do it well". Assertion libraries usually conflate some or all of these concerns.

So I set forth with the following goals:

  • Separate these concerns into separate sub-packages.
    • One package to format test results.
    • One package to compare values.
    • One package to handle the test result, printing the result's description and handling the result's passed/failed status.
  • Make the simplest checks clear, clean, and obvious.
  • Make the complex checks possible without implementing several methods on a matcher type.
  • Never leave it ambiguous which value was the actual and which was the expected.

How we implement these goals

One package to format test results

result is a package of result types which can describe match results. Result types will describe the match using a theme, which may be set during init() with result.InitDefaultTheme or overridden in the context.Context.

One package to compare values

match is a sub-package with match functions. These are simply functions which take an actual value and return a result. Many of them are constructed using an expected value (or set of values).

A matcher in correct is any function which takes a single parameter and returns an it.Result, so implementing custom matchers is as easy as:

func HasUserID(id string) func(*User) it.Result {
    return func(u *User) it.Result {
        if u.ID != id {
            return result.Compare(false, u, "does not have ID", id)
        }
        return result.Compare(true, u, "has ID", id)
    }
}

One package to wrap everything up and mark the test as failed

it contains functions which take actuals and match functions, track results, and call methods on the *testing.T (usually Fatalf). Extra options, like telling the assertion to print on success as well as failure, exist as functional options.

Generally, you'll use something like it.Must(t, foo, match.Equal("bar")).

Never leave it ambiguous which value was the actual and which was the expected

Functions in it always take an actual and a match function. The match function may be constructed around expected values, but it should never be ambiguous that those values are constructing the match function.

Simplest checks are clear, clean, and obvious

None of these simple assertions should look complicated or confusing:

func TestFoo(t *testing.T) {
    var s []string
    err := json.Unmarshal(someJSONArray, &s)
    it.Must(t, err, match.NotErr)
    it.Must(t, s, match.HaveSliceLen[string](10))
    it.Must(t, s[9], match.Equal("foo"))
}

Complex checks are possible without complex custom matchers

Match function

In correct, matchers are just functions. This means that a custom matcher can be defined in a closure - no need to share outside of your test function. You don't even need to assign the matcher to a variable if you don't want to - just define the closure in the call directly.

func TestFoo(t *testing.T) {
    someVal := someFn()
    it.Must(t, someVal, func(v foo.SomeType) it.Result {
        if v.Status == statusFailed {
            return result.Simple(false, v, "has a failed status")
        }
        return result.Simple(true, v, "does not have a failed status")
    })
}
Use Map to extract data from complex types

Map, inspired by functional programming map functions like elm's Maybe.map, provides a powerful method of performing a match against a complex value. For example, here is a test that wraps up multiple Map assertions in All to check the response code, Authorization header, and JSON body of an HTTP response:

func TestHTTP(t *testing.T) {
    code := match.Map(func(r *http.Response) int {
        return r.StatusCode
    })
    authHeader := match.Map(
        func(r *http.Response) string {
            return resp.Header.Get("Authorization")
        },
        match.DescribeMap("response auth header"),
    )
    jsonBody := match.Map(
        func(r *http.Response) map[string]any {
            m := make(map[string]any)
            err := json.NewDecoder(resp.Body).Decode(&m)
            it.Must(t, err, match.NotErr)
            return m
        },
        match.DescribeMap("response JSON body")
    )
    resp, err := somepkg.DoRequest()
    it.Must(t, err, match.NotErr)
    it.Must(t, resp, match.All(
        code(match.Equal(http.StatusOK)),
        authHeader(match.Equal("some-auth-header")),
        jsonBody(match.DeepEqual(map[string]any{"jsonKey": "jsonVal"})),
    ))
}

The only custom code above is the set of functions that we pass to Map - which are easy enough to define as variables in our test function.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL