diff --git a/README.md b/README.md index 5ed230f..665d7ec 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ func TestMyFunc(t *testing.T) { got, err := MyFunc() g.NoError(err) - g.Must(be.Not(be.Nil(got))) + g.MustNot(be.Nil(got)) g.Should(be.Equal(got.SomeString, "my value")) g.Should(be.SliceLen(got.SomeSlice, 3)) } @@ -50,16 +50,18 @@ func TestMyFunc_error(t *testing.T) { ### Checks -Ghost comes with two main checks: `Should` and `Must`. +Ghost comes with four main checks: `Should`, `ShouldNot`, `Must`, and `MustNot`. -`Should` checks whether an assertion has succeeded, failing the test otherwise. -Like `t.Error`, the test is allowed to proceed if the assertion fails: +`Should` and `ShouldNot` check whether an assertion has succeeded, failing the +test otherwise. Like `t.Error`, the test is allowed to proceed if the assertion +fails: ```go g.Should(be.Equal(got, want)) +g.ShouldNot(be.Nil(val)) ``` -The function also returns a boolean indicating whether the check was +Both functions also return a boolean indicating whether the check was successful, allowing you to safely chain assertion logic: ```go @@ -68,12 +70,12 @@ if g.Should(be.SliceLen(mySlice, 1)) { } ``` -`Must` works similarly, but ends test execution if the assertion does not pass, -analogous to `t.Fatal`: +`Must` and `MustNot` work similarly, but end test execution if the assertion +does not pass, analogous to `t.Fatal`: ```go g.Must(be.True(ok)) -g.Must(be.Equal(got, want)) +g.MustNot(be.Nil(val)) ``` For convenience, a `NoError` check is also available, which fails and ends test @@ -83,7 +85,7 @@ execution for non-nil errors: g.NoError(err) // Equivalent to: -g.Must(be.Not(be.Error(err))) +g.MustNot(be.Error(err)) ``` ### Assertions @@ -99,6 +101,7 @@ operations, error and panic handling, and JSON equality. ```go g.Should(be.True(true)) +g.ShouldNot(be.False(true)) g.Should(be.Equal(1+1, 2)) g.Should(be.DeepEqual([]string{"a", "b"}, []string{"a", "b"})) @@ -111,6 +114,7 @@ g.Should(be.Panic(func() { panic("oh no") })) var err error g.NoError(err) g.Must(be.Nil(err)) +g.MustNot(be.Error(err)) err = errors.New("test error: oh no") g.Should(be.Error(err)) @@ -118,6 +122,7 @@ g.Should(be.ErrorEqual(err, "test error: oh no")) g.Should(be.ErrorContaining(err, "oh no")) g.Should(be.JSONEqual(`{"b": 1, "a": 0}`, `{"a": 0, "b": 1}`)) +g.ShouldNot(be.JSONEqual(`{"a":1}`, `{"a":2}`)) ``` For the full list available, see [the documentation][godoc/be]. @@ -126,14 +131,7 @@ For the full list available, see [the documentation][godoc/be]. Ghost allows assertions to be composed into powerful expressions. -The simplest composer is `be.Not`, which negates the result of an assertion: - -```go -g.Should(be.Not(be.True(ok))) -g.Must(be.Not(be.Nil(val))) -``` - -Another composer is `be.Eventually`, which retries an assertion over time until +One composer is `be.Eventually`, which retries an assertion over time until it either succeeds or times out: ```go @@ -142,7 +140,16 @@ g.Should(be.Eventually(func() ghost.Result { }, 3*time.Second, 100*time.Millisecond)) ``` -Composers can also be composed: +Another composer is `be.Not`, which negates the result of an assertion: + +```go +g.Should(be.Not(be.True(ok))) +g.Must(be.Not(be.Nil(val))) +``` + +While `be.Not` in a simple assertion would simply be a more verbose version of +of `ShouldNot` or `MustNot`, the real benefit becomes obvious when you combine +composers together: ```go g.Should(be.Eventually(func() ghost.Result { @@ -227,6 +234,13 @@ convention: 2. "Haystack" comes before "needle". 3. All other arguments come last. +### Ghost Does Assertions + +Go's `testing` package is fantastic; Ghost doesn't try to do anything that the +standard library already does. + +Test suites, mocking, logging, and non-assertion failures are all out of scope. + [godoc]: https://pkg.go.dev/github.com/rliebz/ghost [godoc/be]: https://pkg.go.dev/github.com/rliebz/ghost/be [godoc/ghostlib]: https://pkg.go.dev/github.com/rliebz/ghost/ghostlib diff --git a/example_test.go b/example_test.go index 6c0498d..9608f4c 100644 --- a/example_test.go +++ b/example_test.go @@ -13,7 +13,7 @@ func TestExample(t *testing.T) { g := ghost.New(t) g.Should(be.True(true)) - g.Should(be.Not(be.False(true))) + g.ShouldNot(be.False(true)) g.Should(be.Equal(1+1, 2)) g.Should(be.DeepEqual([]string{"a", "b"}, []string{"a", "b"})) @@ -28,12 +28,12 @@ func TestExample(t *testing.T) { g.Should(be.StringContaining("foobar", "foo")) g.Should(be.Panic(func() { panic("oh no") })) - g.Should(be.Not(be.Panic(func() {}))) + g.ShouldNot(be.Panic(func() {})) var err error g.NoError(err) g.Must(be.Nil(err)) - g.Must(be.Not(be.Error(err))) + g.MustNot(be.Error(err)) err = errors.New("test error: oh no") g.Should(be.Error(err)) @@ -41,7 +41,7 @@ func TestExample(t *testing.T) { g.Should(be.ErrorContaining(err, "oh no")) g.Should(be.JSONEqual(`{"b": 1, "a": 0}`, `{"a": 0, "b": 1}`)) - g.Should(be.Not(be.JSONEqual(`{"a":1}`, `{"a":2}`))) + g.ShouldNot(be.JSONEqual(`{"a":1}`, `{"a":2}`)) count := 0 g.Should(be.Eventually(func() ghost.Result { diff --git a/ghost.go b/ghost.go index f2bd190..84be1c6 100644 --- a/ghost.go +++ b/ghost.go @@ -40,6 +40,22 @@ func (g Ghost) Should(result Result) bool { return true } +// ShouldNot runs an assertion that should not be successful, returning true if +// the assertion was not successful. +func (g Ghost) ShouldNot(result Result) bool { + if h, ok := g.t.(interface{ Helper() }); ok { + h.Helper() + } + + if result.Ok { + g.t.Log(result.Message) + g.t.Fail() + return false + } + + return true +} + // Must runs an assertion that must be successful, failing the test if it is not. func (g Ghost) Must(result Result) { if h, ok := g.t.(interface{ Helper() }); ok { @@ -51,6 +67,17 @@ func (g Ghost) Must(result Result) { } } +// MustNot runs an assertion that must not be successful, failing the test if it is. +func (g Ghost) MustNot(result Result) { + if h, ok := g.t.(interface{ Helper() }); ok { + h.Helper() + } + + if !g.ShouldNot(result) { + g.t.FailNow() + } +} + // NoError asserts that an error should be nil, failing the test if it is not. func (g Ghost) NoError(err error) { if h, ok := g.t.(interface{ Helper() }); ok { diff --git a/ghost_test.go b/ghost_test.go index f1c9f4c..d5d57a1 100644 --- a/ghost_test.go +++ b/ghost_test.go @@ -43,14 +43,50 @@ func TestGhost_Should(t *testing.T) { g.Should(be.False(ok)) g.Should(be.SliceLen(mockT.failNowCalls, 0)) - g.Should(be.DeepEqual( - [][]any{{msg}}, - mockT.logCalls, - )) + g.Should(be.DeepEqual(mockT.logCalls, [][]any{{msg}})) g.Should(be.SliceLen(mockT.failCalls, 1)) }) } +func TestGhost_ShouldNot(t *testing.T) { + t.Run("ok", func(t *testing.T) { + g := ghost.New(t) + + mockT := newMockT() + testG := ghost.New(mockT) + msg := "some message" + + ok := testG.ShouldNot(ghost.Result{ + Ok: true, + Message: msg, + }) + + g.Should(be.False(ok)) + g.Should(be.SliceLen(mockT.failNowCalls, 0)) + + g.Should(be.DeepEqual(mockT.logCalls, [][]any{{msg}})) + g.Should(be.SliceLen(mockT.failCalls, 1)) + }) + + t.Run("not ok", func(t *testing.T) { + g := ghost.New(t) + + mockT := newMockT() + testG := ghost.New(mockT) + msg := "some message" + + ok := testG.ShouldNot(ghost.Result{ + Ok: false, + Message: msg, + }) + + g.Should(be.True(ok)) + g.Should(be.SliceLen(mockT.logCalls, 0)) + g.Should(be.SliceLen(mockT.failCalls, 0)) + g.Should(be.SliceLen(mockT.failNowCalls, 0)) + }) +} + func TestGhost_Must(t *testing.T) { t.Run("ok", func(t *testing.T) { g := ghost.New(t) @@ -81,11 +117,43 @@ func TestGhost_Must(t *testing.T) { Message: msg, }) + g.Should(be.DeepEqual(mockT.logCalls, [][]any{{msg}})) g.Should(be.SliceLen(mockT.failNowCalls, 1)) - g.Should(be.DeepEqual( - mockT.logCalls, - [][]any{{msg}}, - )) + }) +} + +func TestGhost_MustNot(t *testing.T) { + t.Run("ok", func(t *testing.T) { + g := ghost.New(t) + + mockT := newMockT() + testG := ghost.New(mockT) + msg := "some message" + + testG.MustNot(ghost.Result{ + Ok: true, + Message: msg, + }) + + g.Should(be.DeepEqual(mockT.logCalls, [][]any{{msg}})) + g.Should(be.SliceLen(mockT.failNowCalls, 1)) + }) + + t.Run("not ok", func(t *testing.T) { + g := ghost.New(t) + + mockT := newMockT() + testG := ghost.New(mockT) + msg := "some message" + + testG.MustNot(ghost.Result{ + Ok: false, + Message: msg, + }) + + g.Should(be.SliceLen(mockT.logCalls, 0)) + g.Should(be.SliceLen(mockT.failCalls, 0)) + g.Should(be.SliceLen(mockT.failNowCalls, 0)) }) }