Giter Club home page Giter Club logo

onpar's Introduction

onpar

docs gha

Parallel testing framework for Go

Goals

  • Provide structured testing, with per-spec setup and teardown.
  • Discourage using closure state to share memory between setup/spec/teardown functions.
    • Sharing memory between the steps of a spec by using closure state means that you're also sharing memory with other tests. This often results in test pollution.
  • Run tests in parallel by default.
    • Most of the time, well-written unit tests are perfectly capable of running in parallel, and sometimes running tests in parallel can uncover extra bugs. This should be the default.
  • Work within standard go test functions, simply wrapping standard t.Run semantics.
    • onpar should not feel utterly alien to people used to standard go testing. It does some extra work to allow structured tests, but for the most part it isn't hiding any complicated logic - it mostly just calls t.Run.

Onpar provides a BDD style of testing, similar to what you might find with something like ginkgo or goconvey. The biggest difference between onpar and its peers is that a BeforeEach function in onpar may return a value, and that value will become the parameter required in child calls to Spec, AfterEach, and BeforeEach.

This allows you to write tests that share memory between BeforeEach, Spec, and AfterEach functions without sharing memory with other tests. When used properly, this makes test pollution nearly impossible and makes it harder to write flaky tests.

Running

After constructing a top-level *Onpar, defer o.Run().

If o.Run() is never called, the test will panic during t.Cleanup. This is to prevent false passes when o.Run() is accidentally omitted.

Assertions

OnPar provides an expectation library in the expect sub-package. Here is some more information about Expect and some of the matchers that are available:

However, OnPar is not opinionated - any assertion library or framework may be used within specs.

Specs

Test assertions are done within a Spec() function. Each Spec has a name and a function with a single argument. The type of the argument is determined by how the suite was constructed: New() returns a suite that takes a *testing.T, while BeforeEach constructs a suite that takes the return type of the setup function.

Each Spec is run in parallel (t.Parallel() is invoked for each spec before calling the given function).

func TestSpecs(t *testing.T) {
    type testContext struct {
        t *testing.T
        a int
        b float64
    }

    o := onpar.BeforeEach(onpar.New(t), func(t *testing.T) testContext {
        return testContext{t: t, a: 99, b: 101.0}
    })
    defer o.Run()

    o.AfterEach(func(tt testContext) {
            // ...
    })

    o.Spec("something informative", func(tt testContext) {
        if tt.a != 99 {
            tt.t.Errorf("%d != 99", tt.a)
        }
    })
}

Serial Specs

While onpar is intended to heavily encourage running specs in parallel, we recognize that that's not always an option. Sometimes proper mocking is just too time consuming, or a singleton package is just too hard to replace with something better.

For those times that you just can't get around the need for serial tests, we provide SerialSpec. It works exactly the same as Spec, except that onpar doesn't call t.Parallel before running it.

Grouping

Groups are used to keep Specs in logical place. The intention is to gather each Spec in a reasonable place. Each Group may construct a new child suite using BeforeEach.

