A large collection of composable test matchers for Go -
works with stdlib testing, Ginkgo/Gomega, Gomock, and testify.
be is a matcher library: instead of asserting on booleans and losing the failure
message, you say what a value should be. Matchers compose - almost any argument
that takes a value also takes another matcher:
be.Expect(t, user.Email).To(be_string.ValidEmail())
be.Expect(t, items).To(be.HaveLength(be.Gte(3)))Every matcher works everywhere: with the built-in stdlib runner shown above, as a Gomega matcher inside Ginkgo, and as a mock argument matcher for Gomock and testify/mockery. The core module imports no test framework.
Note
be is at v1.0.0-rc.* - the API is stable and being hardened for the v1.0.0 release.
- Install
- Quick Start
- Say It with a Matcher
- Matching an HTTP Request
- Test Framework Integration
- Matchers - flat catalog in MATCHERS.md
- Contributing
- License
go get github.com/expectto/bepackage user_test
import (
"testing"
"github.com/expectto/be"
"github.com/expectto/be/be_string"
)
func TestNewUser(t *testing.T) {
u, err := NewUser("john@tests.com")
be.Require(t, err).To(be.Succeed()) // hard fail, require-style
be.Expect(t, u.Email).To(be_string.ValidEmail()) // soft fail, assert-style
be.Expect(t, u.ID).To(be.NonZero())
}No Gomega, no testify - just *testing.T and matchers. If you already use
Ginkgo/Gomega or testify, see Test Framework Integration.
Wrapping a raw expression in be.True(...) throws away the failure message -
all you learn is "expected true, got false". There is a matcher for almost
every idiom; this table is the cheat-sheet (and what belint
flags automatically):
| Instead of | Use |
|---|---|
be.Not(be.Nil()) |
be.NotNil() |
be.HaveLength(0) |
be.Empty() |
be.Not(be.HaveLength(0)) |
be.NotEmpty() |
be.Not(be.Eq(0)), be.Ne(0) |
be.NonZero() |
x == y → be.True() |
be.Eq(y) |
x >= n → be.True() |
be.Gte(n) |
len(xs) >= n → be.True() |
be.HaveLength(be.Gte(n)) |
slices.Contains(xs, v) → be.True() |
be.ContainElement(v) |
strings.Contains(s, q) → be.True() |
be.ContainSubstring(q) |
strings.HasPrefix(s, p) → be.True() |
be_string.HavingPrefix(p) |
_, ok := m[k]; ok → be.True() |
be.HaveKey(k) |
errors.Is(err, X) → be.True()/False() |
be.MatchError(X) / be.Not(be.MatchError(X)) |
errors.As(err, &v) → be.True() |
be.MatchErrorAs[V]() - only when v is unused afterward |
t1.Equal(t2) → be.True() |
be_time.SameExactSecond(t2) / be_time.Approx(...) |
The full flat catalog of every matcher across all packages lives in MATCHERS.md.
Composability is the point: matchers nest into matchers, so one assertion can describe an entire HTTP request - URL, method, context, JSON body, headers, even a JWT inside a header template:
req, err := buildRequestForServiceFoo()
Expect(err).To(Succeed())
Expect(req).To(be_http.Request(
// Matching the URL
be_http.HavingURL(be_url.URL(
be_url.WithHttps(),
be_url.HavingHost("example.com"),
be_url.HavingPath("/path"),
be_url.HavingSearchParam("status", "active"),
be_url.HavingSearchParam("v", be_reflected.AsNumericString()),
be_url.HavingSearchParam("q", "Hello World"),
)),
// Matching the HTTP method
be_http.POST(),
// Matching the request's context
be_http.HavingCtx(
be_ctx.CtxWithDeadline(be_time.LaterThan(time.Now().Add(30*time.Minute))),
be_ctx.CtxWithValue("foobar", 100),
),
// Matching the request body using JSON matchers
be_http.HavingBody(
be.JSON(
be_json.JsonAsReader,
be_json.HaveKeyValue("hello", "world"),
// NOTE: JSON numbers decode to float64, so use AsFloat (not AsInteger) here
be_json.HaveKeyValue("n", be_reflected.AsFloat(), be_math.GreaterThan(10)),
be_json.HaveKeyValue("ids", be_reflected.AsSliceOf[string]()),
Not(be_json.HaveKeyValue("deleted_field")),
be_json.HaveKeyValue("email", be_string.ValidEmail(), HaveSuffix("@tests.com")),
// "details":[{"key":"foo"},{"key":"bar"}]
be_json.HaveKeyValue("details", And(
be_reflected.AsObjects(),
be.HaveLength(be_math.GreaterThan(2)),
ContainElements(
be_json.HaveKeyValue("key", "foo"),
be_json.HaveKeyValue("key", "bar"),
),
)),
),
),
// Matching HTTP headers
be_http.HavingHeader("X-Custom", "Hey-There"),
be_http.HavingHeader("Authorization",
be_string.MatchTemplate("Bearer {{jwt}}",
be_string.V("jwt",
be_jwt.Token(
be_jwt.Valid(),
be_jwt.HavingClaim("name", "John Doe"),
),
),
),
),
))be matchers are framework-agnostic. The core github.com/expectto/be module
imports no test framework - pick how you run assertions:
Standard library (no extra deps). Two equivalent spellings - a fluent one and
a flat, testify-style one - both backed by the same engine, both driven by the
stdlib *testing.T:
import "github.com/expectto/be"
// fluent (ginkgo/gomega-flavored)
be.Expect(t, n).To(be_math.GreaterThan(10)) // soft fail (assert-style)
be.Require(t, n).To(be_math.GreaterThan(10)) // hard fail (require-style)
be.Expect(t, s).NotTo(be_string.EmptyString())
// flat (testify-flavored)
be.AssertThat(t, n, be_math.GreaterThan(10)) // soft fail (assert-style)
be.RequireThat(t, s, be_string.NonEmptyString()) // hard fail (require-style)Already on testify? Keep your assert/require calls and reach for be
only where a matcher earns its keep - be.AssertThat / be.RequireThat are the
drop-in slots (no extra dependency):
assert.Equal(t, want, got) // testify, as usual
be.AssertThat(t, got, be.Eq(want)) // be - and now `got` can face any matcher,
// e.g. be_url.URL(be_url.HavingHost("x"), ...)The subject comes first and the expected value lives inside the matcher (
be.Eq(want)), so - unlike testify'sEqual(t, want, got)- there's no want/got order to memorize or get wrong.
Ginkgo / Gomega: every be matcher already satisfies gomega's matcher
interface, so use it directly inside Expect(...).To(...).
be matchers also work as mock argument matchers:
Gomock: every be matcher already satisfies gomock.Matcher, so pass it directly:
mockObj.EXPECT().Do(be_math.GreaterThan(10)).Return("ok")Testify mock / mockery: wrap with MatchedBy (works for hand-written and
mockery-generated mocks) - the matcher equivalent of testify's own
mock.MatchedBy. This is the one place you need the separate x/mock module
(it's what keeps testify out of the core deps); install it with @latest (the
submodule shares version numbers with the core module, which confuses
go get <pkg>@<version>):
go get github.com/expectto/be/x/mock@latestimport bemock "github.com/expectto/be/x/mock"
svc.On("Do", bemock.MatchedBy(be_math.GreaterThan(10))).Return("ok")One package per domain; each has its own README with detailed docs. The flat searchable catalog of everything is MATCHERS.md.
Core matchers for common testing scenarios. Detailed docs
- Core:
Always,Never,All,Any,Eq,Not,HaveLength,Dive,DiveAny,DiveFirst - Everyday:
Nil,NotNil,True,False,Eq,Ne,Zero,NonZero,Empty,NotEmpty,Identical,NotIdentical,Via,Succeed,HaveOccurred,MatchError,MatchErrorAs,Panic,NotPanic,ContainElement,ContainElements,ContainSubstring,HaveKey,HaveKeyWithValue,HaveField,HaveFields - Numeric aliases at root (from be_math):
Gt,Gte,Lt,Lte,GreaterThan,GreaterThanEqual,LessThan,LessThanEqual,InRange,Positive,Negative - Assertion shortcuts & async:
NoError,Error,ErrorIs(hard, the testifyrequiretrio) ·Eventually,Consistently(native poll loop, no gomega output leakage)
Matchers on values' reflect kinds and types. Detailed docs
- By reflect.Kind:
AsKind,AsFunc,AsChan,AsPointer,AsFinalPointer,AsStruct,AsPointerToStruct,AsSlice,AsPointerToSlice,AsSliceOf,AsMap,AsPointerToMap,AsObject,AsObjects,AsPointerToObject - Data types:
AsString,AsBytes,AsNumeric,AsNumericString,AsInteger,AsIntegerString,AsFloat,AsFloatishString - Interfaces:
AsReader,AsStringer - Type compatibility:
AssignableTo,Implementing
Matchers for mathematical assertions. Detailed docs
GreaterThan,GreaterThanEqual,LessThan,LessThanEqual,Approx,InRange,Odd,Even,Negative,Positive,Zero,Integral,DivisibleBy- Shortcuts:
Gt,Gte,Lt,Lte
Matchers on strings. Detailed docs
NonEmptyString,EmptyString,Alpha,Numeric,AlphaNumeric,AlphaNumericWithDots,Float,Titled,LowerCaseOnly,MatchWildcard,ValidEmail- Templates:
MatchTemplate
Matchers on time.Time. Detailed docs
LaterThan,LaterThanEqual,EarlierThan,EarlierThanEqual,Eq,ApproxSameExactMilli,SameExactSecond,SameExactMinute,SameExactHour,SameExactDay,SameExactWeekday,SameExactWeek,SameExactMonthSameSecond,SameMinute,SameHour,SameDay,SameYearDay,SameWeek,SameMonth,SameYear,SameTimezone,SameOffset,IsDST
Matchers on JSON Web Tokens (via golang-jwt/jwt/v5). Detailed docs
- Transformers:
TransformSignedJwtFromString,TransformJwtFromString - Matchers:
Token,Valid,HavingClaims,HavingClaim,HavingMethodAlg,SignedVia
Matchers on url.URL. Detailed docs
- Transformers:
TransformUrlFromString,TransformSchemelessUrlFromString - Matchers:
URL,Values,HavingHost,HavingHostname,HavingScheme,NotHavingScheme,WithHttps,WithHttp,HavingPort,NotHavingPort,HavingPath,HavingRawQuery,HavingSearchParam,NotHavingSearchParam,HavingMultipleSearchParam,HavingUsername,HavingUserinfo,HavingPassword
Matchers on context.Context. Detailed docs
Ctx,CtxWithValue,CtxWithDeadline,CtxWithError
Matchers for expressive assertions on JSON. Detailed docs
Matcher,HaveKeyValue
Matchers on struct fields. Detailed docs
HavingField
Matchers on http.Request. Detailed docs
Request,HavingMethod,GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS,CONNECT,TRACEHavingURL,HavingBody,HavingHost,HavingProto,HavingCtx,HavingHeader,HavingHeaders
Issues, ideas, and pull requests are welcome - see CONTRIBUTING.md.
MIT - see LICENSE.