├── .vscode ├── extensions.json ├── tasks.json ├── launch.json └── settings.json ├── .gitattributes ├── goyek.ps1 ├── goyek.sh ├── go.sum ├── .markdownlint.yaml ├── go.mod ├── bool.go ├── and.go ├── or.go ├── .github ├── workflows │ ├── release.yml │ └── build.yml ├── pull_request_template.md └── dependabot.yml ├── assertion_error.go ├── doc.go ├── nil.go ├── or_test.go ├── panic.go ├── and_test.go ├── bool_test.go ├── nil_test.go ├── .gitignore ├── panic_test.go ├── helpers_test.go ├── LICENSE ├── eventually.go ├── comparable.go ├── .golangci.yml ├── CONTRIBUTING.md ├── any.go ├── ordered.go ├── error.go ├── ordered_test.go ├── eventually_test.go ├── number_test.go ├── comparable_test.go ├── failmsg.go ├── any_test.go ├── error_test.go ├── CODE_OF_CONDUCT.md ├── number.go ├── map.go ├── slice_test.go ├── slice.go ├── string.go ├── map_test.go ├── string_test.go ├── CHANGELOG.md ├── failmsg_test.go └── README.md /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "golang.Go" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.{cmd,[cC][mM][dD]} text eol=crlf 3 | *.{bat,[bB][aA][tT]} text eol=crlf 4 | -------------------------------------------------------------------------------- /goyek.ps1: -------------------------------------------------------------------------------- 1 | Push-Location "$PSScriptRoot\build" -ErrorAction Stop 2 | & go run . $args 3 | Pop-Location 4 | exit $global:LASTEXITCODE 5 | -------------------------------------------------------------------------------- /goyek.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" 5 | cd "$DIR/build" 6 | go run . $@ -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 2 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 3 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | # Default state for all rules 2 | default: true 3 | 4 | # hard-tabs 5 | MD010: false 6 | 7 | # 80 char line-length 8 | MD013: 9 | code_blocks: false 10 | tables: false 11 | 12 | # no-duplicate-header 13 | MD024: 14 | siblings_only: true 15 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | // Deprecated: avoid using assertion libraries. 2 | // Use the standard library and github.com/google/go-cmp/cmp instead. 3 | // Follow the official Go Test Comments: https://go.dev/wiki/TestComments. 4 | module github.com/fluentassert/verify 5 | 6 | go 1.18 7 | 8 | require github.com/google/go-cmp v0.6.0 9 | -------------------------------------------------------------------------------- /bool.go: -------------------------------------------------------------------------------- 1 | package verify 2 | 3 | // True tests if the object is a true value. 4 | func True[T ~bool](got T) FailureMessage { 5 | if got { 6 | return "" 7 | } 8 | return "the value is false" 9 | } 10 | 11 | // False tests if the object is a false value. 12 | func False[T ~bool](got T) FailureMessage { 13 | if !got { 14 | return "" 15 | } 16 | return "the value is true" 17 | } 18 | -------------------------------------------------------------------------------- /and.go: -------------------------------------------------------------------------------- 1 | package verify 2 | 3 | // And accumalates non-empty failure messages. 4 | func And(assertions ...FailureMessage) FailureMessage { 5 | var msg FailureMessage 6 | for _, assertion := range assertions { 7 | if assertion == "" { 8 | continue 9 | } 10 | if msg == "" { 11 | msg = assertion 12 | continue 13 | } 14 | msg = msg + "\n\n" + assertion 15 | } 16 | return msg 17 | } 18 | -------------------------------------------------------------------------------- /or.go: -------------------------------------------------------------------------------- 1 | package verify 2 | 3 | // Or accumalates failure messages if all are not empty. 4 | func Or(assertions ...FailureMessage) FailureMessage { 5 | var msg FailureMessage 6 | for _, assertion := range assertions { 7 | if assertion == "" { 8 | return "" 9 | } 10 | if msg == "" { 11 | msg = assertion 12 | continue 13 | } 14 | msg = msg + "\n\n" + assertion 15 | } 16 | return msg 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: [ 'v*' ] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | godoc: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Wait 1 minute 13 | # make sure that new the tag is available for pkg.go.dev 14 | run: sleep 60 15 | - name: Update pkg.go.dev 16 | run: curl https://proxy.golang.org/github.com/${{ github.repository }}/@v/${{ github.ref_name }}.info 17 | -------------------------------------------------------------------------------- /assertion_error.go: -------------------------------------------------------------------------------- 1 | package verify 2 | 3 | // AssertionError is an error type used to represent failure messages from assertions. 4 | // It is compatible with the error interface and can be used in instances where an error shall be returned instead of failing a test. 5 | type AssertionError struct { 6 | Message FailureMessage 7 | } 8 | 9 | // Error returns the failure message as a string. It makes AssertionError compatible with the error interface. 10 | func (err *AssertionError) Error() string { 11 | return string(err.Message) 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "dev", 8 | "type": "shell", 9 | "command": "./goyek.sh", 10 | "problemMatcher": [ 11 | "$go" 12 | ], 13 | "group": { 14 | "kind": "build", 15 | "isDefault": true 16 | } 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "${fileDirname}", 13 | "env": {}, 14 | "args": [] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package verify contains the most useful type-safe fluent assertion functions. 2 | // 3 | // It also provides the FailureMessage type which you can use 4 | // for creating your own fluent assertions. 5 | // 6 | // At last, you may embed a Fluent* type 7 | // to extend it with additional assertion functions. 8 | // 9 | // Deprecated: avoid using assertion libraries. 10 | // Use the standard library and [github.com/google/go-cmp/cmp] instead. 11 | // Follow the official [Go Test Comments]. 12 | // 13 | // [Go Test Comments]: https://go.dev/wiki/TestComments 14 | package verify 15 | -------------------------------------------------------------------------------- /nil.go: -------------------------------------------------------------------------------- 1 | package verify 2 | 3 | import "fmt" 4 | 5 | // Nil tests if provided interface value is nil. 6 | // Use it only for interfaces. 7 | // For structs and pointers use Obj(got).Zero(). 8 | func Nil(v any) FailureMessage { 9 | if v == nil { 10 | return "" 11 | } 12 | return FailureMessage(fmt.Sprintf("value is not nil\ngot: %+v", v)) 13 | } 14 | 15 | // NotNil tests if provided interface is not nil. 16 | // Use it only for interfaces. 17 | // For structs and pointers use Obj(got).NonZero(). 18 | func NotNil(v any) FailureMessage { 19 | if v != nil { 20 | return "" 21 | } 22 | return "value is " 23 | } 24 | -------------------------------------------------------------------------------- /or_test.go: -------------------------------------------------------------------------------- 1 | package verify_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/fluentassert/verify" 7 | ) 8 | 9 | func TestOr(t *testing.T) { 10 | t.Run("Empty", func(t *testing.T) { 11 | msg := verify.Or() 12 | assertPassed(t, msg) 13 | }) 14 | t.Run("NoneFailed", func(t *testing.T) { 15 | msg := verify.Or("", "") 16 | assertPassed(t, msg) 17 | }) 18 | t.Run("OneFailed", func(t *testing.T) { 19 | msg := verify.Or("", "failure") 20 | assertPassed(t, msg) 21 | }) 22 | t.Run("TwoFailed", func(t *testing.T) { 23 | msg := verify.Or("one", "two") 24 | assertFailed(t, msg, "one\n\ntwo") 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /panic.go: -------------------------------------------------------------------------------- 1 | package verify 2 | 3 | import "fmt" 4 | 5 | // Panics tests if the function panics when executed. 6 | func Panics(fn func()) (msg FailureMessage) { 7 | defer func() { 8 | if r := recover(); r == nil { 9 | msg = "the function returned instead of panicking" 10 | } 11 | }() 12 | fn() 13 | return 14 | } 15 | 16 | // NotPanics tests if the function does not panic when executed. 17 | func NotPanics(fn func()) (msg FailureMessage) { 18 | defer func() { 19 | if r := recover(); r != nil { 20 | msg = FailureMessage(fmt.Sprintf("the function panicked\ngot: %v", r)) 21 | } 22 | }() 23 | fn() 24 | return 25 | } 26 | -------------------------------------------------------------------------------- /and_test.go: -------------------------------------------------------------------------------- 1 | package verify_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/fluentassert/verify" 7 | ) 8 | 9 | func TestAnd(t *testing.T) { 10 | t.Run("Empty", func(t *testing.T) { 11 | msg := verify.And() 12 | assertPassed(t, msg) 13 | }) 14 | t.Run("NoneFailed", func(t *testing.T) { 15 | msg := verify.And("", "") 16 | assertPassed(t, msg) 17 | }) 18 | t.Run("OneFailed", func(t *testing.T) { 19 | msg := verify.And("", "failure") 20 | assertFailed(t, msg, "failure") 21 | }) 22 | t.Run("TwoFailed", func(t *testing.T) { 23 | msg := verify.And("one", "two") 24 | assertFailed(t, msg, "one\n\ntwo") 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /bool_test.go: -------------------------------------------------------------------------------- 1 | package verify_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/fluentassert/verify" 7 | ) 8 | 9 | func TestTrue(t *testing.T) { 10 | t.Run("Passed", func(t *testing.T) { 11 | got := verify.True(true) 12 | assertPassed(t, got) 13 | }) 14 | t.Run("Failed", func(t *testing.T) { 15 | got := verify.True(false) 16 | assertFailed(t, got, "the value is false") 17 | }) 18 | } 19 | 20 | func TestFalse(t *testing.T) { 21 | t.Run("Passed", func(t *testing.T) { 22 | got := verify.False(false) 23 | assertPassed(t, got) 24 | }) 25 | t.Run("Failed", func(t *testing.T) { 26 | got := verify.False(true) 27 | assertFailed(t, got, "the value is true") 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /nil_test.go: -------------------------------------------------------------------------------- 1 | package verify_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/fluentassert/verify" 7 | ) 8 | 9 | func TestNil(t *testing.T) { 10 | t.Run("Passed", func(t *testing.T) { 11 | var err error 12 | msg := verify.Nil(err) 13 | assertPassed(t, msg) 14 | }) 15 | t.Run("Failed", func(t *testing.T) { 16 | msg := verify.Nil(0) 17 | assertFailed(t, msg, "value is not nil") 18 | }) 19 | } 20 | 21 | func TestNotNil(t *testing.T) { 22 | t.Run("Passed", func(t *testing.T) { 23 | msg := verify.NotNil(0) 24 | assertPassed(t, msg) 25 | }) 26 | t.Run("Failed", func(t *testing.T) { 27 | var err error 28 | msg := verify.NotNil(err) 29 | assertFailed(t, msg, "value is ") 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # MacOS DS_Store files 2 | .DS_Store 3 | 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | coverage.html 17 | 18 | # Dependency directories (remove the comment below to include it) 19 | #vendor/ 20 | 21 | # Visual Studio Code files 22 | .vscode/* 23 | !.vscode/settings.json 24 | !.vscode/tasks.json 25 | !.vscode/launch.json 26 | !.vscode/extensions.json 27 | *.code-workspace 28 | .history/ 29 | 30 | # GoLand and IntelliJ IDEA files 31 | .idea/ 32 | 33 | # env files that usually contain secrets or local config 34 | .env 35 | .envrc 36 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[go]": { 3 | "editor.formatOnSave": true, 4 | "editor.codeActionsOnSave": { 5 | "source.organizeImports": "explicit" 6 | }, 7 | }, 8 | "[go.mod]": { 9 | "editor.formatOnSave": true, 10 | "editor.codeActionsOnSave": { 11 | "source.organizeImports": "explicit" 12 | }, 13 | }, 14 | "gopls": { 15 | "formatting.local": "github.com/fluentassert/verify", 16 | "formatting.gofumpt": true, 17 | }, 18 | "go.lintTool": "golangci-lint", 19 | "go.lintFlags": [ 20 | "--fast" 21 | ], 22 | "[go][go.mod]": { 23 | "editor.codeActionsOnSave": { 24 | "source.organizeImports": "explicit" 25 | } 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /panic_test.go: -------------------------------------------------------------------------------- 1 | package verify_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/fluentassert/verify" 7 | ) 8 | 9 | func TestPanics(t *testing.T) { 10 | t.Run("Passed", func(t *testing.T) { 11 | msg := verify.Panics(func() { panic("exception") }) 12 | assertPassed(t, msg) 13 | }) 14 | t.Run("Failed", func(t *testing.T) { 15 | msg := verify.Panics(func() {}) 16 | assertFailed(t, msg, "the function returned instead of panicking") 17 | }) 18 | } 19 | 20 | func TestNotPanics(t *testing.T) { 21 | t.Run("Passed", func(t *testing.T) { 22 | msg := verify.NotPanics(func() {}) 23 | assertPassed(t, msg) 24 | }) 25 | t.Run("Failed", func(t *testing.T) { 26 | msg := verify.NotPanics(func() { panic("exception") }) 27 | assertFailed(t, msg, "the function panicked") 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Why 2 | 3 | 5 | 6 | ## What 7 | 8 | 10 | 11 | ## Checklist 12 | 13 | 15 | 16 | - [ ] `CHANGELOG.md` is updated. 17 | - [ ] `README.md` is updated. 18 | - [ ] The code changes follow [Effective Go](https://golang.org/doc/effective_go). 19 | - [ ] The code changes follow [CodeReviewComments](https://github.com/golang/go/wiki/CodeReviewComments). 20 | - [ ] The code changes are covered by tests. 21 | 22 | 23 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | # Maintain dependencies for Go 9 | - package-ecosystem: "gomod" 10 | directory: "/" 11 | schedule: 12 | interval: "weekly" 13 | 14 | # Maintain dependencies for build tools 15 | - package-ecosystem: "gomod" 16 | directory: "/build" 17 | schedule: 18 | interval: "weekly" 19 | 20 | # Maintain dependencies for GitHub Actions 21 | - package-ecosystem: "github-actions" 22 | directory: "/" 23 | schedule: 24 | interval: "weekly" 25 | -------------------------------------------------------------------------------- /helpers_test.go: -------------------------------------------------------------------------------- 1 | package verify_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | 9 | "github.com/fluentassert/verify" 10 | ) 11 | 12 | func assertEqual[T any](t *testing.T, got, want T, opts ...cmp.Option) { 13 | t.Helper() 14 | if diff := cmp.Diff(want, got, opts...); diff != "" { 15 | t.Errorf("mismatch (-want +got):\n%s", diff) 16 | } 17 | } 18 | 19 | func assertTrue(t *testing.T, got bool) { 20 | t.Helper() 21 | if !got { 22 | t.Errorf("want = true; got = false") 23 | } 24 | } 25 | 26 | func assertFalse(t *testing.T, got bool) { 27 | t.Helper() 28 | if got { 29 | t.Errorf("want = true; got = false") 30 | } 31 | } 32 | 33 | func assertPassed(t *testing.T, got verify.FailureMessage) { 34 | t.Helper() 35 | if got != "" { 36 | t.Errorf("\nSHOULD PASS; GOT:\n%s", string(got)) 37 | } 38 | } 39 | 40 | func assertFailed(t *testing.T, got verify.FailureMessage, substr string) { 41 | t.Helper() 42 | if !strings.Contains(string(got), substr) { 43 | t.Errorf("\nSHOULD FAIL AND CONTAIN:\n%s\nGOT:\n%s", substr, string(got)) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Robert Pająk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /eventually.go: -------------------------------------------------------------------------------- 1 | package verify 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Eventually executes the test function until it returns an empty FailureMessage 8 | // or timeout elapses. 9 | func Eventually(timeout, interval time.Duration, fn func() FailureMessage) FailureMessage { 10 | timer := time.NewTimer(timeout) 11 | defer timer.Stop() 12 | ticker := time.NewTicker(interval) 13 | defer ticker.Stop() 14 | return EventuallyChan(timer.C, ticker.C, fn) 15 | } 16 | 17 | // EventuallyChan executes the test function until it returns an empty FailureMessage or timeout elapses. 18 | func EventuallyChan[TTimerPayload, TTickPayload any](timeout <-chan (TTimerPayload), ticker <-chan (TTickPayload), fn func() FailureMessage) FailureMessage { 19 | var err string 20 | fail := func() FailureMessage { 21 | return FailureMessage("function never passed, last failure message:\n" + err) 22 | } 23 | 24 | for { 25 | select { 26 | case <-timeout: 27 | return fail() 28 | default: 29 | } 30 | 31 | err = string(fn()) 32 | 33 | select { 34 | case <-timeout: 35 | return fail() 36 | default: 37 | } 38 | 39 | if err == "" { 40 | return "" 41 | } 42 | 43 | select { 44 | case <-timeout: 45 | return fail() 46 | case <-ticker: 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | ci-build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-go@v5 16 | with: 17 | go-version: '1.23' 18 | cache-dependency-path: '**/go.sum' 19 | - run: ./goyek.sh -v all diff 20 | - name: Upload HTML coverage 21 | uses: actions/upload-artifact@v4 22 | with: 23 | name: coverage 24 | path: coverage.* 25 | - name: Upload coverage to Codecov 26 | uses: codecov/codecov-action@v5 27 | with: 28 | fail_ci_if_error: true 29 | files: ./coverage.out 30 | token: ${{ secrets.CODECOV_TOKEN }} 31 | 32 | compatibility: 33 | strategy: 34 | fail-fast: false 35 | matrix: 36 | go-version: 37 | - '1.18' 38 | - '1.19' 39 | - '1.20' 40 | - '1.21' 41 | - '1.22' 42 | - '1.23' 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: actions/checkout@v4 46 | - uses: actions/setup-go@v5 47 | with: 48 | go-version: ${{ matrix.go-version }} 49 | - run: go test -race ./... 50 | -------------------------------------------------------------------------------- /comparable.go: -------------------------------------------------------------------------------- 1 | package verify 2 | 3 | import "fmt" 4 | 5 | // FluentObj encapsulates assertions for comparable object. 6 | type FluentObj[T comparable] struct { 7 | FluentAny[T] 8 | } 9 | 10 | // Obj is used for testing a comparable object. 11 | func Obj[T comparable](got T) FluentObj[T] { 12 | return FluentObj[T]{FluentAny[T]{got}} 13 | } 14 | 15 | // Equal tests the objects using == operator. 16 | func (x FluentObj[T]) Equal(want T) FailureMessage { 17 | if x.Got == want { 18 | return "" 19 | } 20 | return FailureMessage(fmt.Sprintf("the objects are not equal\ngot: %+v\nwant: %+v", x.Got, want)) 21 | } 22 | 23 | // NotEqual tests the objects using != operator. 24 | func (x FluentObj[T]) NotEqual(obj T) FailureMessage { 25 | if x.Got != obj { 26 | return "" 27 | } 28 | return "the objects are equal" 29 | } 30 | 31 | // Zero tests if the object is a zero value. 32 | func (x FluentObj[T]) Zero() FailureMessage { 33 | var want T 34 | if want == x.Got { 35 | return "" 36 | } 37 | return FailureMessage(fmt.Sprintf("not a zero value\ngot: %+v", x.Got)) 38 | } 39 | 40 | // NonZero tests if the object is a non-zero value. 41 | func (x FluentObj[T]) NonZero() FailureMessage { 42 | var want T 43 | if want != x.Got { 44 | return "" 45 | } 46 | return FailureMessage(fmt.Sprintf("not a zero value\ngot: %+v", x.Got)) 47 | } 48 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | gocyclo: 3 | min-complexity: 15 4 | goimports: 5 | local-prefixes: github.com/fluentassert/verify 6 | govet: 7 | enable-all: true 8 | disable: 9 | - fieldalignment 10 | - nilness 11 | misspell: 12 | locale: US 13 | nolintlint: 14 | allow-leading-space: false # require machine-readable nolint directives (with no leading space) 15 | allow-unused: false # report any unused nolint directives 16 | require-explanation: true # require an explanation for nolint directives 17 | require-specific: false # don't require nolint directives to be specific about which linter is being skipped 18 | revive: 19 | confidence: 0 20 | 21 | linters: 22 | # please, do not use `enable-all`: it's deprecated and will be removed soon. 23 | # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint 24 | disable-all: true 25 | enable: 26 | - dogsled 27 | - errcheck 28 | - gochecknoinits 29 | - gocritic 30 | - goconst 31 | - gocyclo 32 | - gofumpt 33 | - goimports 34 | - revive 35 | - goprintffuncname 36 | - gosec 37 | - gosimple 38 | - govet 39 | - ineffassign 40 | - misspell 41 | - mnd 42 | - nolintlint 43 | - staticcheck 44 | - stylecheck 45 | - typecheck 46 | - unconvert 47 | - unparam 48 | - unused 49 | - whitespace 50 | 51 | issues: 52 | exclude: 53 | - EXC0001 54 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Feel free to create an issue or propose a pull request. 4 | 5 | Follow the [Code of Conduct](CODE_OF_CONDUCT.md). 6 | 7 | ## Developing 8 | 9 | The latest version of Go is required. 10 | 11 | Docker is recommended. 12 | 13 | Run `./goyek.sh` (Bash) or `.\goyek.ps1` (PowerShell) 14 | to execute the build pipeline. 15 | 16 | The repository contains basic confiugration for 17 | [Visual Studio Code](https://code.visualstudio.com/). 18 | 19 | ## Releasing 20 | 21 | This section describes how to prepare and publish a new release. 22 | 23 | ### Pre-release 24 | 25 | Create a pull request named `Release ` that does the following: 26 | 27 | 1. Update the examples in [README.md](README.md) 28 | and make sure the documentation is up to date. 29 | 30 | 2. Update [`CHANGELOG.md`](CHANGELOG.md). 31 | - Change the `Unreleased` header to represent the new release. 32 | - Consider adding a description for the new release. 33 | Especially if it adds new features or introduces breaking changes. 34 | - Add a new `Unreleased` header above the new release, with no details. 35 | 36 | ### Release 37 | 38 | 1. Add and push a signed tag: 39 | 40 | ```sh 41 | TAG='v' 42 | COMMIT='' 43 | git tag -s -m $TAG $TAG $COMMIT 44 | git push upstream $TAG 45 | ``` 46 | 47 | 2. Create a GitHib Release named `` with `v` tag. 48 | 49 | The release description should include all the release notes 50 | from the [`CHANGELOG.md`](CHANGELOG.md) for this release. 51 | -------------------------------------------------------------------------------- /any.go: -------------------------------------------------------------------------------- 1 | package verify 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/google/go-cmp/cmp" 7 | ) 8 | 9 | // FluentAny encapsulates assertions for any object. 10 | type FluentAny[T any] struct { 11 | Got T 12 | } 13 | 14 | // Any is used for testing any object. 15 | func Any[T any](got T) FluentAny[T] { 16 | return FluentAny[T]{got} 17 | } 18 | 19 | // Check tests the object using the provided function. 20 | func (x FluentAny[T]) Check(fn func(got T) FailureMessage) FailureMessage { 21 | return fn(x.Got) 22 | } 23 | 24 | // Should tests if the object meets the predicate criteria. 25 | func (x FluentAny[T]) Should(pred func(got T) bool) FailureMessage { 26 | if pred(x.Got) { 27 | return "" 28 | } 29 | return FailureMessage(fmt.Sprintf("object does not meet the predicate criteria\ngot: %+v", x.Got)) 30 | } 31 | 32 | // ShouldNot tests if the object does not meet the predicate criteria. 33 | func (x FluentAny[T]) ShouldNot(fn func(got T) bool) FailureMessage { 34 | if !fn(x.Got) { 35 | return "" 36 | } 37 | return FailureMessage(fmt.Sprintf("object meets the predicate criteria\ngot: %+v", x.Got)) 38 | } 39 | 40 | // DeepEqual tests if the objects are deep equal using github.com/google/go-cmp/cmp. 41 | func (x FluentAny[T]) DeepEqual(want T, opts ...cmp.Option) FailureMessage { 42 | diff := cmp.Diff(want, x.Got, opts...) 43 | if diff == "" { 44 | return "" 45 | } 46 | return FailureMessage("mismatch (-want +got):\n" + diff) 47 | } 48 | 49 | // NotDeepEqual tests if the objects are not deep equal using github.com/google/go-cmp/cmp. 50 | func (x FluentAny[T]) NotDeepEqual(obj T, opts ...cmp.Option) FailureMessage { 51 | ok := cmp.Equal(obj, x.Got, opts...) 52 | if !ok { 53 | return "" 54 | } 55 | return FailureMessage(fmt.Sprintf("the objects are equal\ngot: %+v", x.Got)) 56 | } 57 | -------------------------------------------------------------------------------- /ordered.go: -------------------------------------------------------------------------------- 1 | package verify 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // FluentOrdered encapsulates assertions for ordered object 8 | // that supports the operators < <= >= >. 9 | type FluentOrdered[T ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64 | ~string] struct { 10 | FluentObj[T] 11 | } 12 | 13 | // Ordered is used for testing a ordered object 14 | // that supports the operators < <= >= >. 15 | func Ordered[T ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64 | ~string](got T) FluentOrdered[T] { 16 | return FluentOrdered[T]{FluentObj[T]{FluentAny[T]{got}}} 17 | } 18 | 19 | // Lesser tests the objects using < operator. 20 | func (x FluentOrdered[T]) Lesser(than T) FailureMessage { 21 | if x.Got < than { 22 | return "" 23 | } 24 | return FailureMessage(fmt.Sprintf("the object is not lesser\ngot: %v\nthan: %v", x.Got, than)) 25 | } 26 | 27 | // LesserOrEqual tests the objects using <= operator. 28 | func (x FluentOrdered[T]) LesserOrEqual(than T) FailureMessage { 29 | if x.Got <= than { 30 | return "" 31 | } 32 | return FailureMessage(fmt.Sprintf("the object is not lesser or equal\ngot: %v\nthan: %v", x.Got, than)) 33 | } 34 | 35 | // GreaterOrEqual tests the objects using >= operator. 36 | func (x FluentOrdered[T]) GreaterOrEqual(than T) FailureMessage { 37 | if x.Got >= than { 38 | return "" 39 | } 40 | return FailureMessage(fmt.Sprintf("the object is not greater or equal\ngot: %v\nthan: %v", x.Got, than)) 41 | } 42 | 43 | // Greater tests the objects using > operator. 44 | func (x FluentOrdered[T]) Greater(than T) FailureMessage { 45 | if x.Got > than { 46 | return "" 47 | } 48 | return FailureMessage(fmt.Sprintf("the object is not greater\ngot: %v\nthan: %v", x.Got, than)) 49 | } 50 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package verify 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | // NoError tests if the error is nil. 9 | func NoError(err error) FailureMessage { 10 | if err == nil { 11 | return "" 12 | } 13 | return "non-nil error:\n" + FailureMessage(err.Error()) 14 | } 15 | 16 | // IsError tests if the error is non-nil. 17 | func IsError(err error) FailureMessage { 18 | if err != nil { 19 | return "" 20 | } 21 | return "the error is " 22 | } 23 | 24 | // FluentError encapsulates assertions for error object. 25 | type FluentError struct { 26 | FluentAny[error] 27 | FluentString[string] 28 | } 29 | 30 | // Error is used for testing error object. 31 | func Error(got error) FluentError { 32 | res := FluentError{FluentAny: FluentAny[error]{got}} 33 | if got != nil { 34 | res.FluentString.Got = got.Error() 35 | } 36 | return res 37 | } 38 | 39 | // Is tests whether any error in err's chain matches target. 40 | func (x FluentError) Is(target error) FailureMessage { 41 | if errors.Is(x.Got, target) { 42 | return "" 43 | } 44 | return FailureMessage(fmt.Sprintf("no error in err's chain matches\ngot: %#v\ntarget: %#v", x.Got, target)) 45 | } 46 | 47 | // IsNot tests whether no error in err's chain matches target. 48 | func (x FluentError) IsNot(target error) FailureMessage { 49 | if !errors.Is(x.Got, target) { 50 | return "" 51 | } 52 | return FailureMessage(fmt.Sprintf("some error in err's chain matches\ngot: %#v\ntarget: %#v", x.Got, target)) 53 | } 54 | 55 | // As finds the first error in err's chain that matches target, and if one is found, sets 56 | // target to that error value. In such case it is a success.. 57 | func (x FluentError) As(target any) FailureMessage { 58 | if errors.As(x.Got, target) { 59 | return "" 60 | } 61 | return FailureMessage(fmt.Sprintf("no error in err's chain matches\ngot: %#v\ntarget: %T", x.Got, target)) 62 | } 63 | 64 | // AsNot finds the first error in err's chain that matches target, and if one is found, sets 65 | // target to that error value. In such case it is a failure. 66 | func (x FluentError) AsNot(target any) FailureMessage { 67 | if !errors.As(x.Got, target) { 68 | return "" 69 | } 70 | return FailureMessage(fmt.Sprintf("some error in err's chain matches\ngot: %#v\ntarget: %T", x.Got, target)) 71 | } 72 | -------------------------------------------------------------------------------- /ordered_test.go: -------------------------------------------------------------------------------- 1 | package verify_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/fluentassert/verify" 7 | ) 8 | 9 | func TestOrdered(t *testing.T) { 10 | t.Run("Lesser", func(t *testing.T) { 11 | t.Run("Lesser", func(t *testing.T) { 12 | msg := verify.Ordered(0).Lesser(1) 13 | assertPassed(t, msg) 14 | }) 15 | t.Run("Equal", func(t *testing.T) { 16 | msg := verify.Ordered(0).Lesser(0) 17 | assertFailed(t, msg, "the object is not lesser") 18 | }) 19 | t.Run("Greater", func(t *testing.T) { 20 | msg := verify.Ordered(0).Lesser(-1) 21 | assertFailed(t, msg, "the object is not lesser") 22 | }) 23 | }) 24 | 25 | t.Run("LesserOrEqual", func(t *testing.T) { 26 | t.Run("Lesser", func(t *testing.T) { 27 | msg := verify.Ordered(0).LesserOrEqual(1) 28 | assertPassed(t, msg) 29 | }) 30 | t.Run("Equal", func(t *testing.T) { 31 | msg := verify.Ordered(0).LesserOrEqual(0) 32 | assertPassed(t, msg) 33 | }) 34 | t.Run("Greater", func(t *testing.T) { 35 | msg := verify.Ordered(0).LesserOrEqual(-1) 36 | assertFailed(t, msg, "the object is not lesser or equal") 37 | }) 38 | }) 39 | 40 | t.Run("GreaterOrEqual", func(t *testing.T) { 41 | t.Run("Lesser", func(t *testing.T) { 42 | msg := verify.Ordered(0).GreaterOrEqual(1) 43 | assertFailed(t, msg, "the object is not greater or equal") 44 | }) 45 | t.Run("Equal", func(t *testing.T) { 46 | msg := verify.Ordered(0).GreaterOrEqual(0) 47 | assertPassed(t, msg) 48 | }) 49 | t.Run("Greater", func(t *testing.T) { 50 | msg := verify.Ordered(0).GreaterOrEqual(-1) 51 | assertPassed(t, msg) 52 | }) 53 | }) 54 | 55 | t.Run("Greater", func(t *testing.T) { 56 | t.Run("Lesser", func(t *testing.T) { 57 | msg := verify.Ordered(0).Greater(1) 58 | assertFailed(t, msg, "the object is not greater") 59 | }) 60 | t.Run("Equal", func(t *testing.T) { 61 | msg := verify.Ordered(0).Greater(0) 62 | assertFailed(t, msg, "the object is not greater") 63 | }) 64 | t.Run("Greater", func(t *testing.T) { 65 | msg := verify.Ordered(0).Greater(-1) 66 | assertPassed(t, msg) 67 | }) 68 | }) 69 | 70 | t.Run("has assertions from Obj and Any", func(t *testing.T) { 71 | want := 123 72 | got := verify.Ordered(want).FluentObj.FluentAny.Got // type embedding done properly 73 | assertEqual(t, got, want) 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /eventually_test.go: -------------------------------------------------------------------------------- 1 | package verify_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/fluentassert/verify" 8 | ) 9 | 10 | func TestEventually(t *testing.T) { 11 | timeout := 100 * time.Millisecond 12 | interval := 10 * time.Millisecond 13 | 14 | t.Run("InitialPassed", func(t *testing.T) { 15 | msg := verify.Eventually(timeout, interval, func() verify.FailureMessage { 16 | return "" 17 | }) 18 | assertPassed(t, msg) 19 | }) 20 | t.Run("SecondPassed", func(t *testing.T) { 21 | shouldPass := false 22 | msg := verify.Eventually(timeout, interval, func() verify.FailureMessage { 23 | if !shouldPass { 24 | shouldPass = true // next exeucution will pass 25 | return "fail" 26 | } 27 | return "" 28 | }) 29 | assertPassed(t, msg) 30 | }) 31 | t.Run("ReturnedTooLate", func(t *testing.T) { 32 | msg := verify.Eventually(timeout, interval, func() verify.FailureMessage { 33 | time.Sleep(2 * timeout) 34 | return "" 35 | }) 36 | assertFailed(t, msg, "function never passed, last failure message:\n") 37 | }) 38 | t.Run("Failed", func(t *testing.T) { 39 | msg := verify.Eventually(timeout, interval, func() verify.FailureMessage { 40 | return "constant failure" 41 | }) 42 | assertFailed(t, msg, "function never passed, last failure message:\nconstant failure") 43 | }) 44 | } 45 | 46 | func TestEventuallyChan(t *testing.T) { 47 | timeout := 100 * time.Millisecond 48 | interval := 10 * time.Millisecond 49 | 50 | t.Run("Passed", func(t *testing.T) { 51 | timer := time.NewTimer(timeout) 52 | defer timer.Stop() 53 | ticker := time.NewTicker(interval) 54 | defer ticker.Stop() 55 | msg := verify.EventuallyChan(timer.C, ticker.C, func() verify.FailureMessage { 56 | return "" 57 | }) 58 | assertPassed(t, msg) 59 | }) 60 | t.Run("TimeoutBeforeStart", func(t *testing.T) { 61 | ch := make(chan struct{}) 62 | close(ch) 63 | msg := verify.EventuallyChan(ch, ch, func() verify.FailureMessage { 64 | return "" 65 | }) 66 | assertFailed(t, msg, "function never passed, last failure message:\n") 67 | }) 68 | t.Run("Failed", func(t *testing.T) { 69 | ch := make(chan struct{}) 70 | msg := verify.EventuallyChan(time.After(timeout), ch, func() verify.FailureMessage { 71 | return "constant failure" 72 | }) 73 | assertFailed(t, msg, "function never passed, last failure message:\nconstant failure") 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /number_test.go: -------------------------------------------------------------------------------- 1 | package verify_test 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "github.com/fluentassert/verify" 8 | ) 9 | 10 | func TestNumber(t *testing.T) { 11 | t.Run("InDelta", func(t *testing.T) { 12 | t.Run("Near", func(t *testing.T) { 13 | msg := verify.Number(0.0).InDelta(1, 10) 14 | assertPassed(t, msg) 15 | }) 16 | t.Run("Far", func(t *testing.T) { 17 | msg := verify.Number(0.0).InDelta(-100, 10) 18 | assertFailed(t, msg, "absolute error (distance) between numbers is greater than delta") 19 | }) 20 | }) 21 | 22 | t.Run("NotInDelta", func(t *testing.T) { 23 | t.Run("Near", func(t *testing.T) { 24 | msg := verify.Number(0.0).NotInDelta(-1.0, 10) 25 | assertFailed(t, msg, "absolute error (distance) between numbers is lesser or equal than delta") 26 | }) 27 | t.Run("Far", func(t *testing.T) { 28 | msg := verify.Number(0.0).NotInDelta(100, 10) 29 | assertPassed(t, msg) 30 | }) 31 | }) 32 | 33 | t.Run("InEpsilon", func(t *testing.T) { 34 | t.Run("Near", func(t *testing.T) { 35 | msg := verify.Number(1.0).InEpsilon(1, 2) 36 | assertPassed(t, msg) 37 | }) 38 | t.Run("Far", func(t *testing.T) { 39 | msg := verify.Number(100.0).InEpsilon(1, 2) 40 | assertFailed(t, msg, "relative error between numbers is greater than epsilon") 41 | }) 42 | }) 43 | 44 | t.Run("NotInEpsilon", func(t *testing.T) { 45 | t.Run("Near", func(t *testing.T) { 46 | msg := verify.Number(0.0).NotInEpsilon(1, 2) 47 | assertFailed(t, msg, "relative error between numbers is lesser or equal than epsilon") 48 | }) 49 | t.Run("Far", func(t *testing.T) { 50 | msg := verify.Number(100.0).NotInEpsilon(1, 2) 51 | assertPassed(t, msg) 52 | }) 53 | }) 54 | 55 | t.Run("InvalidInputs", func(t *testing.T) { 56 | for _, fn := range []func() verify.FailureMessage{ 57 | func() verify.FailureMessage { 58 | return verify.Number(math.NaN()).InDelta(1, 2) 59 | }, 60 | func() verify.FailureMessage { 61 | return verify.Number(1.0).NotInDelta(math.NaN(), 2) 62 | }, 63 | func() verify.FailureMessage { 64 | return verify.Number(1.0).InDelta(1, -2) 65 | }, 66 | func() verify.FailureMessage { 67 | return verify.Number(math.NaN()).InEpsilon(1, 2) 68 | }, 69 | func() verify.FailureMessage { 70 | return verify.Number(1.0).NotInEpsilon(math.NaN(), 2) 71 | }, 72 | func() verify.FailureMessage { 73 | return verify.Number(1.0).InEpsilon(1, -2) 74 | }, 75 | func() verify.FailureMessage { 76 | return verify.Number(1.0).InEpsilon(0.0, 2) 77 | }, 78 | } { 79 | if msg := fn(); msg == "" { 80 | t.Error("should fail but passed") 81 | } 82 | } 83 | }) 84 | } 85 | -------------------------------------------------------------------------------- /comparable_test.go: -------------------------------------------------------------------------------- 1 | package verify_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/fluentassert/verify" 7 | ) 8 | 9 | func TestObj(t *testing.T) { 10 | type A struct { 11 | Str string 12 | Bool bool 13 | } 14 | 15 | t.Run("Equal", func(t *testing.T) { 16 | t.Run("Passed", func(t *testing.T) { 17 | want := A{Str: "string", Bool: true} 18 | got := A{Str: "string", Bool: true} 19 | msg := verify.Obj(got).Equal(want) 20 | assertPassed(t, msg) 21 | }) 22 | t.Run("Failed", func(t *testing.T) { 23 | want := A{Str: "string", Bool: true} 24 | got := A{Str: "wrong", Bool: true} 25 | msg := verify.Obj(got).Equal(want) 26 | assertFailed(t, msg, "the objects are not equal") 27 | }) 28 | t.Run("nil", func(t *testing.T) { 29 | var got *A 30 | msg := verify.Obj(got).Equal(nil) 31 | assertPassed(t, msg) 32 | }) 33 | }) 34 | 35 | t.Run("NotEqual", func(t *testing.T) { 36 | t.Run("Passed", func(t *testing.T) { 37 | want := A{Str: "string", Bool: true} 38 | got := A{Str: "wrong", Bool: true} 39 | msg := verify.Obj(got).NotEqual(want) 40 | assertPassed(t, msg) 41 | }) 42 | t.Run("Failed", func(t *testing.T) { 43 | want := A{Str: "string", Bool: true} 44 | got := A{Str: "string", Bool: true} 45 | msg := verify.Obj(got).NotEqual(want) 46 | assertFailed(t, msg, "the objects are equal") 47 | }) 48 | t.Run("nil", func(t *testing.T) { 49 | var got *A 50 | msg := verify.Obj(got).NotEqual(nil) 51 | assertFailed(t, msg, "the objects are equal") 52 | }) 53 | }) 54 | 55 | t.Run("Zero", func(t *testing.T) { 56 | t.Run("Passed", func(t *testing.T) { 57 | got := A{} 58 | msg := verify.Obj(got).Zero() 59 | assertPassed(t, msg) 60 | }) 61 | t.Run("Failed", func(t *testing.T) { 62 | got := A{Str: "wrong"} 63 | msg := verify.Obj(got).Zero() 64 | assertFailed(t, msg, "not a zero value\n") 65 | }) 66 | t.Run("nil", func(t *testing.T) { 67 | var got *A 68 | msg := verify.Obj(got).Zero() 69 | assertPassed(t, msg) 70 | }) 71 | }) 72 | 73 | t.Run("NonZero", func(t *testing.T) { 74 | t.Run("Passed", func(t *testing.T) { 75 | got := A{Str: "string"} 76 | msg := verify.Obj(got).NonZero() 77 | assertPassed(t, msg) 78 | }) 79 | t.Run("Failed", func(t *testing.T) { 80 | got := A{} 81 | msg := verify.Obj(got).NonZero() 82 | assertFailed(t, msg, "a zero value") 83 | }) 84 | t.Run("nil", func(t *testing.T) { 85 | var got *A 86 | msg := verify.Obj(got).NonZero() 87 | assertFailed(t, msg, "a zero value") 88 | }) 89 | }) 90 | 91 | t.Run("has assertions from Any", func(t *testing.T) { 92 | want := A{} 93 | got := verify.Obj(want).FluentAny.Got // type embedding done properly 94 | assertEqual(t, got, want) 95 | }) 96 | } 97 | -------------------------------------------------------------------------------- /failmsg.go: -------------------------------------------------------------------------------- 1 | package verify 2 | 3 | // FailureMessage encapsulates a failure message 4 | // that can by emitted using objects compatible 5 | // with the testing.TB interface. 6 | type FailureMessage string 7 | 8 | // Assert calls t.Error if the failure message is not empty. 9 | // Calling Error on *testing.T marks the the function as having failed 10 | // and continues its execution. 11 | // Returns true when the failure message is empty. 12 | func (msg FailureMessage) Assert(t interface { 13 | Error(args ...any) 14 | }, args ...any, 15 | ) bool { 16 | if msg == "" { 17 | return true 18 | } 19 | if h, ok := t.(interface{ Helper() }); ok { 20 | h.Helper() 21 | } 22 | t.Error(append(args, "\n"+string(msg))...) 23 | return false 24 | } 25 | 26 | // Require calls t.Fatal if the failure message is not empty. 27 | // Calling Fatal on *testing.T stops the test function execution. 28 | // Returns true when the failure message is empty. 29 | func (msg FailureMessage) Require(t interface { 30 | Fatal(args ...any) 31 | }, args ...any, 32 | ) bool { 33 | if msg == "" { 34 | return true 35 | } 36 | if h, ok := t.(interface{ Helper() }); ok { 37 | h.Helper() 38 | } 39 | t.Fatal(append(args, "\n"+string(msg))...) 40 | return false 41 | } 42 | 43 | // Assertf calls t.Errorf if the failure message is not empty. 44 | // Calling Errorf on *testing.T marks the the function as having failed 45 | // and continues its execution. 46 | // Returns true when the failure message is empty 47 | func (msg FailureMessage) Assertf(t interface { 48 | Errorf(format string, args ...any) 49 | }, format string, args ...any, 50 | ) bool { 51 | if msg == "" { 52 | return true 53 | } 54 | if h, ok := t.(interface{ Helper() }); ok { 55 | h.Helper() 56 | } 57 | t.Errorf(format+"%s", append(args, "\n"+string(msg))...) 58 | return false 59 | } 60 | 61 | // Requiref calls t.Fatalf if the failure message is not empty. 62 | // Calling Fatalf on *testing.T stops the test function execution. 63 | // Returns true when the failure message is empty. 64 | func (msg FailureMessage) Requiref(t interface { 65 | Fatalf(format string, args ...any) 66 | }, format string, args ...any, 67 | ) bool { 68 | if msg == "" { 69 | return true 70 | } 71 | if h, ok := t.(interface{ Helper() }); ok { 72 | h.Helper() 73 | } 74 | t.Fatalf(format+"%s", append(args, "\n"+string(msg))...) 75 | return false 76 | } 77 | 78 | // Prefix adds prefix if the failure message is not empty. 79 | func (msg FailureMessage) Prefix(s string) FailureMessage { 80 | if msg == "" { 81 | return "" 82 | } 83 | return FailureMessage(s) + msg 84 | } 85 | 86 | // Err returns the failure message as an error type, or nil if the message is empty. 87 | func (msg FailureMessage) Err() *AssertionError { 88 | if msg == "" { 89 | return nil 90 | } 91 | 92 | return &AssertionError{Message: msg} 93 | } 94 | -------------------------------------------------------------------------------- /any_test.go: -------------------------------------------------------------------------------- 1 | package verify_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/fluentassert/verify" 7 | ) 8 | 9 | func TestAny(t *testing.T) { 10 | type A struct { 11 | Str string 12 | Bool bool 13 | Slice []int 14 | } 15 | 16 | t.Run("DeepEqual", func(t *testing.T) { 17 | t.Run("Passed", func(t *testing.T) { 18 | want := A{Str: "string", Bool: true, Slice: []int{1, 2, 3}} 19 | got := A{Str: "string", Bool: true, Slice: []int{1, 2, 3}} 20 | msg := verify.Any(got).DeepEqual(want) 21 | assertPassed(t, msg) 22 | }) 23 | t.Run("Failed", func(t *testing.T) { 24 | want := A{Str: "string", Bool: true, Slice: []int{1, 2, 3}} 25 | got := A{Str: "wrong", Bool: true, Slice: []int{1, 3}} 26 | msg := verify.Any(got).DeepEqual(want) 27 | assertFailed(t, msg, "mismatch (-want +got):\n") 28 | }) 29 | t.Run("nil", func(t *testing.T) { 30 | var got *A 31 | msg := verify.Any(got).DeepEqual(nil) 32 | assertPassed(t, msg) 33 | }) 34 | }) 35 | 36 | t.Run("NotDeepEqual", func(t *testing.T) { 37 | t.Run("Passed", func(t *testing.T) { 38 | want := A{Str: "string", Bool: true, Slice: []int{1, 2, 3}} 39 | got := A{Str: "wrong", Bool: true, Slice: []int{1, 3}} 40 | msg := verify.Any(got).NotDeepEqual(want) 41 | assertPassed(t, msg) 42 | }) 43 | t.Run("Failed", func(t *testing.T) { 44 | want := A{Str: "string", Bool: true, Slice: []int{1, 2, 3}} 45 | got := A{Str: "string", Bool: true, Slice: []int{1, 2, 3}} 46 | msg := verify.Any(got).NotDeepEqual(want) 47 | assertFailed(t, msg, "the objects are equal") 48 | }) 49 | t.Run("nil", func(t *testing.T) { 50 | var got *A 51 | msg := verify.Any(got).NotDeepEqual(nil) 52 | assertFailed(t, msg, "the objects are equal") 53 | }) 54 | }) 55 | 56 | t.Run("Check", func(t *testing.T) { 57 | t.Run("Passed", func(t *testing.T) { 58 | fn := func(A) verify.FailureMessage { 59 | return "" 60 | } 61 | msg := verify.Any(A{}).Check(fn) 62 | assertPassed(t, msg) 63 | }) 64 | t.Run("Failed", func(t *testing.T) { 65 | fn := func(A) verify.FailureMessage { 66 | return "failure" 67 | } 68 | msg := verify.Any(A{}).Check(fn) 69 | assertFailed(t, msg, "failure") 70 | }) 71 | }) 72 | 73 | t.Run("Should", func(t *testing.T) { 74 | t.Run("Passed", func(t *testing.T) { 75 | pred := func(A) bool { 76 | return true 77 | } 78 | msg := verify.Any(A{}).Should(pred) 79 | assertPassed(t, msg) 80 | }) 81 | t.Run("Failed", func(t *testing.T) { 82 | pred := func(A) bool { 83 | return false 84 | } 85 | msg := verify.Any(A{}).Should(pred) 86 | assertFailed(t, msg, "object does not meet the predicate criteria") 87 | }) 88 | }) 89 | 90 | t.Run("ShouldNot", func(t *testing.T) { 91 | t.Run("Passed", func(t *testing.T) { 92 | pred := func(A) bool { 93 | return false 94 | } 95 | msg := verify.Any(A{}).ShouldNot(pred) 96 | assertPassed(t, msg) 97 | }) 98 | t.Run("Failed", func(t *testing.T) { 99 | pred := func(A) bool { 100 | return true 101 | } 102 | msg := verify.Any(A{}).ShouldNot(pred) 103 | assertFailed(t, msg, "object meets the predicate criteria") 104 | }) 105 | }) 106 | } 107 | -------------------------------------------------------------------------------- /error_test.go: -------------------------------------------------------------------------------- 1 | package verify_test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/google/go-cmp/cmp/cmpopts" 9 | 10 | "github.com/fluentassert/verify" 11 | ) 12 | 13 | func TestNoError(t *testing.T) { 14 | t.Run("Passed", func(t *testing.T) { 15 | got := verify.NoError(nil) 16 | assertPassed(t, got) 17 | }) 18 | t.Run("Failed", func(t *testing.T) { 19 | got := verify.NoError(errors.New("some error")) 20 | assertFailed(t, got, "non-nil error:\nsome error") 21 | }) 22 | } 23 | 24 | func TestIsError(t *testing.T) { 25 | t.Run("Passed", func(t *testing.T) { 26 | got := verify.IsError(errors.New("")) 27 | assertPassed(t, got) 28 | }) 29 | t.Run("Failed", func(t *testing.T) { 30 | var err error 31 | got := verify.IsError(err) 32 | assertFailed(t, got, "the error is ") 33 | }) 34 | } 35 | 36 | func TestError(t *testing.T) { 37 | err := errors.New("expected error") 38 | t.Run("Is", func(t *testing.T) { 39 | t.Run("Passed", func(t *testing.T) { 40 | got := verify.Error(fmt.Errorf("wrap: %w", err)).Is(err) 41 | assertPassed(t, got) 42 | }) 43 | t.Run("Failed", func(t *testing.T) { 44 | got := verify.Error(fmt.Errorf("wrap: %v", err)).Is(err) 45 | assertFailed(t, got, "no error in err's chain matches") 46 | }) 47 | }) 48 | t.Run("IsNot", func(t *testing.T) { 49 | t.Run("Passed", func(t *testing.T) { 50 | got := verify.Error(fmt.Errorf("wrap: %v", err)).IsNot(err) 51 | assertPassed(t, got) 52 | }) 53 | t.Run("Failed", func(t *testing.T) { 54 | got := verify.Error(fmt.Errorf("wrap: %w", err)).IsNot(err) 55 | assertFailed(t, got, "some error in err's chain matches") 56 | }) 57 | }) 58 | 59 | t.Run("As", func(t *testing.T) { 60 | t.Run("Passed", func(t *testing.T) { 61 | var wantErr *stubError 62 | got := verify.Error(fmt.Errorf("wrap: %w", &stubError{})).As(&wantErr) 63 | assertPassed(t, got) 64 | assertEqual(t, wantErr, &stubError{}) 65 | }) 66 | t.Run("Failed", func(t *testing.T) { 67 | var wantErr *stubError 68 | got := verify.Error(fmt.Errorf("wrap: %v", &stubError{})).As(&wantErr) 69 | assertFailed(t, got, "no error in err's chain matches") 70 | assertEqual(t, wantErr, nil) 71 | }) 72 | }) 73 | t.Run("AsNot", func(t *testing.T) { 74 | t.Run("Passed", func(t *testing.T) { 75 | var wantErr *stubError 76 | got := verify.Error(fmt.Errorf("wrap: %v", &stubError{})).AsNot(&wantErr) 77 | assertPassed(t, got) 78 | assertEqual(t, wantErr, nil) 79 | }) 80 | t.Run("Failed", func(t *testing.T) { 81 | var wantErr *stubError 82 | got := verify.Error(fmt.Errorf("wrap: %w", &stubError{})).AsNot(&wantErr) 83 | assertFailed(t, got, "some error in err's chain matches") 84 | assertEqual(t, wantErr, &stubError{}) 85 | }) 86 | }) 87 | 88 | t.Run("has assertions from Any", func(t *testing.T) { 89 | want := errors.New("an error") 90 | got := verify.Error(want).FluentAny.Got // type embedding done properly 91 | assertEqual(t, got, want, cmpopts.EquateErrors()) 92 | }) 93 | t.Run("has assertions from String, Ordered, Obj for string", func(t *testing.T) { 94 | want := "an error" 95 | got := verify.Error(errors.New(want)).FluentString.Got // type embedding done properly 96 | assertEqual(t, got, want) 97 | }) 98 | } 99 | 100 | type stubError struct{} 101 | 102 | func (*stubError) Error() string { return "stubError" } 103 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at . All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 71 | version 1.4, available at . 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | . 77 | -------------------------------------------------------------------------------- /number.go: -------------------------------------------------------------------------------- 1 | package verify 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // FluentNumber encapsulates assertions for numbers 9 | // that supports the operators < <= >= > + - * /. 10 | type FluentNumber[T ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64] struct { 11 | FluentOrdered[T] 12 | } 13 | 14 | // Number is used for testing numbers 15 | // that supports the operators < <= >= > + - * /. 16 | func Number[T ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64](got T) FluentNumber[T] { 17 | return FluentNumber[T]{FluentOrdered[T]{FluentObj[T]{FluentAny[T]{got}}}} 18 | } 19 | 20 | // InDelta tests that the numbers have an absolute error (distance) less or equal than delta. 21 | func (x FluentNumber[T]) InDelta(want T, delta float64) FailureMessage { 22 | distance, msg := x.calcDistance(want, delta) 23 | if msg != "" { 24 | return msg 25 | } 26 | if distance < -delta || distance > delta { 27 | return FailureMessage(fmt.Sprintf("absolute error (distance) between numbers is greater than delta\nrelative error: %v\ndelta: %g\ngot: %v\nwant: %v", distance, delta, x.Got, want)) 28 | } 29 | return "" 30 | } 31 | 32 | // NotInDelta tests that the numbers have an absolute error (distance) greater than delta. 33 | func (x FluentNumber[T]) NotInDelta(want T, delta float64) FailureMessage { 34 | distance, msg := x.calcDistance(want, delta) 35 | if msg != "" { 36 | return msg 37 | } 38 | if distance < -delta || distance > delta { 39 | return "" 40 | } 41 | return FailureMessage(fmt.Sprintf("absolute error (distance) between numbers is lesser or equal than delta\nrelative error: %v\ndelta: %g\ngot: %v\nwant: %v", distance, delta, x.Got, want)) 42 | } 43 | 44 | func (x FluentNumber[T]) calcDistance(want T, delta float64) (float64, FailureMessage) { 45 | if math.IsNaN(delta) || delta < 0 { 46 | return 0, "delta must be a non-negative number" 47 | } 48 | wantF := float64(want) 49 | gotF := float64(x.Got) 50 | if math.IsNaN(wantF) { 51 | return 0, "want is NaN" 52 | } 53 | if math.IsNaN(gotF) { 54 | return 0, "got is NaN" 55 | } 56 | return wantF - gotF, "" 57 | } 58 | 59 | // InEpsilon tests that the numbers have a relative error less or equal than epsilon. 60 | func (x FluentNumber[T]) InEpsilon(want T, epsilon float64) FailureMessage { 61 | relativeError, msg := x.calcRelativeError(want, epsilon) 62 | if msg != "" { 63 | return msg 64 | } 65 | if relativeError > epsilon { 66 | return FailureMessage(fmt.Sprintf("relative error between numbers is greater than epsilon\nrelative error: %g\nepsilon: %g\ngot: %v\nwant: %v", relativeError, epsilon, x.Got, want)) 67 | } 68 | return "" 69 | } 70 | 71 | // NotInEpsilon tests that the numbers have a relative error greater than epsilon. 72 | func (x FluentNumber[T]) NotInEpsilon(want T, epsilon float64) FailureMessage { 73 | relativeError, msg := x.calcRelativeError(want, epsilon) 74 | if msg != "" { 75 | return msg 76 | } 77 | if relativeError > epsilon { 78 | return "" 79 | } 80 | return FailureMessage(fmt.Sprintf("relative error between numbers is lesser or equal than epsilon\nrelative error: %g\nepsilon: %g\ngot: %v\nto: %v", relativeError, epsilon, x.Got, want)) 81 | } 82 | 83 | func (x FluentNumber[T]) calcRelativeError(want T, epsilon float64) (float64, FailureMessage) { 84 | if math.IsNaN(epsilon) || epsilon < 0 { 85 | return 0, "epsilon must be a non-negative number" 86 | } 87 | wantF := float64(want) 88 | gotF := float64(x.Got) 89 | if math.IsNaN(wantF) { 90 | return 0, "want is NaN" 91 | } 92 | if math.IsNaN(gotF) { 93 | return 0, "got is NaN" 94 | } 95 | if wantF == 0 { 96 | return 0, "want must have a value other than zero to calculate the relative error" 97 | } 98 | relativeError := math.Abs(wantF-gotF) / math.Abs(wantF) 99 | return relativeError, "" 100 | } 101 | -------------------------------------------------------------------------------- /map.go: -------------------------------------------------------------------------------- 1 | package verify 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/google/go-cmp/cmp" 7 | ) 8 | 9 | // FluentMap encapsulates assertions for a map. 10 | type FluentMap[K comparable, V any] struct { 11 | FluentAny[map[K]V] 12 | } 13 | 14 | // Map is used for testing a map. 15 | func Map[K comparable, V any](got map[K]V) FluentMap[K, V] { 16 | return FluentMap[K, V]{FluentAny[map[K]V]{got}} 17 | } 18 | 19 | // Empty tests if the slice is empty. 20 | func (x FluentMap[K, V]) Empty() FailureMessage { 21 | if len(x.Got) == 0 { 22 | return "" 23 | } 24 | return FailureMessage(fmt.Sprintf("not an empty map\ngot: %+v", x.Got)) 25 | } 26 | 27 | // NotEmpty tests if the slice is not empty. 28 | func (x FluentMap[K, V]) NotEmpty() FailureMessage { 29 | if len(x.Got) > 0 { 30 | return "" 31 | } 32 | return FailureMessage(fmt.Sprintf("an empty map\ngot: %+v", x.Got)) 33 | } 34 | 35 | // Contain tests if the map contains all pairs from want. 36 | func (x FluentMap[K, V]) Contain(want map[K]V, opts ...cmp.Option) FailureMessage { 37 | missing := x.miss(want, opts) 38 | if len(missing) == 0 { 39 | return "" 40 | } 41 | return FailureMessage(fmt.Sprintf("not contains all pairs\ngot: %+v\nwant: %+v\nmissing: %+v", x.Got, want, missing)) 42 | } 43 | 44 | // NotContain tests if the map does not contains all pairs from want. 45 | func (x FluentMap[K, V]) NotContain(want map[K]V, opts ...cmp.Option) FailureMessage { 46 | missing := x.miss(want, opts) 47 | if len(missing) > 0 { 48 | return "" 49 | } 50 | return FailureMessage(fmt.Sprintf("contains all pairs\ngot: %+v\nwant: %+v", x.Got, want)) 51 | } 52 | 53 | func (x FluentMap[K, V]) miss(want map[K]V, opts []cmp.Option) map[K]V { 54 | missing := map[K]V{} 55 | for k, v := range want { 56 | got, ok := x.Got[k] 57 | if !ok { 58 | missing[k] = v 59 | continue 60 | } 61 | if !cmp.Equal(v, got, opts...) { 62 | missing[k] = v 63 | continue 64 | } 65 | } 66 | return missing 67 | } 68 | 69 | // ContainPair tests if the map contains the pair. 70 | func (x FluentMap[K, V]) ContainPair(k K, v V, opts ...cmp.Option) FailureMessage { 71 | got, ok := x.Got[k] 72 | if !ok { 73 | return FailureMessage(fmt.Sprintf("has no value under key\ngot: %+v\nkey: %+v\nvalue: %+v", x.Got, k, v)) 74 | } 75 | if !cmp.Equal(v, got, opts...) { 76 | return FailureMessage(fmt.Sprintf("has different value under key\nkey: %+v\ngot: %+v\nwant: %+v", k, got, v)) 77 | } 78 | return "" 79 | } 80 | 81 | // NotContainPair tests if the map does not contain the pair. 82 | func (x FluentMap[K, V]) NotContainPair(k K, v V, opts ...cmp.Option) FailureMessage { 83 | got, ok := x.Got[k] 84 | if !ok { 85 | return "" 86 | } 87 | if !cmp.Equal(v, got, opts...) { 88 | return "" 89 | } 90 | return FailureMessage(fmt.Sprintf("contains the pair\ngot: %+v\nkey: %+v\nvalue: %+v", x.Got, k, v)) 91 | } 92 | 93 | // Any tests if any of the map's pairs meets the predicate criteria. 94 | func (x FluentMap[K, V]) Any(predicate func(K, V) bool) FailureMessage { 95 | for k, v := range x.Got { 96 | if predicate(k, v) { 97 | return "" 98 | } 99 | } 100 | return FailureMessage(fmt.Sprintf("none pair does meet the predicate criteria\ngot: %+v", x.Got)) 101 | } 102 | 103 | // All tests if all of the map's pairs meet the predicate criteria. 104 | func (x FluentMap[K, V]) All(predicate func(K, V) bool) FailureMessage { 105 | for k, v := range x.Got { 106 | if !predicate(k, v) { 107 | return FailureMessage(fmt.Sprintf("a pair does not meet the predicate criteria\ngot: %+v\npair: %+v", x.Got, v)) 108 | } 109 | } 110 | return "" 111 | } 112 | 113 | // None tests if none of the map's pairs meets the predicate criteria. 114 | func (x FluentMap[K, V]) None(predicate func(K, V) bool) FailureMessage { 115 | for k, v := range x.Got { 116 | if predicate(k, v) { 117 | return FailureMessage(fmt.Sprintf("a pair meets the predicate criteria\ngot: %+v\npair: %+v", x.Got, v)) 118 | } 119 | } 120 | return "" 121 | } 122 | 123 | // Len tests the length of the map. 124 | func (x FluentMap[K, V]) Len(want int) FailureMessage { 125 | gotLen := len(x.Got) 126 | if gotLen != want { 127 | return FailureMessage(fmt.Sprintf("has different length\ngot: %+v\nlen: %v\nwant: %v", x.Got, gotLen, want)) 128 | } 129 | return "" 130 | } 131 | -------------------------------------------------------------------------------- /slice_test.go: -------------------------------------------------------------------------------- 1 | package verify_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/fluentassert/verify" 7 | ) 8 | 9 | func TestSlice(t *testing.T) { 10 | type A struct { 11 | Str string 12 | Bool bool 13 | Slice []int 14 | } 15 | 16 | t.Run("has assertions from Any", func(t *testing.T) { 17 | want := []A{ 18 | {Str: "string", Bool: true, Slice: []int{1, 2, 3}}, 19 | } 20 | got := verify.Slice(want).FluentAny.Got // type embedding done properly 21 | assertEqual(t, got, want) 22 | }) 23 | 24 | t.Run("Empty", func(t *testing.T) { 25 | t.Run("Passed", func(t *testing.T) { 26 | got := verify.Slice([]A{}).Empty() 27 | assertPassed(t, got) 28 | }) 29 | t.Run("Failed", func(t *testing.T) { 30 | got := verify.Slice([]A{{}}).Empty() 31 | assertFailed(t, got, "not an empty slice") 32 | }) 33 | }) 34 | t.Run("NotEmpty", func(t *testing.T) { 35 | t.Run("Passed", func(t *testing.T) { 36 | got := verify.Slice([]A{{}}).NotEmpty() 37 | assertPassed(t, got) 38 | }) 39 | t.Run("Failed", func(t *testing.T) { 40 | got := verify.Slice([]A{}).NotEmpty() 41 | assertFailed(t, got, "an empty slice") 42 | }) 43 | }) 44 | 45 | list := []A{ 46 | {Str: "text", Bool: true, Slice: []int{1, 2, 3}}, 47 | {Slice: []int{9, 8, 7}}, 48 | } 49 | eq := []A{ 50 | {Slice: []int{9, 8, 7}}, 51 | {Str: "text", Bool: true, Slice: []int{1, 2, 3}}, 52 | } 53 | notEq := []A{ 54 | {Slice: []int{0, 0, 0}}, 55 | {Str: "text", Bool: true, Slice: []int{1, 2, 3}}, 56 | } 57 | t.Run("Equivalent", func(t *testing.T) { 58 | t.Run("Passed", func(t *testing.T) { 59 | got := verify.Slice(list).Equivalent(eq) 60 | assertPassed(t, got) 61 | }) 62 | t.Run("Failed", func(t *testing.T) { 63 | got := verify.Slice(list).Equivalent(notEq) 64 | assertFailed(t, got, "not equivalent") 65 | }) 66 | }) 67 | t.Run("NotEquivalent", func(t *testing.T) { 68 | t.Run("Passed", func(t *testing.T) { 69 | got := verify.Slice(list).NotEquivalent(notEq) 70 | assertPassed(t, got) 71 | }) 72 | t.Run("Failed", func(t *testing.T) { 73 | got := verify.Slice(list).NotEquivalent(eq) 74 | assertFailed(t, got, "equivalent") 75 | }) 76 | }) 77 | 78 | t.Run("Contain", func(t *testing.T) { 79 | t.Run("Passed", func(t *testing.T) { 80 | got := verify.Slice(list).Contain(A{Str: "text", Bool: true, Slice: []int{1, 2, 3}}) 81 | assertPassed(t, got) 82 | }) 83 | t.Run("Failed", func(t *testing.T) { 84 | got := verify.Slice(list).Contain(A{}) 85 | assertFailed(t, got, "slice does not contain the item") 86 | }) 87 | }) 88 | t.Run("NotContain", func(t *testing.T) { 89 | t.Run("Passed", func(t *testing.T) { 90 | got := verify.Slice(list).NotContain(A{}) 91 | assertPassed(t, got) 92 | }) 93 | t.Run("Failed", func(t *testing.T) { 94 | got := verify.Slice(list).NotContain(A{Str: "text", Bool: true, Slice: []int{1, 2, 3}}) 95 | assertFailed(t, got, "slice contains the item") 96 | }) 97 | }) 98 | 99 | t.Run("Any", func(t *testing.T) { 100 | t.Run("Passed", func(t *testing.T) { 101 | got := verify.Slice(list).Any(func(A) bool { return true }) 102 | assertPassed(t, got) 103 | }) 104 | t.Run("Failed", func(t *testing.T) { 105 | got := verify.Slice(list).Any(func(A) bool { return false }) 106 | assertFailed(t, got, "none item does meet the predicate criteria") 107 | }) 108 | }) 109 | t.Run("All", func(t *testing.T) { 110 | t.Run("Passed", func(t *testing.T) { 111 | got := verify.Slice(list).All(func(A) bool { return true }) 112 | assertPassed(t, got) 113 | }) 114 | t.Run("Failed", func(t *testing.T) { 115 | got := verify.Slice(list).All(func(A) bool { return false }) 116 | assertFailed(t, got, "an item does not meet the predicate criteria") 117 | }) 118 | }) 119 | t.Run("None", func(t *testing.T) { 120 | t.Run("Passed", func(t *testing.T) { 121 | got := verify.Slice(list).None(func(A) bool { return false }) 122 | assertPassed(t, got) 123 | }) 124 | t.Run("Failed", func(t *testing.T) { 125 | got := verify.Slice(list).None(func(A) bool { return true }) 126 | assertFailed(t, got, "an item meets the predicate criteria") 127 | }) 128 | }) 129 | 130 | t.Run("Len", func(t *testing.T) { 131 | t.Run("Passed", func(t *testing.T) { 132 | got := verify.Slice(list).Len(len(list)) 133 | assertPassed(t, got) 134 | }) 135 | t.Run("Failed", func(t *testing.T) { 136 | got := verify.Slice(list).Len(10) 137 | assertFailed(t, got, "has different length") 138 | }) 139 | }) 140 | } 141 | -------------------------------------------------------------------------------- /slice.go: -------------------------------------------------------------------------------- 1 | package verify 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/google/go-cmp/cmp" 7 | ) 8 | 9 | // FluentSlice encapsulates assertions for a slice. 10 | type FluentSlice[T any] struct { 11 | FluentAny[[]T] 12 | } 13 | 14 | // Slice is used for testing a slice. 15 | func Slice[T any](got []T) FluentSlice[T] { 16 | return FluentSlice[T]{FluentAny[[]T]{got}} 17 | } 18 | 19 | // Empty tests if the slice is empty. 20 | func (x FluentSlice[T]) Empty() FailureMessage { 21 | if len(x.Got) == 0 { 22 | return "" 23 | } 24 | return FailureMessage(fmt.Sprintf("not an empty slice\ngot: %+v", x.Got)) 25 | } 26 | 27 | // NotEmpty tests if the slice is not empty. 28 | func (x FluentSlice[T]) NotEmpty() FailureMessage { 29 | if len(x.Got) > 0 { 30 | return "" 31 | } 32 | return FailureMessage(fmt.Sprintf("an empty slice\ngot: %+v", x.Got)) 33 | } 34 | 35 | // Equivalent tests if the slice has the same items as want in any order. 36 | func (x FluentSlice[T]) Equivalent(want []T, opts ...cmp.Option) FailureMessage { 37 | extraGot, extraWant := x.diff(want, opts) 38 | if len(extraGot) == 0 && len(extraWant) == 0 { 39 | return "" 40 | } 41 | return FailureMessage(fmt.Sprintf("not equivalent\ngot: %+v\nwant: %+v\nextra got: %+v\nextra want: %+v", x.Got, want, extraGot, extraWant)) 42 | } 43 | 44 | // NotEquivalent tests if the slice does not have the same items as want in any order. 45 | func (x FluentSlice[T]) NotEquivalent(want []T, opts ...cmp.Option) FailureMessage { 46 | extraGot, extraWant := x.diff(want, opts) 47 | if len(extraGot) != 0 || len(extraWant) != 0 { 48 | return "" 49 | } 50 | return FailureMessage(fmt.Sprintf("equivalent\ngot: %+v", extraGot)) 51 | } 52 | 53 | func (x FluentSlice[T]) diff(want []T, opts []cmp.Option) (extraGot, extraWant []T) { 54 | aLen := len(x.Got) 55 | bLen := len(want) 56 | 57 | // Mark indexes in list that we already used 58 | visited := make([]bool, bLen) 59 | for i := 0; i < aLen; i++ { 60 | found := false 61 | for j := 0; j < bLen; j++ { 62 | if visited[j] { 63 | continue 64 | } 65 | if cmp.Equal(want[j], x.Got[i], opts...) { 66 | visited[j] = true 67 | found = true 68 | break 69 | } 70 | } 71 | if !found { 72 | extraGot = append(extraGot, x.Got[i]) 73 | } 74 | } 75 | 76 | for j := 0; j < bLen; j++ { 77 | if visited[j] { 78 | continue 79 | } 80 | extraWant = append(extraWant, want[j]) 81 | } 82 | 83 | return 84 | } 85 | 86 | // Contain tests if the slice contains the item. 87 | func (x FluentSlice[T]) Contain(item T, opts ...cmp.Option) FailureMessage { 88 | for _, v := range x.Got { 89 | if cmp.Equal(item, v, opts...) { 90 | return "" 91 | } 92 | } 93 | return FailureMessage(fmt.Sprintf("slice does not contain the item\ngot: %+v\nitem: %+v", x.Got, item)) 94 | } 95 | 96 | // NotContain tests if the slice does not contain the item. 97 | func (x FluentSlice[T]) NotContain(item T, opts ...cmp.Option) FailureMessage { 98 | for _, v := range x.Got { 99 | if cmp.Equal(item, v, opts...) { 100 | return FailureMessage(fmt.Sprintf("slice contains the item\ngot: %+v\nitem: %+v", x.Got, item)) 101 | } 102 | } 103 | return "" 104 | } 105 | 106 | // Any tests if any of the slice's items meets the predicate criteria. 107 | func (x FluentSlice[T]) Any(predicate func(T) bool) FailureMessage { 108 | for _, v := range x.Got { 109 | if predicate(v) { 110 | return "" 111 | } 112 | } 113 | return FailureMessage(fmt.Sprintf("none item does meet the predicate criteria\ngot: %+v", x.Got)) 114 | } 115 | 116 | // All tests if all of the slice's items meet the predicate criteria. 117 | func (x FluentSlice[T]) All(predicate func(T) bool) FailureMessage { 118 | for _, v := range x.Got { 119 | if !predicate(v) { 120 | return FailureMessage(fmt.Sprintf("an item does not meet the predicate criteria\ngot: %+v\nitem: %+v", x.Got, v)) 121 | } 122 | } 123 | return "" 124 | } 125 | 126 | // None tests if none of the slice's items meets the predicate criteria. 127 | func (x FluentSlice[T]) None(predicate func(T) bool) FailureMessage { 128 | for _, v := range x.Got { 129 | if predicate(v) { 130 | return FailureMessage(fmt.Sprintf("an item meets the predicate criteria\ngot: %+v\nitem: %+v", x.Got, v)) 131 | } 132 | } 133 | return "" 134 | } 135 | 136 | // Len tests the length of the slice. 137 | func (x FluentSlice[T]) Len(want int) FailureMessage { 138 | gotLen := len(x.Got) 139 | if gotLen != want { 140 | return FailureMessage(fmt.Sprintf("has different length\ngot: %+v\nlen: %v\nwant: %v", x.Got, gotLen, want)) 141 | } 142 | return "" 143 | } 144 | -------------------------------------------------------------------------------- /string.go: -------------------------------------------------------------------------------- 1 | package verify 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | // FluentString encapsulates assertions for string object. 10 | type FluentString[T ~string] struct { 11 | FluentOrdered[T] 12 | } 13 | 14 | // String is used for testing a string object. 15 | func String[T ~string](got T) FluentString[T] { 16 | return FluentString[T]{FluentOrdered[T]{FluentObj[T]{FluentAny[T]{got}}}} 17 | } 18 | 19 | // Empty tests if the string is not empty. 20 | func (x FluentString[T]) Empty() FailureMessage { 21 | if x.Got == "" { 22 | return "" 23 | } 24 | return FailureMessage(fmt.Sprintf("the value was not an empty string\ngot: %q", x.Got)) 25 | } 26 | 27 | // NotEmpty tests if the string is not empty. 28 | func (x FluentString[T]) NotEmpty() FailureMessage { 29 | if x.Got != "" { 30 | return "" 31 | } 32 | return "the value was \"\"" 33 | } 34 | 35 | // Contain tests if the string contains the substring. 36 | func (x FluentString[T]) Contain(substr string) FailureMessage { 37 | if strings.Contains(string(x.Got), substr) { 38 | return "" 39 | } 40 | return FailureMessage(fmt.Sprintf("the value does not contain the substring\ngot: %q\nsubstr: %q", x.Got, substr)) 41 | } 42 | 43 | // NotContain tests if the string does not contain the substring. 44 | func (x FluentString[T]) NotContain(substr string) FailureMessage { 45 | if !strings.Contains(string(x.Got), substr) { 46 | return "" 47 | } 48 | 49 | return FailureMessage(fmt.Sprintf("the value contains the substring\ngot: %q\nsubstr: %q", x.Got, substr)) 50 | } 51 | 52 | // Prefix tests if the string starts with the prefix. 53 | func (x FluentString[T]) Prefix(prefix string) FailureMessage { 54 | if strings.HasPrefix(string(x.Got), prefix) { 55 | return "" 56 | } 57 | return FailureMessage(fmt.Sprintf("the value does not have the prefix\ngot: %q\nprefix: %q", x.Got, prefix)) 58 | } 59 | 60 | // NoPrefix tests if the string does not start with the prefix. 61 | func (x FluentString[T]) NoPrefix(prefix string) FailureMessage { 62 | if !strings.HasPrefix(string(x.Got), prefix) { 63 | return "" 64 | } 65 | return FailureMessage(fmt.Sprintf("the value has the prefix\ngot: %q\nprefix: %q", x.Got, prefix)) 66 | } 67 | 68 | // Sufix tests if the string ends with the sufix. 69 | func (x FluentString[T]) Sufix(sufix string) FailureMessage { 70 | if strings.HasSuffix(string(x.Got), sufix) { 71 | return "" 72 | } 73 | return FailureMessage(fmt.Sprintf("the value does not have the sufix\ngot: %q\nsufix: %q", x.Got, sufix)) 74 | } 75 | 76 | // NoSufix tests if the string does not end with the sufix. 77 | func (x FluentString[T]) NoSufix(sufix string) FailureMessage { 78 | if !strings.HasSuffix(string(x.Got), sufix) { 79 | return "" 80 | } 81 | return FailureMessage(fmt.Sprintf("the value has the sufix\ngot: %q\nsufix: %q", x.Got, sufix)) 82 | } 83 | 84 | // EqualFold tests if the values interpreted as UTF-8 strings, 85 | // are equal under simple Unicode case-folding, 86 | // which is a more general form of case-insensitivity. 87 | func (x FluentString[T]) EqualFold(want string) FailureMessage { 88 | if strings.EqualFold(string(x.Got), want) { 89 | return "" 90 | } 91 | return FailureMessage(fmt.Sprintf("the string values are not equal under Unicode case folding\ngot: %q\nwant: %q", x.Got, want)) 92 | } 93 | 94 | // NotEqualFold tests if the values interpreted as UTF-8 strings, 95 | // are not equal under simple Unicode case-folding, 96 | // which is a more general form of case-insensitivity. 97 | func (x FluentString[T]) NotEqualFold(want string) FailureMessage { 98 | if !strings.EqualFold(string(x.Got), want) { 99 | return "" 100 | } 101 | return FailureMessage(fmt.Sprintf("the string values are equal under Unicode case folding\ngot: %q\nwant: %q", x.Got, want)) 102 | } 103 | 104 | // MatchRegex tests if the string matches the regular expression. 105 | func (x FluentString[T]) MatchRegex(regex *regexp.Regexp) FailureMessage { 106 | if regex.MatchString(string(x.Got)) { 107 | return "" 108 | } 109 | return FailureMessage(fmt.Sprintf("the string value does not match the regular expression\ngot: %q\nregex: %s", x.Got, regex.String())) 110 | } 111 | 112 | // NotMatchRegex tests if the string does not match the regular expression. 113 | func (x FluentString[T]) NotMatchRegex(regex *regexp.Regexp) FailureMessage { 114 | if !regex.MatchString(string(x.Got)) { 115 | return "" 116 | } 117 | return FailureMessage(fmt.Sprintf("the string value matches the regular expression\ngot: %q\nregex: %s", x.Got, regex.String())) 118 | } 119 | 120 | // Len tests the length of the string. 121 | func (x FluentString[T]) Len(want int) FailureMessage { 122 | gotLen := len(x.Got) 123 | if gotLen != want { 124 | return FailureMessage(fmt.Sprintf("has different length\ngot: %+v\nlen: %v\nwant: %v", x.Got, gotLen, want)) 125 | } 126 | return "" 127 | } 128 | -------------------------------------------------------------------------------- /map_test.go: -------------------------------------------------------------------------------- 1 | package verify_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/fluentassert/verify" 7 | ) 8 | 9 | func TestMap(t *testing.T) { 10 | type A struct { 11 | Str string 12 | Bool bool 13 | Slice []int 14 | } 15 | 16 | t.Run("has assertions from Any", func(t *testing.T) { 17 | want := map[string]A{ 18 | "id": {Str: "string", Bool: true, Slice: []int{1, 2, 3}}, 19 | } 20 | got := verify.Map(want).FluentAny.Got // type embedding done properly 21 | assertEqual(t, got, want) 22 | }) 23 | 24 | dict := map[string]A{ 25 | "a": {Str: "text", Bool: true, Slice: []int{1, 2, 3}}, 26 | "b": {Slice: []int{9, 8, 7}}, 27 | } 28 | has := map[string]A{ 29 | "a": {Str: "text", Bool: true, Slice: []int{1, 2, 3}}, 30 | } 31 | notHas := map[string]A{ 32 | "a": {Str: "text", Bool: true, Slice: []int{1, 2, 3}}, 33 | "b": {Slice: []int{1, 4, 7}}, 34 | "c": {Slice: []int{9, 8, 7}}, 35 | } 36 | t.Run("Empty", func(t *testing.T) { 37 | t.Run("Passed", func(t *testing.T) { 38 | got := verify.Map(map[string]A{}).Empty() 39 | assertPassed(t, got) 40 | }) 41 | t.Run("Failed", func(t *testing.T) { 42 | got := verify.Map(dict).Empty() 43 | assertFailed(t, got, "not an empty map") 44 | }) 45 | }) 46 | t.Run("NotEmpty", func(t *testing.T) { 47 | t.Run("Passed", func(t *testing.T) { 48 | got := verify.Map(dict).NotEmpty() 49 | assertPassed(t, got) 50 | }) 51 | t.Run("Failed", func(t *testing.T) { 52 | got := verify.Map(map[string]A{}).NotEmpty() 53 | assertFailed(t, got, "an empty map") 54 | }) 55 | }) 56 | 57 | t.Run("Contain", func(t *testing.T) { 58 | t.Run("Passed", func(t *testing.T) { 59 | got := verify.Map(dict).Contain(has) 60 | assertPassed(t, got) 61 | }) 62 | t.Run("Failed", func(t *testing.T) { 63 | got := verify.Map(dict).Contain(notHas) 64 | assertFailed(t, got, "not contains all pairs") 65 | }) 66 | }) 67 | t.Run("NotContain", func(t *testing.T) { 68 | t.Run("Passed", func(t *testing.T) { 69 | got := verify.Map(dict).NotContain(notHas) 70 | assertPassed(t, got) 71 | }) 72 | t.Run("Failed", func(t *testing.T) { 73 | got := verify.Map(dict).NotContain(has) 74 | assertFailed(t, got, "contains all pairs") 75 | }) 76 | }) 77 | 78 | t.Run("ContainPair", func(t *testing.T) { 79 | t.Run("Has", func(t *testing.T) { 80 | got := verify.Map(dict).ContainPair("b", A{Slice: []int{9, 8, 7}}) 81 | assertPassed(t, got) 82 | }) 83 | t.Run("DiffKey", func(t *testing.T) { 84 | got := verify.Map(dict).ContainPair("z", A{Slice: []int{9, 8, 7}}) 85 | assertFailed(t, got, "has no value under key") 86 | }) 87 | t.Run("DiffValue", func(t *testing.T) { 88 | got := verify.Map(dict).ContainPair("b", A{Slice: []int{1, 1, 1}}) 89 | assertFailed(t, got, "has different value under key") 90 | }) 91 | }) 92 | t.Run("NotContainPair", func(t *testing.T) { 93 | t.Run("Has", func(t *testing.T) { 94 | got := verify.Map(dict).NotContainPair("b", A{Slice: []int{9, 8, 7}}) 95 | assertFailed(t, got, "contains the pair") 96 | }) 97 | t.Run("DiffKey", func(t *testing.T) { 98 | got := verify.Map(dict).NotContainPair("z", A{Slice: []int{9, 8, 7}}) 99 | assertPassed(t, got) 100 | }) 101 | t.Run("DiffValue", func(t *testing.T) { 102 | got := verify.Map(dict).NotContainPair("b", A{Slice: []int{1, 1, 1}}) 103 | assertPassed(t, got) 104 | }) 105 | }) 106 | 107 | t.Run("Any", func(t *testing.T) { 108 | t.Run("Passed", func(t *testing.T) { 109 | got := verify.Map(dict).Any(func(string, A) bool { return true }) 110 | assertPassed(t, got) 111 | }) 112 | t.Run("Failed", func(t *testing.T) { 113 | got := verify.Map(dict).Any(func(string, A) bool { return false }) 114 | assertFailed(t, got, "none pair does meet the predicate criteria") 115 | }) 116 | }) 117 | t.Run("All", func(t *testing.T) { 118 | t.Run("Passed", func(t *testing.T) { 119 | got := verify.Map(dict).All(func(string, A) bool { return true }) 120 | assertPassed(t, got) 121 | }) 122 | t.Run("Failed", func(t *testing.T) { 123 | got := verify.Map(dict).All(func(string, A) bool { return false }) 124 | assertFailed(t, got, "a pair does not meet the predicate criteria") 125 | }) 126 | }) 127 | t.Run("None", func(t *testing.T) { 128 | t.Run("Passed", func(t *testing.T) { 129 | got := verify.Map(dict).None(func(string, A) bool { return false }) 130 | assertPassed(t, got) 131 | }) 132 | t.Run("Failed", func(t *testing.T) { 133 | got := verify.Map(dict).None(func(string, A) bool { return true }) 134 | assertFailed(t, got, "a pair meets the predicate criteria") 135 | }) 136 | }) 137 | 138 | t.Run("Len", func(t *testing.T) { 139 | t.Run("Passed", func(t *testing.T) { 140 | got := verify.Map(dict).Len(len(dict)) 141 | assertPassed(t, got) 142 | }) 143 | t.Run("Failed", func(t *testing.T) { 144 | got := verify.Map(dict).Len(10) 145 | assertFailed(t, got, "has different length") 146 | }) 147 | }) 148 | } 149 | -------------------------------------------------------------------------------- /string_test.go: -------------------------------------------------------------------------------- 1 | package verify_test 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | 7 | "github.com/fluentassert/verify" 8 | ) 9 | 10 | func TestString(t *testing.T) { 11 | t.Run("Empty", func(t *testing.T) { 12 | t.Run("Passed", func(t *testing.T) { 13 | msg := verify.String("").Empty() 14 | assertPassed(t, msg) 15 | }) 16 | t.Run("Failed", func(t *testing.T) { 17 | msg := verify.String("val").Empty() 18 | assertFailed(t, msg, "the value was not an empty string") 19 | }) 20 | }) 21 | 22 | t.Run("NotEmpty", func(t *testing.T) { 23 | t.Run("Passed", func(t *testing.T) { 24 | msg := verify.String("val").NotEmpty() 25 | assertPassed(t, msg) 26 | }) 27 | t.Run("Failed", func(t *testing.T) { 28 | msg := verify.String("").NotEmpty() 29 | assertFailed(t, msg, "the value was \"\"") 30 | }) 31 | }) 32 | 33 | t.Run("Contains", func(t *testing.T) { 34 | t.Run("Passed", func(t *testing.T) { 35 | msg := verify.String("text").Contain("ex") 36 | assertPassed(t, msg) 37 | }) 38 | t.Run("Failed", func(t *testing.T) { 39 | msg := verify.String("text").Contain("asd") 40 | assertFailed(t, msg, "the value does not contain the substring") 41 | }) 42 | }) 43 | 44 | t.Run("NotContains", func(t *testing.T) { 45 | t.Run("Passed", func(t *testing.T) { 46 | msg := verify.String("text").NotContain("asd") 47 | assertPassed(t, msg) 48 | }) 49 | t.Run("Failed", func(t *testing.T) { 50 | msg := verify.String("text").NotContain("ex") 51 | assertFailed(t, msg, "the value contains the substring") 52 | }) 53 | }) 54 | 55 | t.Run("Prefix", func(t *testing.T) { 56 | t.Run("Passed", func(t *testing.T) { 57 | msg := verify.String("[ok]a").Prefix("[ok]") 58 | assertPassed(t, msg) 59 | }) 60 | t.Run("Failed", func(t *testing.T) { 61 | msg := verify.String("a[ok]").Prefix("[ok]") 62 | assertFailed(t, msg, "the value does not have the prefix") 63 | }) 64 | }) 65 | 66 | t.Run("NoPrefix", func(t *testing.T) { 67 | t.Run("Passed", func(t *testing.T) { 68 | msg := verify.String("a[ok]").NoPrefix("[ok]") 69 | assertPassed(t, msg) 70 | }) 71 | t.Run("Failed", func(t *testing.T) { 72 | msg := verify.String("[ok]a").NoPrefix("[ok]") 73 | assertFailed(t, msg, "the value has the prefix") 74 | }) 75 | }) 76 | 77 | t.Run("Sufix", func(t *testing.T) { 78 | t.Run("Passed", func(t *testing.T) { 79 | msg := verify.String("a[ok]").Sufix("[ok]") 80 | assertPassed(t, msg) 81 | }) 82 | t.Run("Failed", func(t *testing.T) { 83 | msg := verify.String("[ok]a").Sufix("[ok]") 84 | assertFailed(t, msg, "the value does not have the sufix") 85 | }) 86 | }) 87 | 88 | t.Run("NoSufix", func(t *testing.T) { 89 | t.Run("Passed", func(t *testing.T) { 90 | msg := verify.String("[ok]a").NoSufix("[ok]") 91 | assertPassed(t, msg) 92 | }) 93 | t.Run("Failed", func(t *testing.T) { 94 | msg := verify.String("a[ok]").NoSufix("[ok]") 95 | assertFailed(t, msg, "the value has the sufix") 96 | }) 97 | }) 98 | 99 | t.Run("EqualFold", func(t *testing.T) { 100 | t.Run("Passed", func(t *testing.T) { 101 | msg := verify.String("aBc").EqualFold("ABC") 102 | assertPassed(t, msg) 103 | }) 104 | t.Run("Failed", func(t *testing.T) { 105 | msg := verify.String("aBc").EqualFold("aB") 106 | assertFailed(t, msg, "the string values are not equal under Unicode case folding") 107 | }) 108 | }) 109 | 110 | t.Run("NotEqualFold", func(t *testing.T) { 111 | t.Run("Passed", func(t *testing.T) { 112 | msg := verify.String("aBc").NotEqualFold("aB") 113 | assertPassed(t, msg) 114 | }) 115 | t.Run("Failed", func(t *testing.T) { 116 | msg := verify.String("aBc").NotEqualFold("ABC") 117 | assertFailed(t, msg, "the string values are equal under Unicode case folding") 118 | }) 119 | }) 120 | 121 | t.Run("MatchRegex", func(t *testing.T) { 122 | t.Run("Passed", func(t *testing.T) { 123 | msg := verify.String("3aD").MatchRegex(regexp.MustCompile("[0-9][a-z][A-Z]")) 124 | assertPassed(t, msg) 125 | }) 126 | t.Run("Failed", func(t *testing.T) { 127 | msg := verify.String("123").MatchRegex(regexp.MustCompile("[0-9][a-z][A-Z]")) 128 | assertFailed(t, msg, "the string value does not match the regular expression") 129 | }) 130 | }) 131 | 132 | t.Run("NotMatchRegex", func(t *testing.T) { 133 | t.Run("Passed", func(t *testing.T) { 134 | msg := verify.String("123").NotMatchRegex(regexp.MustCompile("[0-9][a-z][A-Z]")) 135 | assertPassed(t, msg) 136 | }) 137 | t.Run("Failed", func(t *testing.T) { 138 | msg := verify.String("3aD").NotMatchRegex(regexp.MustCompile("[0-9][a-z][A-Z]")) 139 | assertFailed(t, msg, "the string value matches the regular expression") 140 | }) 141 | }) 142 | 143 | t.Run("Len", func(t *testing.T) { 144 | t.Run("Passed", func(t *testing.T) { 145 | got := verify.String("abc").Len(3) 146 | assertPassed(t, got) 147 | }) 148 | t.Run("Failed", func(t *testing.T) { 149 | got := verify.String("abc").Len(10) 150 | assertFailed(t, got, "has different length") 151 | }) 152 | }) 153 | 154 | t.Run("has assertions from Ordered, Obj, Any", func(t *testing.T) { 155 | want := "text" 156 | got := verify.String(want).FluentOrdered.FluentObj.FluentAny.Got // type embedding done properly 157 | assertEqual(t, got, want) 158 | }) 159 | } 160 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this library are documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 | and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.3.0](https://github.com/fluentassert/verify/releases/tag/v1.2.0) - 2025-02-17 9 | 10 | ### Deprecated 11 | 12 | - Deprecate the whole library. Avoid using assertion libraries. 13 | Use the standard library and [github.com/google/go-cmp/cmp](https://pkg.go.dev/github.com/google/go-cmp/cmp) 14 | instead. Follow the official [Go Test Comments](https://go.dev/wiki/TestComments). 15 | 16 | ## [1.2.0](https://github.com/fluentassert/verify/releases/tag/v1.2.0) - 2024-09-10 17 | 18 | ### Added 19 | 20 | - Add `FailureMessage.Err` method together with `AssertionError` type 21 | to represent assertion results as `error` type. 22 | 23 | ## [1.1.0](https://github.com/fluentassert/verify/releases/tag/v1.1.0) - 2024-02-06 24 | 25 | This release adds length assertions. 26 | 27 | ### Added 28 | 29 | - Add `Len` assertion for `string`, `[]T`, `map[K]V` types. 30 | 31 | ## [1.0.0](https://github.com/fluentassert/verify/releases/tag/v1.0.0) - 2023-04-05 32 | 33 | This release contains breaking changes. 34 | 35 | The repository is moved to `github.com/fluentassert/verify` 36 | and the `f` package is renamed to `verify`. 37 | 38 | The main additions are the new assertions for 39 | `bool`, `constraints.Ordered`, `constraints.Float`, `constraints.Integer`, 40 | `string`, `error`, `[]T`, `map[K]V`, `func()` types. 41 | 42 | ### Added 43 | 44 | - Add `True`, `False`, assertion functions. 45 | - Add `Nil`, `NotNil`, assertion functions. 46 | - Add `NoError`, `IsError` assertion functions. 47 | - Add `Panics`, `NoPanic` assertion functions. 48 | - Add `Eventually`, `EventuallyChan` assertion functions. 49 | - Add `Ordered` function which provides following assertions, 50 | in addition to `Comparable`, via `FluentOrdered` type: 51 | - `Lesser` 52 | - `LesserOrEqual` 53 | - `GreaterOrEqual` 54 | - `Greater` 55 | - Add `String` function which provides following assertions, 56 | in addition to `Ordered`, via `FluentString` type: 57 | - `Empty` 58 | - `NotEmpty` 59 | - `Contain` 60 | - `NotContain` 61 | - `Prefix` 62 | - `NoPrefix` 63 | - `Sufix` 64 | - `NoSufix` 65 | - `EqualFold` 66 | - `NotEqualFold` 67 | - `MatchRegex` 68 | - `NotMatchRegex` 69 | - Add `Number` function which provides following assertions, 70 | in addition to `Ordered`, via `FluentNumber` type: 71 | - `InDelta` 72 | - `NotInDelta` 73 | - `InEpsilon` 74 | - `NotInEpsilon` 75 | - Add `Error` function which provides following assertions, 76 | in addition to `Any` and `String` (for error message), 77 | via `FluentObj` and `FluentString` types: 78 | - `Is` 79 | - `IsNot` 80 | - `As` 81 | - `NotAs` 82 | - Add `Slice` function which provides following assertions, 83 | in addition to `Any`, via `FluentSlice` type: 84 | - `Empty` 85 | - `NotEmpty` 86 | - `Equivalent` 87 | - `NotEquivalent` 88 | - `Contain` 89 | - `NotContain` 90 | - `Any` 91 | - `All` 92 | - `None` 93 | - Add `Map` function which provides following assertions, 94 | in addition to `Any`, via `FlientMap` type: 95 | - `Empty` 96 | - `NotEmpty` 97 | - `Contain` 98 | - `NotContain` 99 | - `ContainPair` 100 | - `NotContainPair` 101 | - `Any` 102 | - `All` 103 | - `None` 104 | - Add `FailureMessage.Prefix` method together with `And` and `Or` functions 105 | to facilitate creating complex assertions. 106 | 107 | ### Changed 108 | 109 | - The `f` package is renamed to `verify`. 110 | - Rename `Obj` and `FluentObj` to `Any` and `FluentAny`. 111 | - Rename `Comparable` and `FluentComparable` to `Obj` and `FluentObj`. 112 | - Change the `Check` assertion for `any` object so that the 113 | provided function has to return `FailureMessage` 114 | instead of a `string`. 115 | - `Zero` and `NonZero` methods are moved to `FluentComparable`. 116 | - Better failure messages. 117 | 118 | ## [0.2.0](https://github.com/fluentassert/verify/releases/tag/v0.2.0) - 2022-10-01 119 | 120 | This release is a complete rewrite. 121 | It is not compatible with the previous release. 122 | 123 | The new API is type-safe and easier to extend. 124 | 125 | The release provides assertions for `any`, `comparable`. 126 | 127 | The next release is supposed to provide assertions for 128 | `constraints.Ordered`, `string`, `error`, `[]T`, `map[K]V`, `func()`. 129 | 130 | ### Added 131 | 132 | - Add `FailureMessage` which encapsulates the failure message 133 | and methods for error reporting. 134 | - Add `Obj` function which provides following assertions 135 | via `FluentObject` type: 136 | - `Check` 137 | - `Should` 138 | - `ShouldNot` 139 | - `DeepEqual` 140 | - `NotDeepEqual` 141 | - `Zero` 142 | - `NonZero` 143 | - Add `Comparable` function which provides following assertions, 144 | in addition to `Obj`, via `FluentComparable` type: 145 | - `Equal` 146 | - `NotEqual` 147 | 148 | ### Changed 149 | 150 | - Require Go 1.18. 151 | 152 | ### Fixed 153 | 154 | - Fix error reporting line (use `t.Helper()` when available). 155 | 156 | ### Removed 157 | 158 | - Remove all previous functions and types (API rewrite). 159 | 160 | ## [0.1.0](https://github.com/fluentassert/verify/releases/tag/v0.1.0) - 2021-05-11 161 | 162 | First release after the experiential phase. 163 | 164 | ### Added 165 | 166 | - Add `f.Assert` that can be used instead of `t.Error` methods. 167 | - Add `f.Require` that can be used instead of `t.Fatal` methods. 168 | - Add `Should` assertion that can be used with custom predicates. 169 | - Add `Eq` assertion that checks if `got` is deeply equal to `want`. 170 | - Add `Nil` assertion that checks if `got` is `nil`. 171 | - Add `Err` assertion that checks if `got` is an `error`. 172 | - Add `Panic` assertion that checks if calling `got` results in a panic. 173 | - Add `NoPanic` assertion that checks if calling `got` returns without panicking. 174 | -------------------------------------------------------------------------------- /failmsg_test.go: -------------------------------------------------------------------------------- 1 | package verify_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/fluentassert/verify" 7 | ) 8 | 9 | func TestFailureMessage(t *testing.T) { 10 | t.Run("Assert", func(t *testing.T) { 11 | t.Run("Empty", func(t *testing.T) { 12 | mock := &errorMock{} 13 | got := verify.FailureMessage("").Assert(mock) 14 | assertTrue(t, got) 15 | assertEqual(t, mock, &errorMock{}) 16 | }) 17 | t.Run("NoArgs", func(t *testing.T) { 18 | mock := &errorMock{} 19 | got := verify.FailureMessage("failed").Assert(mock) 20 | assertFalse(t, got) 21 | assertEqual(t, mock, &errorMock{ 22 | HelperCalled: true, 23 | Called: true, 24 | Args: []any{"\nfailed"}, 25 | }) 26 | }) 27 | t.Run("WithArgs", func(t *testing.T) { 28 | mock := &errorMock{} 29 | got := verify.FailureMessage("failed").Assert(mock, "arg1", 2) 30 | assertFalse(t, got) 31 | assertEqual(t, mock, &errorMock{ 32 | HelperCalled: true, 33 | Called: true, 34 | Args: []any{"arg1", 2, "\nfailed"}, 35 | }) 36 | }) 37 | }) 38 | 39 | t.Run("Require", func(t *testing.T) { 40 | t.Run("Empty", func(t *testing.T) { 41 | mock := &fatalMock{} 42 | got := verify.FailureMessage("").Require(mock) 43 | assertTrue(t, got) 44 | assertEqual(t, mock, &fatalMock{}) 45 | }) 46 | t.Run("NoArgs", func(t *testing.T) { 47 | mock := &fatalMock{} 48 | got := verify.FailureMessage("failed").Require(mock) 49 | assertFalse(t, got) 50 | assertEqual(t, mock, &fatalMock{ 51 | HelperCalled: true, 52 | Called: true, 53 | Args: []any{"\nfailed"}, 54 | }) 55 | }) 56 | t.Run("WithArgs", func(t *testing.T) { 57 | mock := &fatalMock{} 58 | got := verify.FailureMessage("failed").Require(mock, "arg1", 2) 59 | assertFalse(t, got) 60 | assertEqual(t, mock, &fatalMock{ 61 | HelperCalled: true, 62 | Called: true, 63 | Args: []any{"arg1", 2, "\nfailed"}, 64 | }) 65 | }) 66 | }) 67 | 68 | t.Run("Assertf", func(t *testing.T) { 69 | t.Run("Empty", func(t *testing.T) { 70 | mock := &errorfMock{} 71 | got := verify.FailureMessage("").Assertf(mock, "should pass") 72 | assertTrue(t, got) 73 | assertEqual(t, mock, &errorfMock{}) 74 | }) 75 | t.Run("NoArgs", func(t *testing.T) { 76 | mock := &errorfMock{} 77 | got := verify.FailureMessage("failed").Assertf(mock, "should pass") 78 | assertFalse(t, got) 79 | assertEqual(t, mock, &errorfMock{ 80 | HelperCalled: true, 81 | Called: true, 82 | Format: "should pass%s", 83 | Args: []any{"\nfailed"}, 84 | }) 85 | }) 86 | t.Run("WithArgs", func(t *testing.T) { 87 | mock := &errorfMock{} 88 | got := verify.FailureMessage("failed").Assertf(mock, "should work %d", 1) 89 | assertFalse(t, got) 90 | assertEqual(t, mock, &errorfMock{ 91 | HelperCalled: true, 92 | Called: true, 93 | Format: "should work %d%s", 94 | Args: []any{1, "\nfailed"}, 95 | }) 96 | }) 97 | }) 98 | 99 | t.Run("Requiref", func(t *testing.T) { 100 | t.Run("Empty", func(t *testing.T) { 101 | mock := &fatalfMock{} 102 | got := verify.FailureMessage("").Requiref(mock, "should pass") 103 | assertTrue(t, got) 104 | assertEqual(t, mock, &fatalfMock{}) 105 | }) 106 | t.Run("NoArgs", func(t *testing.T) { 107 | mock := &fatalfMock{} 108 | got := verify.FailureMessage("failed").Requiref(mock, "should pass") 109 | assertFalse(t, got) 110 | assertEqual(t, mock, &fatalfMock{ 111 | HelperCalled: true, 112 | Called: true, 113 | Format: "should pass%s", 114 | Args: []any{"\nfailed"}, 115 | }) 116 | }) 117 | t.Run("WithArgs", func(t *testing.T) { 118 | mock := &fatalfMock{} 119 | got := verify.FailureMessage("failed").Requiref(mock, "should work %d", 1) 120 | assertFalse(t, got) 121 | assertEqual(t, mock, &fatalfMock{ 122 | HelperCalled: true, 123 | Called: true, 124 | Format: "should work %d%s", 125 | Args: []any{1, "\nfailed"}, 126 | }) 127 | }) 128 | }) 129 | 130 | t.Run("Prefix", func(t *testing.T) { 131 | t.Run("Empty", func(t *testing.T) { 132 | got := verify.FailureMessage("").Prefix("[fail]") 133 | assertEqual(t, got, "") 134 | }) 135 | t.Run("Empty", func(t *testing.T) { 136 | got := verify.FailureMessage("errored").Prefix("[fail] ") 137 | assertEqual(t, got, "[fail] errored") 138 | }) 139 | }) 140 | 141 | t.Run("AsError", func(t *testing.T) { 142 | t.Run("With Message", func(t *testing.T) { 143 | got := verify.FailureMessage("failed").Err() 144 | assertEqual(t, got.Error(), "failed") 145 | }) 146 | 147 | t.Run("Empty", func(t *testing.T) { 148 | got := verify.FailureMessage("").Err() 149 | assertEqual(t, got, nil) 150 | }) 151 | }) 152 | } 153 | 154 | type errorMock struct { 155 | Called bool 156 | Args []any 157 | HelperCalled bool 158 | } 159 | 160 | func (mock *errorMock) Error(args ...any) { 161 | mock.Called = true 162 | mock.Args = args 163 | } 164 | 165 | func (mock *errorMock) Helper() { 166 | mock.HelperCalled = true 167 | } 168 | 169 | type fatalMock struct { 170 | Called bool 171 | Args []any 172 | HelperCalled bool 173 | } 174 | 175 | func (mock *fatalMock) Fatal(args ...any) { 176 | mock.Called = true 177 | mock.Args = args 178 | } 179 | 180 | func (mock *fatalMock) Helper() { 181 | mock.HelperCalled = true 182 | } 183 | 184 | type errorfMock struct { 185 | Called bool 186 | Format string 187 | Args []any 188 | HelperCalled bool 189 | } 190 | 191 | func (mock *errorfMock) Errorf(format string, args ...any) { 192 | mock.Called = true 193 | mock.Format = format 194 | mock.Args = args 195 | } 196 | 197 | func (mock *errorfMock) Helper() { 198 | mock.HelperCalled = true 199 | } 200 | 201 | type fatalfMock struct { 202 | Called bool 203 | Format string 204 | Args []any 205 | HelperCalled bool 206 | } 207 | 208 | func (mock *fatalfMock) Fatalf(format string, args ...any) { 209 | mock.Called = true 210 | mock.Format = format 211 | mock.Args = args 212 | } 213 | 214 | func (mock *fatalfMock) Helper() { 215 | mock.HelperCalled = true 216 | } 217 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fluentassert 2 | 3 | > [!CAUTION] 4 | > [Avoid using assertion libraries](https://go.dev/wiki/TestComments#assert-libraries). 5 | > Instead, use [`go-cmp`](https://github.com/google/go-cmp) 6 | > and write custom test helpers. 7 | > Using the popular [`testify`](https://github.com/stretchr/testify) 8 | > may be also an acceptable choice, 9 | > especially together with [`testifylint`](https://github.com/Antonboom/testifylint) 10 | > to avoid common mistakes. 11 | > Use this library if you still want to. 12 | > Consider yourself warned. 13 | 14 | [![Go Reference](https://pkg.go.dev/badge/github.com/fluentassert/verify.svg)](https://pkg.go.dev/github.com/fluentassert/verify) 15 | [![Keep a Changelog](https://img.shields.io/badge/changelog-Keep%20a%20Changelog-%23E05735)](CHANGELOG.md) 16 | [![GitHub Release](https://img.shields.io/github/v/release/fluentassert/verify)](https://github.com/fluentassert/verify/releases) 17 | [![go.mod](https://img.shields.io/github/go-mod/go-version/fluentassert/verify)](go.mod) 18 | [![LICENSE](https://img.shields.io/github/license/fluentassert/verify)](LICENSE) 19 | 20 | [![Build Status](https://img.shields.io/github/actions/workflow/status/fluentassert/verify/build.yml?branch=main)](https://github.com/fluentassert/verify/actions?query=workflow%3Abuild+branch%3Amain) 21 | [![Go Report Card](https://goreportcard.com/badge/github.com/fluentassert/verify)](https://goreportcard.com/report/github.com/fluentassert/verify) 22 | [![Codecov](https://codecov.io/gh/fluentassert/verify/branch/main/graph/badge.svg)](https://codecov.io/gh/fluentassert/verify) 23 | 24 | ## Description 25 | 26 | The fluent API makes the assertion code easier 27 | to read and write ([more](https://dave.cheney.net/2019/09/24/be-wary-of-functions-which-take-several-parameters-of-the-same-type)). 28 | 29 | The generics (type parameters) make the usage type-safe. 30 | 31 | The library is [extensible](#extensibility) by design. 32 | 33 | ### Quick start 34 | 35 | ```go 36 | package test 37 | 38 | import ( 39 | "testing" 40 | 41 | "github.com/fluentassert/verify" 42 | ) 43 | 44 | func Foo() (string, error) { 45 | return "wrong", nil 46 | } 47 | 48 | func TestFoo(t *testing.T) { 49 | got, err := Foo() 50 | 51 | verify.NoError(err).Require(t) // Require(f) uses t.Fatal(f), stops execution if fails 52 | verify.String(got).Equal("ok").Assert(t) // Assert(f) uses t.Error(f), continues execution if fails 53 | } 54 | ``` 55 | 56 | ```sh 57 | $ go test 58 | --- FAIL: TestFoo (0.00s) 59 | basic_test.go:17: 60 | the objects are not equal 61 | got: "wrong" 62 | want: "ok" 63 | ``` 64 | 65 | ⚠ Do not forget calling 66 | [`Assert(t)`](https://pkg.go.dev/github.com/fluentassert/verify#FailureMessage.Assert) 67 | or [`Require(t)`](https://pkg.go.dev/github.com/fluentassert/verify#FailureMessage.Require) 68 | which executes the actual assertion. 69 | 70 | ## Supported types 71 | 72 | Out-of-the-box the package provides fluent assertions for the following types. 73 | The more specific function you use, the more assertions you get. 74 | 75 | | Go type | Assertion entry point | 76 | | - | - | 77 | | `interface{}` ([`any`](https://pkg.go.dev/builtin#any)) | [`verify.Any()`](https://pkg.go.dev/github.com/fluentassert/verify#Any) | 78 | | [`comparable`](https://pkg.go.dev/builtin#comparable) | [`verify.Obj()`](https://pkg.go.dev/github.com/fluentassert/verify#Obj) | 79 | | [`constraints.Ordered`](https://pkg.go.dev/golang.org/x/exp/constraints#Ordered) | [`verify.Ordered()`](https://pkg.go.dev/github.com/fluentassert/verify#Ordered) | 80 | | [`constraints.Number`](https://pkg.go.dev/golang.org/x/exp/constraints#Number) | [`verify.Number()`](https://pkg.go.dev/github.com/fluentassert/verify#Number) | 81 | | [`string`](https://pkg.go.dev/builtin#string) | [`verify.String()`](https://pkg.go.dev/github.com/fluentassert/verify#String) | 82 | | [`error`](https://go.dev/ref/spec#Errors) | [`verify.Error()`](https://pkg.go.dev/github.com/fluentassert/verify#Error) | 83 | | `[]T` ([slice](https://go.dev/ref/spec#Slice_types)) | [`verify.Slice()`](https://pkg.go.dev/github.com/fluentassert/verify#Slice) | 84 | | `map[K]V` ([map](https://go.dev/ref/spec#Map_types)) | [`verify.Map()`](https://pkg.go.dev/github.com/fluentassert/verify#Map) | 85 | 86 | Below you can find some convenience functions. 87 | 88 | - [`verify.NoError()`](https://pkg.go.dev/github.com/fluentassert/verify#NoError) 89 | - [`verify.IsError()`](https://pkg.go.dev/github.com/fluentassert/verify#IsError) 90 | - [`verify.Nil()`](https://pkg.go.dev/github.com/fluentassert/verify#Nil) 91 | - [`verify.NotNil()`](https://pkg.go.dev/github.com/fluentassert/verify#NotNil) 92 | - [`verify.True()`](https://pkg.go.dev/github.com/fluentassert/verify#True) 93 | - [`verify.False()`](https://pkg.go.dev/github.com/fluentassert/verify#False) 94 | 95 | ### Deep equality 96 | 97 | For testing deep equality use 98 | [`DeepEqual()`](https://pkg.go.dev/github.com/fluentassert/verify#FluentAny.DeepEqual) 99 | or [`NotDeepEqual()`](https://pkg.go.dev/github.com/fluentassert/verify#FluentAny.NotDeepEqual). 100 | 101 | ```go 102 | package test 103 | 104 | import ( 105 | "testing" 106 | 107 | "github.com/fluentassert/verify" 108 | ) 109 | 110 | type A struct { 111 | Str string 112 | Bool bool 113 | Slice []int 114 | } 115 | 116 | func TestDeepEqual(t *testing.T) { 117 | got := A{Str: "wrong", Slice: []int{1, 4}} 118 | 119 | verify.Any(got).DeepEqual( 120 | A{Str: "string", Bool: true, Slice: []int{1, 2}}, 121 | ).Assert(t) 122 | } 123 | ``` 124 | 125 | ```sh 126 | $ go test 127 | --- FAIL: TestDeepEqual (0.00s) 128 | deepeq_test.go:20: 129 | mismatch (-want +got): 130 | test.A{ 131 | - Str: "string", 132 | + Str: "wrong", 133 | - Bool: true, 134 | + Bool: false, 135 | Slice: []int{ 136 | 1, 137 | - 2, 138 | + 4, 139 | }, 140 | } 141 | ``` 142 | 143 | ### Collection assertions 144 | 145 | The library contains many collection assertions. 146 | Below is an example of checking unordered equality. 147 | 148 | ```go 149 | package test 150 | 151 | import ( 152 | "testing" 153 | 154 | "github.com/fluentassert/verify" 155 | ) 156 | 157 | func TestSlice(t *testing.T) { 158 | got := []int { 3, 1, 2 } 159 | 160 | verify.Slice(got).Equivalent([]int { 2, 3, 4 }).Assert(t) 161 | } 162 | ``` 163 | 164 | ```sh 165 | $ go test 166 | --- FAIL: TestSlice (0.00s) 167 | slice_test.go:12: 168 | not equivalent 169 | got: [3 1 2] 170 | want: [2 3 4] 171 | extra got: [1] 172 | extra want: [4] 173 | ``` 174 | 175 | ### Periodic polling 176 | 177 | For asynchronous testing you can use 178 | [`verify.Eventually()`](https://pkg.go.dev/github.com/fluentassert/verify#Eventually) 179 | or [`verify.EventuallyChan()`](https://pkg.go.dev/github.com/fluentassert/verify#EventuallyChan). 180 | 181 | ```go 182 | package test 183 | 184 | import ( 185 | "net/http" 186 | "testing" 187 | "time" 188 | 189 | "github.com/fluentassert/verify" 190 | ) 191 | 192 | func TestPeriodic(t *testing.T) { 193 | verify.Eventually(10*time.Second, time.Second, func() verify.FailureMessage { 194 | client := http.Client{Timeout: time.Second} 195 | resp, err := client.Get("http://not-existing:1234") 196 | if err != nil { 197 | return verify.NoError(err) 198 | } 199 | return verify.Number(resp.StatusCode).Lesser(300) 200 | }).Assert(t) 201 | } 202 | ``` 203 | 204 | ```sh 205 | $ go test 206 | --- FAIL: TestPeriodic (10.00s) 207 | async_test.go:19: 208 | function never passed, last failure message: 209 | Get "http://not-existing:1234": context deadline exceeded (Client.Timeout exceeded while awaiting headers) 210 | ``` 211 | 212 | ### Custom predicates 213 | 214 | For the most basic scenarios, you can use one of the 215 | [`Check()`](https://pkg.go.dev/github.com/fluentassert/verify#FluentAny.Check), 216 | [`Should()`](https://pkg.go.dev/github.com/fluentassert/verify#FluentAny.Should), 217 | [`ShouldNot()`](https://pkg.go.dev/github.com/fluentassert/verify#FluentAny.ShouldNot) 218 | assertions. 219 | 220 | ```go 221 | package test 222 | 223 | import ( 224 | "strings" 225 | "testing" 226 | 227 | "github.com/fluentassert/verify" 228 | ) 229 | 230 | func TestShould(t *testing.T) { 231 | got := "wrong" 232 | 233 | chars := "abc" 234 | verify.Any(got).Should(func(got string) bool { 235 | return strings.ContainsAny(got, chars) 236 | }).Assertf(t, "does not contain any of: %s", chars) 237 | } 238 | ``` 239 | 240 | ```sh 241 | $ go test 242 | --- FAIL: TestShould (0.00s) 243 | should_test.go:16: does not contain any of: abc 244 | object does not meet the predicate criteria 245 | got: "wrong" 246 | ``` 247 | 248 | ### Panics 249 | 250 | For testing panics use [`verify.Panics()`](https://pkg.go.dev/github.com/fluentassert/verify#Panics) 251 | and [`verify.NotPanics()`](https://pkg.go.dev/github.com/fluentassert/verify#NotPanics). 252 | 253 | ### Custom assertion function 254 | 255 | You can create a function that returns [`FailureMessage`](https://pkg.go.dev/github.com/fluentassert/verify#FailureMessage). 256 | Use [`verify.And()`](https://pkg.go.dev/github.com/fluentassert/verify#And) 257 | and [`verify.Or()`](https://pkg.go.dev/github.com/fluentassert/verify#Or) 258 | functions together with [`Prefix()`](https://pkg.go.dev/github.com/fluentassert/verify#FailureMessage.Prefix) 259 | method to create complex assertions. 260 | 261 | ```go 262 | package test 263 | 264 | import ( 265 | "testing" 266 | 267 | "github.com/fluentassert/verify" 268 | ) 269 | 270 | type A struct { 271 | Str string 272 | Ok bool 273 | } 274 | 275 | func TestCustom(t *testing.T) { 276 | got := A{Str: "something was wrong"} 277 | 278 | verifyA(got).Assert(t) 279 | } 280 | 281 | func verifyA(got A) verify.FailureMessage { 282 | return verify.And( 283 | verify.String(got.Str).Contain("ok").Prefix("got.String: "), 284 | verify.True(got.Ok).Prefix("got.Ok: "), 285 | ) 286 | } 287 | ``` 288 | 289 | ```sh 290 | $ go test 291 | --- FAIL: TestCustom (0.00s) 292 | custom_test.go:17: 293 | got.String: the value does not contain the substring 294 | got: "something was wrong" 295 | substr: "ok" 296 | 297 | got.Ok: the value is false 298 | ``` 299 | 300 | ## Extensibility 301 | 302 | You can take advantage of the [`FailureMessage`](https://pkg.go.dev/github.com/fluentassert/verify#FailureMessage) 303 | and `Fluent*` types 304 | to create your own fluent assertions for a given type. 305 | 306 | For reference, take a look at the implementation 307 | of existing fluent assertions in this repository 308 | (for example [comparable.go](comparable.go)). 309 | 310 | ## Supported Go versions 311 | 312 | Minimal supported Go version is 1.18. 313 | 314 | ## Contributing 315 | 316 | See [CONTRIBUTING.md](CONTRIBUTING.md) if you want to help. 317 | 318 | ## License 319 | 320 | **fluentassert** is licensed under the terms of [the MIT license](LICENSE). 321 | 322 | [`github.com/google/go-cmp`](https://github.com/google/go-cmp) 323 | (license: [BSD-3-Clause](https://pkg.go.dev/github.com/google/go-cmp/cmp?tab=licenses)) 324 | is the only [third-party dependency](go.mod). 325 | --------------------------------------------------------------------------------