func TestGrouping(t *testing.T) {
    type topContext struct {
        t *testing.T
        a int
        b float64
    }

    o := onpar.BeforeEach(onpar.New(t), func(t *testing.T) topContext {
        return topContext{t: t, a: 99, b: 101}
    }
    defer o.Run()

    o.Group("some-group", func() {
        type groupContext struct {
            t *testing.T
            s string
        }
        o := onpar.BeforeEach(o, func(tt topContext) groupContext {
            return groupContext{t: tt.t, s: "foo"}
        })

        o.AfterEach(func(tt groupContext) {
            // ...
        })

        o.Spec("something informative", func(tt groupContext) {
            // ...
        })
    })
}

Run Order

Each BeforeEach() runs before any Spec in the same Group. It will also run before any sub-group Specs and their BeforeEaches. Any AfterEach() will run after the Spec and before parent AfterEaches.

func TestRunOrder(t *testing.T) {
    type topContext struct {
        t *testing.T
        i int
        s string
    }
    o := onpar.BeforeEach(onpar.New(t), func(t *testing.T) topContext {
        // Spec "A": Order = 1
        // Spec "B": Order = 1
        // Spec "C": Order = 1
        return topContext{t: t, i: 99, s: "foo"}
    })
    defer o.Run()

    o.AfterEach(func(tt topContext) {
        // Spec "A": Order = 4
        // Spec "B": Order = 6
        // Spec "C": Order = 6
    })

    o.Group("DA", func() {
        o.AfterEach(func(tt topContext) {
            // Spec "A": Order = 3
            // Spec "B": Order = 5
            // Spec "C": Order = 5
        })

        o.Spec("A", func(tt topContext) {
            // Spec "A": Order = 2
        })

        o.Group("DB", func() {
            type dbContext struct {
                t *testing.T
                f float64
            }
            o.BeforeEach(func(tt topContext) dbContext {
                // Spec "B": Order = 2
                // Spec "C": Order = 2
                return dbContext{t: tt.t, f: 101}
            })

            o.AfterEach(func(tt dbContext) {
                // Spec "B": Order = 4
                // Spec "C": Order = 4
            })

            o.Spec("B", func(tt dbContext) {
                // Spec "B": Order = 3
            })

            o.Spec("C", func(tt dbContext) {
                // Spec "C": Order = 3
            })
        })

        o.Group("DC", func() {
            o.BeforeEach(func(tt topContext) *testing.T {
                // Will not be invoked (there are no specs)
            })

            o.AfterEach(func(t *testing.T) {
                // Will not be invoked (there are no specs)
            })
        })
    })
}

Avoiding Closure

Why bother with returning values from a BeforeEach? To avoid closure of course! When running Specs in parallel (which they always do), each variable needs a new instance to avoid race conditions. If you use closure, then this gets tough. So onpar will pass the arguments to the given function returned by the BeforeEach.

The BeforeEach is a gatekeeper for arguments. The returned values from BeforeEach are required for the following Specs. Child Groups are also passed what their direct parent BeforeEach returns.

onpar's People

Contributors

bradylove avatar gitter-badger avatar jasonkeene avatar nelsam avatar poy avatar tripledogdare avatar wfernandes avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

onpar's Issues

Double Path Separators Without Groups

I'm avoiding groups as much as possible in my tests, instead grouping using test functions. It looks like using o.Spec without o.Group is causing double path separators.

Sample test:

func TestFoo(t *testing.T) {
    o := onpar.New()
    defer o.Run(t)
    o.BeforeEach(func(t *testing.T) (*testing.T, foo.Foo) {
        return t, foo.NewFoo()
    })
    o.Spec("Bar", testBar)
}

func testBar(t *testing.T, foo foo.Foo) {
    if foo.Bar() != expectedBar {
        t.Errorf("Expected %v to equal %v", foo.Bar(), expectedBar)
    }
}

The output ends up being:

=== RUN   TestFoo//Bar

I haven't tracked down what is causing the duplicate, but it's there. Not a deal breaker by any means, but a little odd nonetheless.

expect: make `To` return something that can be used to set some output options

The specific use case that we need to tackle here is when the output is MASSIVE and it needs to be separated onto multiple lines. This isn't an option that is needed consistently, but more on a case-by-case basis.

expect(a).To(equal(b)).FormatOutput(SeparateLines())

Acceptance:

  1. Write a test without any output formatting
  2. Confirm that it prints expected and actual on the same line
  3. Write a test with output formatting to put the expected and actual on separate lines
  4. Confirm that it prints expected and actual on separate lines

Need `Fetcher` Matcher

There are times when you need the actual value out to do something with. This is similar to the Gomega functionality when a Receive matcher would take a pointer.

ReceiveMatcher should read from a channel with a timeout

The ReceiveMatcher will return an error if a channel is empty. This means that you have to use ViaPollingMatcher to make it useful in most cases. ReceiveMatcher would be more useful if it waited a period of time before returning an error.

Not matcher doesn't provide descriptive output

When running the following assertion,

Expect(t, fmt.Errorf("some error")).To(Not(HaveOccurred()))

we get the output below

$ go test
--- FAIL: TestSomething (0.00s)
	expect.go:44: match matchers.HaveOccurredMatcher{}
		/Users/wfernandes/workspace/go/src/test/some_test.go:14

match matchers.HaveOccurredMatcher{} is not as useful when it comes to understanding what originally failed. It would be nice to see the failure from the HaveOccurred matcher.

string differ either hangs or does *way* too much work

My tests seem to hang, and I've traced it to the Differ computing the shortest diff between the following strings

Actual:
{"current":[{"kind":0,"at":{"seconds":1596288863,"nanos":13000000},"msg":"Something bad happened."}]}

Expected
{"current": [{"kind": "GENERIC", "at": "2020-08-01T13:34:23.013Z", "msg": "Something bad happened."}], "history": []}

Note these are the literal strings, not escaped like they would be in go source.

Single process for many tests support

We often write a test that will launch a process, make assertions against it and then kill the process. This will be difficult with running parallel tests. Processes can be launched via an init func, but killing them is a different story. OnPar should offer a solution that wraps the process with a finalizer.

[Matchers] ReceiveMatcher can cause falsely passing tests

I'm really not sure of the details of how this happens, but the short version is that when I revert a fix I made to my code and adjust this test to use not(viaPolling(receive())), the test still passes. Without the bug fix in place, it should be failing - and it does fail with the custom receive matcher that I have in that file.

[Note: I'm using local variables to alias matchers 'cause I hate dot imports, but using package selectors causes some stumbles in readability]

What I'm expecting to happen:

  • not will invert the result
  • viaPolling will return the first time there is a success
  • receive will return a success if it can read from the channel

This should result in a failing test the first time the channel has a value that can be read during the polling phase. The reality is that even though a value is available, the default case is always chosen - so my tests pass no matter how long I set the ViaPolling duration to, even though my code is broken.

Again, I really can't understand the details, because I've tried to reproduce with simpler cases in the playground and haven't had any luck. Maybe it has to do with the code in hel using reflect to generate a select statement? But it definitely happens consistently. I can occasionally reproduce the problem with my custom receiveMatcher if I set the timeout to 0 - I get some false passes there, too, albeit much less often.

MatchJSON only accepts JSON Object at top level.

The MatchJSON matcher only accepts a JSON object at the top level, however it is valid to have an array, string, number and boolean as well.

Example test:

package command_test

import (
        "testing"

        "github.com/poy/onpar"
        . "github.com/poy/onpar/expect"
        . "github.com/poy/onpar/matchers"
)

func TestSampleJSON(t *testing.T) {
        o := onpar.New()
        defer o.Run(t)

        o.Spec("example JSON array", func(t *testing.T) {
                Expect(t, `[{"name":"test"}]`).To(MatchJSON(`[{"name":"test"}]`))
        })

        o.Spec("example JSON string", func(t *testing.T) {
                Expect(t, `"valid"`).To(MatchJSON(`"valid"`))
        })

        o.Spec("example JSON number", func(t *testing.T) {
                Expect(t, `234`).To(MatchJSON(`234`))
        })

        o.Spec("example JSON bool", func(t *testing.T) {
                Expect(t, `true`).To(MatchJSON(`true`))
        })
}

Results:

--- FAIL: TestSampleJSON (0.00s)
    --- FAIL: TestSampleJSON/example_JSON_string (0.00s)
        sample_test.go:20: Error with "valid": json: cannot unmarshal string into Go value of type map[string]interface {}
            sample_test.go:20
    --- FAIL: TestSampleJSON/example_JSON_array (0.00s)
        sample_test.go:16: Error with [{"name":"test"}]: json: cannot unmarshal array into Go value of type map[string]interface {}
            sample_test.go:16
    --- FAIL: TestSampleJSON/example_JSON_bool (0.00s)
        sample_test.go:28: Error with true: json: cannot unmarshal bool into Go value of type map[string]interface {}
            sample_test.go:28
    --- FAIL: TestSampleJSON/example_JSON_number (0.00s)
        sample_test.go:24: Error with 234: json: cannot unmarshal number into Go value of type map[string]interface {}
            sample_test.go:24

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.