Skip to content

Commit

Permalink
Reintroduce ShouldNot/MustNot
Browse files Browse the repository at this point in the history
I love the purity of doing everything through composers, and having
exactly one way to make each assertion. Practically speaking, it was
more verbose than it needed to be and not any more readable. A function
like `be.Not` can continue to exist for complex use cases, and we let a
top-level `ShouldNot`/`MustNot` handle the vast majority of negations
with a much lighter syntax and more readable English.

This also implies that other native functions like `ShouldEventually`
might one day warrant existing. I don't think today is that day.
  • Loading branch information
rliebz committed Jan 11, 2024
1 parent 8dbfde1 commit fd8c0d1
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 30 deletions.
50 changes: 32 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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"}))
Expand All @@ -111,13 +114,15 @@ 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))
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].
Expand All @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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
8 changes: 4 additions & 4 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"}))
Expand All @@ -28,20 +28,20 @@ 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))
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.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 {
Expand Down
27 changes: 27 additions & 0 deletions ghost.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
84 changes: 76 additions & 8 deletions ghost_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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))
})
}

Expand Down

0 comments on commit fd8c0d1

Please sign in to comment.