├── .envrc ├── .github └── workflows │ ├── golang.yaml │ └── nix.yaml ├── .gitignore ├── .golangci.yaml ├── .vscode ├── extensions.json └── settings.json ├── Justfile ├── LICENSE ├── README.md ├── VERSION ├── assert ├── assert.go ├── assert_test.go ├── nofailures.go └── nofailures_test.go ├── check ├── check.go └── check_test.go ├── common └── t.go ├── default.nix ├── example_test.go ├── flake.lock ├── flake.nix ├── go.mod ├── go.sum ├── main_test.go └── shell.nix /.envrc: -------------------------------------------------------------------------------- 1 | # Load the development shell using Nix, via one of: 2 | # 3 | # - Lorri https://github.com/nix-community/lorri 4 | # - use flake https://direnv.net/man/direnv-stdlib.1.html#codeuse-flake-ltinstallablegtcode 5 | # - use nix https://direnv.net/man/direnv-stdlib.1.html#codeuse-nix-code 6 | # 7 | # in that order of preference 8 | 9 | if has lorri; then 10 | echo "direnv: loading env from lorri ($(type -p lorri))" 11 | eval "$(lorri direnv)" 12 | exit 13 | fi 14 | 15 | if has use_flake; then 16 | echo "direnv: loading env from use_flake" 17 | use flake 18 | exit 19 | fi 20 | 21 | if has use_nix; then 22 | echo "direnv: loading env from use_nix" 23 | use nix 24 | exit 25 | fi 26 | 27 | echo "direnv: failed to load env" 28 | -------------------------------------------------------------------------------- /.github/workflows/golang.yaml: -------------------------------------------------------------------------------- 1 | name: golang 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: setup-go 13 | uses: actions/setup-go@v5 14 | with: 15 | go-version: 1.24 # matches `go version` in nix devshell 16 | cache: true 17 | cache-dependency-path: go.sum 18 | - name: build 19 | run: go build -v ./... 20 | - name: test 21 | run: go test ./... 22 | lint: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | - name: setup-go 27 | uses: actions/setup-go@v5 28 | with: 29 | go-version: 1.24 # matches `go version` in nix devshell 30 | cache: true 31 | cache-dependency-path: go.sum 32 | - name: golangci-lint 33 | uses: golangci/golangci-lint-action@v6.5.0 34 | with: 35 | install-mode: "binary" 36 | version: v1.64.5 # matches `golangci-lint version` in nix devshell 37 | verify: true 38 | -------------------------------------------------------------------------------- /.github/workflows/nix.yaml: -------------------------------------------------------------------------------- 1 | name: nix 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | jobs: 8 | nix-devshell: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Install Nix 13 | uses: cachix/install-nix-action@v30 14 | with: 15 | nix_path: nixpkgs=channel:nixos-unstable 16 | extra_nix_config: | 17 | access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} 18 | - run: nix develop --command nixpkgs-fmt --check *.nix 19 | - run: nix flake check 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # created by the vscode debugger 2 | __debug_bin 3 | 4 | # we use this to store the golang modules and caches 5 | .toolchain/ 6 | 7 | # When you run `nix-build`, it symlinks the outputs at ./result -> 8 | result 9 | 10 | # When you run `just build`, it stores the output in bin/ 11 | bin/ 12 | 13 | # The direnv use_flake integration seems to put content here 14 | .direnv/ 15 | 16 | # Used by `air` to hold build outputs 17 | tmp/ 18 | 19 | # macos garbage 20 | .DS_Store 21 | **/.DS_Store 22 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/golangci-lint.json 2 | # https://golangci-lint.run/usage/configuration/ 3 | linters-settings: 4 | gocritic: 5 | disabled-checks: 6 | - ifElseChain 7 | goimports: 8 | local-prefixes: github.com/peterldowns/testy 9 | govet: 10 | enable-all: true 11 | disable: 12 | - fieldalignment 13 | exhaustive: 14 | default-signifies-exhaustive: true 15 | nolintlint: 16 | allow-unused: false 17 | allow-no-explanation: 18 | - gochecknoglobals 19 | - gochecknoinits 20 | - unparam 21 | require-explanation: true 22 | require-specific: true 23 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md 24 | revive: 25 | enable-all-rules: false 26 | rules: 27 | - name: atomic 28 | - name: blank-imports 29 | - name: bare-return 30 | - name: bool-literal-in-expr 31 | - name: call-to-gc 32 | - name: comment-spacings 33 | arguments: ["nolint"] 34 | - name: confusing-results 35 | - name: constant-logical-expr 36 | - name: context-as-argument 37 | arguments: 38 | - allowTypesBefore: "*testing.T" 39 | - name: context-keys-type 40 | - name: datarace 41 | # - name: deep-exit 42 | - name: defer 43 | - name: dot-imports 44 | - name: duplicated-imports 45 | - name: early-return 46 | - name: empty-block 47 | - name: empty-lines 48 | - name: error-naming 49 | - name: error-return 50 | - name: error-strings 51 | - name: errorf 52 | # - name: exported 53 | - name: function-result-limit 54 | arguments: [3] 55 | - name: get-return 56 | - name: identical-branches 57 | - name: if-return 58 | - name: imports-blocklist 59 | arguments: 60 | - "github.com/stretchr/testify" 61 | - "github.com/stretchr/testify/require" 62 | - "github.com/stretchr/testify/assert" 63 | - name: import-shadowing 64 | - name: increment-decrement 65 | - name: indent-error-flow 66 | - name: modifies-parameter 67 | - name: modifies-value-receiver 68 | # - name: nested-structs 69 | # - name: package-comments 70 | - name: range 71 | - name: range-val-address 72 | - name: range-val-in-closure 73 | - name: receiver-naming 74 | - name: redefines-builtin-id 75 | - name: string-format 76 | - name: string-of-int 77 | - name: struct-tag 78 | - name: superfluous-else 79 | - name: time-equal 80 | - name: time-naming 81 | - name: unconditional-recursion 82 | - name: unexported-naming 83 | - name: unexported-return 84 | - name: unhandled-error 85 | arguments: 86 | - "fmt.*" 87 | - "strings.Builder.WriteString" 88 | - name: unnecessary-stmt 89 | - name: unreachable-code 90 | - name: unused-parameter 91 | - name: unused-receiver 92 | - name: use-any 93 | - name: useless-break 94 | - name: var-declaration 95 | - name: waitgroup-by-value 96 | 97 | run: 98 | tests: true 99 | timeout: 1m 100 | 101 | # https://golangci-lint.run/usage/linters/ 102 | linters: 103 | disable-all: true 104 | enable: 105 | - asciicheck 106 | - errcheck 107 | - exhaustive 108 | - gocritic 109 | - gofmt 110 | - gofumpt 111 | - goimports 112 | - gomodguard 113 | - govet 114 | - ineffassign 115 | - nolintlint 116 | - revive 117 | - staticcheck 118 | - typecheck 119 | - unused 120 | - whitespace 121 | - paralleltest 122 | - errname 123 | - errorlint 124 | - gosimple 125 | - unparam 126 | issues: 127 | exclude-use-default: false 128 | exclude: 129 | # Allow shadowing of `err` because it's so common 130 | - 'declaration of "err" shadows declaration at' 131 | # Upstream Defaults from 132 | # https://golangci-lint.run/usage/false-positives/#default-exclusions 133 | - 'Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*print(f|ln)?|os\.(Un)?Setenv). is not checked' 134 | exclude-rules: [] 135 | max-same-issues: 10000 136 | max-issues-per-linter: 10000 137 | exclude-dirs-use-default: false 138 | exclude-dirs: 139 | - ^/nix/store/.* 140 | - .*/.toolchain-*/.* 141 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "golang.go", 4 | "jnoortheen.nix-ide", 5 | "skellock.just", 6 | "timonwong.shellcheck", 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/.git": true, 4 | "**/.DS_Store": true, 5 | "**/__debug_bin": true, 6 | ".go/": true, 7 | ".toolchain/": true, 8 | "bin": true, 9 | "result": true, 10 | "tmp": true 11 | }, 12 | "explorer.expandSingleFolderWorkspaces": true, 13 | "go.autocompleteUnimportedPackages": true, 14 | "go.coverOnSingleTest": true, 15 | "go.coverOnSingleTestFile": true, 16 | "go.lintTool": "golangci-lint", 17 | "go.lintFlags": ["--config=${workspaceFolder}/.golangci.yaml", "--fast", "--fix"], 18 | "go.lintOnSave": "package", 19 | "go.testEnvVars": {}, 20 | "go.testFlags": ["-tags", "manual"], 21 | "go.coverageOptions": "showCoveredCodeOnly", 22 | "go.buildOnSave": "off", 23 | "go.vetOnSave": "off", 24 | "go.useLanguageServer": true, 25 | "go.toolsManagement.autoUpdate": false, 26 | "go.toolsManagement.checkForUpdates": "off", 27 | "gopls": { 28 | "buildFlags": ["-tags", "manual"], 29 | "formatting.gofumpt": true, 30 | "formatting.local": "github.com/peterldowns/check", 31 | "build.directoryFilters": [ 32 | "-.toolchain", 33 | "-.direnv" 34 | ] 35 | }, 36 | "[go]": { 37 | "editor.formatOnSave": true 38 | }, 39 | "nix.formatterPath": "nixpkgs-fmt", 40 | "[nix]": { 41 | "editor.formatOnSave": true 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | # This Justfile contains rules/targets/scripts/commands that are used when 2 | # developing. Unlike a Makefile, running `just ` will always invoke 3 | # that command. For more information, see https://github.com/casey/just 4 | # 5 | # 6 | # this setting will allow passing arguments through to tasks, see the docs here 7 | # https://just.systems/man/en/chapter_24.html#positional-arguments 8 | set positional-arguments 9 | 10 | # print all available commands by default 11 | help: 12 | @just --list 13 | 14 | # run the test suite 15 | test *args='./...': 16 | go test "$@" 17 | 18 | # lint go and nix 19 | lint *args: 20 | @just lint-go "$@" 21 | @just lint-nix 22 | 23 | # lint golang 24 | lint-go *args: 25 | golangci-lint config verify --config .golangci.yaml 26 | golangci-lint run --fix --config .golangci.yaml "$@" 27 | 28 | # lint nix 29 | lint-nix: 30 | find . -name '*.nix' | xargs nixpkgs-fmt 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Peter Downs 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 😤 Testy 2 | 3 | ![Latest Version](https://badgers.space/badge/latest%20version/v0.0.6/blueviolet?corner_radius=m) 4 | ![Golang](https://badgers.space/badge/golang/1.21+/blue?corner_radius=m) 5 | 6 | Testy is a library for writing meaningful, readable, and maintainable tests. 7 | Testy is typesafe (using generics), based on [go-cmp](https://github.com/google/go-cmp), and designed as an alternative 8 | to [testify](https://github.com/stretchr/testify), [gotools.test](https://github.com/gotestyourself/gotest.tools), and [is](https://github.com/matryer/is). 9 | 10 | Major features: 11 | - Typesafe comparisons mean that when you refactor your code, your tests 12 | can be easily updated. 13 | - A limited number of methods makes it easier to write tests by constraining your options. 14 | - Extendable: if you need a more complicated assertion method, write your own helper functions. 15 | - Deep equality testing by default using [go-cmp](https://github.com/google/go-cmp). 16 | - **checks**: soft assertions using `t.Fail`, the test is marked as failed but will continue running. 17 | - **asserts**: hard assertions using `t.FailNow`, the test is marked as failed and stops immediately. 18 | - Optional helpers for structuring tests in more readable ways. 19 | 20 | ## Install 21 | 22 | ```shell 23 | go get github.com/peterldowns/testy@latest 24 | ``` 25 | 26 | ## Documentation 27 | - [The github README, https://github.com/peterldowns/testy](https://github.com/peterldowns/testy) 28 | - [The go.dev docs, pkg.go.dev/github.com/peterldowns/testy](https://pkg.go.dev/github.com/peterldowns/testy) 29 | 30 | The github README is the primary source for documentation. The code itself is 31 | supposed to be well-organized, and each function has a meaningful docstring, so 32 | you should be able to explore it quite easily using an LSP plugin, reading the 33 | code, or clicking through the go.dev docs. 34 | 35 | 36 | ## API 37 | 38 | - `True(t, x)` checks if `x == true` 39 | - `False(t, x)` checks if `x == false` 40 | - `Equal(t, want, got)` checks if its arguments are equal using [go-cmp](https://github.com/google/go-cmp) 41 | - `NotEqual(t, want, got)` checks if its arguments are not equal using [go-cmp](https://github.com/google/go-cmp) 42 | - `LessThan(t, small, big)` checks if `small < big` 43 | - `LessThanOrEqual(t, small, big)` checks if `small <= big` 44 | - `GreaterThan(t, big, small)` checks if `big > small` 45 | - `GreaterThanOrEqual(t, big, small)` checks if `big >= small` 46 | - `Error(t, err)` checks if `err == nil` 47 | - `NoError(t, err)` checks if `err != nil` 48 | - `In(t, item, slice)` checks if `item in slice` 49 | - `NotIn(t, item, slice)` checks if `item not in slice` 50 | - `Nil(t, val)` checks if `val == nil` using reflection to support any nilable value. 51 | - `NotNil(t, val)` checks if `val != nil` using reflection to support any nilable value. 52 | 53 | ```go 54 | package api_test 55 | 56 | import ( 57 | "fmt" 58 | "testing" 59 | 60 | "github.com/peterldowns/testy/assert" 61 | "github.com/peterldowns/testy/check" 62 | ) 63 | 64 | func TestChecks(t *testing.T) { 65 | t.Parallel() 66 | check.True(t, true) 67 | check.False(t, false) 68 | check.Equal(t, []string{"hello"}, []string{"hello"}) 69 | check.NotEqual(t, 70 | map[string]int{"hello": 1}, 71 | map[string]int{"goodbye": 2}, 72 | ) 73 | check.LessThan(t, 1, 4) 74 | check.LessThanOrEqual(t, 4, 4) 75 | check.GreaterThan(t, 8, 6) 76 | check.GreaterThanOrEqual(t, 6, 6) 77 | check.Error(t, fmt.Errorf("oh no")) 78 | check.NoError(t, nil) 79 | check.In(t, 4, []int{2, 3, 4, 5}) 80 | check.NotIn(t, "hello", []string{"goodbye", "world"}) 81 | 82 | var nilm map[string]string 83 | check.Nil(t, nilm) 84 | nilm = map[string]string{"hello": "world"} 85 | check.NotNil(t, nilm) 86 | } 87 | 88 | func TestAsserts(t *testing.T) { 89 | t.Parallel() 90 | assert.True(t, true) 91 | assert.False(t, false) 92 | assert.Equal(t, []string{"hello"}, []string{"hello"}) 93 | assert.NotEqual(t, 94 | map[string]int{"hello": 1}, 95 | map[string]int{"goodbye": 2}, 96 | ) 97 | assert.LessThan(t, 1, 4) 98 | assert.LessThanOrEqual(t, 4, 4) 99 | assert.GreaterThan(t, 8, 6) 100 | assert.GreaterThanOrEqual(t, 6, 6) 101 | assert.Error(t, fmt.Errorf("oh no")) 102 | assert.NoError(t, nil) 103 | assert.In(t, 4, []int{2, 3, 4, 5}) 104 | assert.NotIn(t, "hello", []string{"goodbye", "world"}) 105 | 106 | var nilm map[string]string 107 | assert.Nil(t, nilm) 108 | nilm = map[string]string{"hello": "world"} 109 | assert.NotNil(t, nilm) 110 | } 111 | ``` 112 | 113 | # Details 114 | 115 | ## `check` methods call `t.Fail` 116 | `check` contains methods for checking a condition, marking the test as failed 117 | but allowing it to continue running if the condition is not met. This is a 118 | "soft" style assert, equivalent to the methods in `testify/assert` or the 119 | `Check` method in `gotest.tools/assert`. If a check fails, testy calls 120 | `t.Fail`. 121 | 122 | Each `check` method returns a boolean, which is `true` if the check passed, and 123 | `false` otherwise. You can use this to conditionally run other logic in your 124 | code. 125 | 126 | ```go 127 | func TestExample(t *testing.T) { 128 | var f *MyFoo 129 | var err error 130 | f, err = ServiceThatGetsAFoo() 131 | // f is only meaningful if err == nil 132 | if check.NoError(t, err) { 133 | check.Equal(f.Name, "peter") 134 | } 135 | } 136 | ``` 137 | 138 | ## `assert` methods call `t.FailNow` 139 | `assert` contains methods for asserting a condition, marking the test as failed 140 | and immediately exiting the test if the condition is not met. This is a "hard" 141 | or traditional assert, equivalent to the methods in `testify/require` or the 142 | `Assert` method in `gotest.tools/assert`. If an assertion fails, testy calls 143 | `t.FailNow`. 144 | 145 | ```go 146 | func TestExample(t *testing.T) { 147 | var f *MyFoo 148 | var err error 149 | f, err = ServiceThatGetsAFoo() 150 | assert.NoError(t, err) // if err != nil, the test will end here 151 | assert.Equal(f.Name, "peter") 152 | } 153 | ``` 154 | 155 | ## Structuring Helpers 156 | The `assert` package also provides helpers for structuring your tests and making 157 | them more expressive. You can use these helpers to determine which checks are 158 | run in parallel, and which checks should halt test execution. 159 | 160 | - `assert.NoFailures(t)` and `assert.NoErrors(t)` will instantly fail the test 161 | if any previous `check` has failed, or any other code has called 162 | `t.Fail()`/`t.Error()` for any reason. 163 | - `assert.NoFailures(t, thunks...(func()))` will run each thunk function. After 164 | each thunk, it will check that no failures have occurred. If they have, the test 165 | immediately exits without running any of the following thunks. 166 | - `assert.NoErrors(t, thunks...(func() error))` will run each thunk function. If 167 | the thunk returns an error, or any check failure has occurred, the test 168 | immediately exits without running any of the following thunks. 169 | 170 | You can use these to stage your tests and make them more useful. 171 | 172 | ```go 173 | func TestStructuringHelpers(t *testing.T) { 174 | // Here's an easy way to "stage" your tests, where 175 | // - each stage runs all of the checks 176 | // - at the end, report any failures. 177 | // - if there were any failures, end the test. 178 | check.Equal(t, 2, 2) 179 | check.LessThanOrEqual(t, 2, 3) 180 | check.GreaterThan(t, 3, 1) 181 | assert.NoFailures(t) 182 | 183 | // This is another, equivalent, way to express the same logic 184 | assert.NoFailures(t, func() { 185 | check.Equal(t, 2, 2) 186 | check.LessThanOrEqual(t, 2, 3) 187 | check.GreaterThan(t, 3, 1) 188 | }) 189 | 190 | // You can also use the helpers to adopt a standard error-returning style 191 | assert.NoErrors(t, func() error { 192 | if _, err := myHelper(baz, bar); err != nil { 193 | return err 194 | } 195 | if _, err := anotherF(bar); err != nil { 196 | return err 197 | } 198 | return nil 199 | }) 200 | 201 | // This is equivalent to 202 | _, err := myHelper(baz, bar) 203 | assert.NoError(err) 204 | _, err = anotherF(bar) 205 | assert.NoError(err) 206 | } 207 | ``` 208 | 209 | ## More Examples 210 | 211 | Beyond the examples presented in this README, please read the code and its tests 212 | to see `testy` in action. 213 | 214 | # FAQ 215 | 216 | ## Why create this library? 217 | Each of the existing libraries (testify, gotest.tools, is) are phenomenal projects and Testy 218 | has been deeply influenced by each of them, and could not exist without them. While writing Testy, I used those libraries as reference, and I have great respect for their authors and contributors. 219 | 220 | That said, these libraries are (in my opinion) too limited in terms of expressiveness. Tests serve multiple purposes: 221 | - they prove the ground-level correctness of the system 222 | - they prove that a bug was fixed 223 | - therefore, they act as a historical record of all discovered bugs 224 | - they are documentation of the current implementation of the system and how to use its components 225 | - they are documentation of the business goals and invariants of the system 226 | - some of which are OK to change 227 | - some of which should never be changed without a big discussion 228 | - they are guard-rails to prevent accidental changes 229 | 230 | Testy is designed to make it easier to write tests for all of these different purposes. By making it easier to write tests, more tests get written. By giving the author more control over when to halt the test (checks vs. asserts, structuring helpers), debugging failing tests becomes easier. 231 | 232 | Explicitly, these other libraries have the following problems: 233 | 234 | - `testify` 235 | - Has a massive surface area, so many methods make it confusing to know which one to use 236 | - Not typesafe, and most likely never will be (v2 seems abandoned) 237 | - Uses `reflect.DeepEquals` instead of `go-cmp` 238 | - `gotest.tools` 239 | - Not typesafe (although may be soon?) 240 | - Makes `Equal` non-deep by default 241 | - Does not provide a soft/check version of `Equal` and `DeepEqual` 242 | - Magic implementation using ast-walking to determine comparison types is very cool but hard to understand 243 | - `is` 244 | - Missing common and useful methods like `NotEqual`. 245 | - Magic implementation using ast-walking to determine comment messages is very cool but hard to understand 246 | 247 | Only Testy is typesafe and able to perform both soft and hard style assertions, 248 | with a reasonable API surface area, with `go-cmp`-powered deep equality by 249 | default. 250 | 251 | ## Why not add more helper methods like testify? 252 | When I was working on a real-life, multi-year, multi-developer project, 253 | I regularly heard that testify was confusing because it wasn't clear which 254 | methods to use when writing a test. I did some grepping/analysis, and found that 255 | most tests were easily expressed using `Error/NoError`, `Equal/NotEqual`, 256 | `True/False`, `Nil/NotNil`, `Zero/NotZero`, `Empty/NotEmpty`. 257 | Testy handles all of these cases gracefully with a much reduced API surface area. 258 | Hopefully this means it is easier to learn and use. 259 | 260 | ## What should I do if I rely on testify methods that aren't present here? 261 | You have the following options: 262 | 263 | - Rewrite the test to not use those methods. This sounds scary but is often not 264 | that hard. 265 | - Write your own helper method for your specific check. Most codebases have 266 | their own test helpers anyway because it makes the tests more fluent to have 267 | domain-specific helpers. 268 | - File an issue or PR to add the helper to this library. I'm open to the idea of 269 | adding a few more methods. 270 | 271 | ## I wrote custom test helpers like you recommended, but now they show up in the testing output and ruin the stacktrace. How can I avoid this? 272 | In any testing helpers you create, just call `t.Helper()` to exclude them from the stacktrace. This is what Testy does (look at the code!) 273 | 274 | ```go 275 | func myTestHelper(t *testing.T, otherArgs ...any) { 276 | t.Helper() // <- excludes this function from the stacktraces reported during test failures 277 | // ... actual logic goes here 278 | } 279 | ``` 280 | 281 | ## Why use `go-cmp` instead of `reflect.DeepEquals`? 282 | There were a bunch of github issues about it being better, and in general it seems to give developers more control over how the comparison is implemented, including which fields to include/ignore. This seems to make a big difference particularly when comparing `time.Time` objects. For more information, see these discussions: 283 | 284 | - https://github.com/stretchr/testify/issues/535 285 | - https://github.com/matryer/is/issues/53 286 | 287 | ## How did you decide on "check" and "assert"? 288 | 289 | Most languages have an "assert" concept that halts program execution if the 290 | condition being asserted fails. So it makes sense to me to call them asserts. 291 | 292 | Not many languages have the ability to easily perform soft asserts. One of the nice 293 | things about Go is that it does, with the `t.Fail()`/`t.Error()` methods of its builtin `testing` library/framework/tool. There is no one name that everyone uses for this, but "check" makes sense to me and it's also used by `gotest.tools`. 294 | 295 | `testify`.... ugh! It actually did the most, in my opinion, to popularize the use of both soft and hard style asserts. But it calls the soft style "asserts" and the hard style "requires". Their minds! What were they thinking! 😤 296 | 297 | ## How can I contribute? 298 | 299 | Testy is a standard golang project, you'll need a working golang environment. 300 | If you're of the nix persuasion, this repo comes with a flakes-compatible 301 | development shell that you can enter with `nix develop` (flakes) or `nix-shell` 302 | (standard). 303 | 304 | If you use VSCode, the repo comes with suggested extensions and settings. 305 | 306 | Testing and linting scripts are defined with Just, see the Justfile to see how 307 | to run those commands manually. There are also Github Actions that will lint and test 308 | your PRs. 309 | 310 | Contributions are more than welcome! 311 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.0.6 2 | -------------------------------------------------------------------------------- /assert/assert.go: -------------------------------------------------------------------------------- 1 | package assert 2 | 3 | import ( 4 | "cmp" 5 | 6 | gocmp "github.com/google/go-cmp/cmp" 7 | 8 | "github.com/peterldowns/testy/check" 9 | "github.com/peterldowns/testy/common" 10 | ) 11 | 12 | // True passes if x == true, otherwise immediately failing the test. 13 | func True(t common.T, x bool) { 14 | t.Helper() 15 | if !check.True(t, x) { 16 | t.FailNow() 17 | } 18 | } 19 | 20 | // False passes if x == false, otherwise immediately failing the test. 21 | func False(t common.T, x bool) { 22 | t.Helper() 23 | if !check.False(t, x) { 24 | t.FailNow() 25 | } 26 | } 27 | 28 | // Equal passes if want == got, otherwise immediately failing the test. 29 | // 30 | // This is a typesafe check for inequality using go-cmp, allowing arguments only 31 | // of the same type to be compared. Most of the time, this is the 32 | // equality-checking method that you want. 33 | // 34 | // You can change the behavior of the equality checking using the go-cmp/cmp 35 | // Options system. For more information, see [the go-cmp documentation](https://pkg.go.dev/github.com/google/go-cmp/cmp#Equal). 36 | func Equal[Type any](t common.T, want Type, got Type, opts ...gocmp.Option) { 37 | t.Helper() 38 | if !check.Equal(t, want, got, opts...) { 39 | t.FailNow() 40 | } 41 | } 42 | 43 | // NotEqual passes if want != got, otherwise immediately failing the test. 44 | // 45 | // This is a typesafe check for inequality using go-cmp, allowing arguments only 46 | // of the same type to be compared. Most of the time, this is the 47 | // inequality-checking method that you want. 48 | // 49 | // You can change the behavior of the equality checking using the go-cmp/cmp 50 | // Options system. For more information, see [the go-cmp documentation](https://pkg.go.dev/github.com/google/go-cmp/cmp#Equal). 51 | func NotEqual[Type any](t common.T, want Type, got Type, opts ...gocmp.Option) { 52 | t.Helper() 53 | if !check.NotEqual(t, want, got, opts...) { 54 | t.FailNow() 55 | } 56 | } 57 | 58 | // LessThan passes if small < big, otherwise immediately failing the test. 59 | func LessThan[Type cmp.Ordered](t common.T, small Type, big Type) { 60 | t.Helper() 61 | if !check.LessThan(t, small, big) { 62 | t.FailNow() 63 | } 64 | } 65 | 66 | // LessThanOrEqual passes if small <= big, otherwise immediately failing the 67 | // test. 68 | func LessThanOrEqual[Type cmp.Ordered](t common.T, small Type, big Type) { 69 | t.Helper() 70 | if !check.LessThanOrEqual(t, small, big) { 71 | t.FailNow() 72 | } 73 | } 74 | 75 | // GreaterThan passes if big > small, otherwise immediately failing the test. 76 | func GreaterThan[Type cmp.Ordered](t common.T, big Type, small Type) { 77 | t.Helper() 78 | if !check.GreaterThan(t, big, small) { 79 | t.FailNow() 80 | } 81 | } 82 | 83 | // GreaterThanOrEqual passes if big >= small, otherwise immediately failing the 84 | // test. 85 | func GreaterThanOrEqual[Type cmp.Ordered](t common.T, big Type, small Type) { 86 | t.Helper() 87 | if !check.GreaterThanOrEqual(t, big, small) { 88 | t.FailNow() 89 | } 90 | } 91 | 92 | // Error passes if err != nil, otherwise immediately failing the test. 93 | func Error(t common.T, err error) { 94 | t.Helper() 95 | if !check.Error(t, err) { 96 | t.FailNow() 97 | } 98 | } 99 | 100 | // NoError passes if err == nil otherwise immediately failing the test. 101 | // 102 | // NoError is an alias for [Nil] 103 | func NoError(t common.T, err error) { 104 | t.Helper() 105 | if !check.NoError(t, err) { 106 | t.FailNow() 107 | } 108 | } 109 | 110 | // In passes if want is an element of slice, otherwise immediately failing the 111 | // test. 112 | func In[Type any](t common.T, want Type, slice []Type, opts ...gocmp.Option) { 113 | t.Helper() 114 | if !check.In(t, want, slice, opts...) { 115 | t.FailNow() 116 | } 117 | } 118 | 119 | // NotIn passes if want is an not element of slice, otherwise immediately 120 | // failing the test. 121 | func NotIn[Type any](t common.T, want Type, slice []Type, opts ...gocmp.Option) { 122 | t.Helper() 123 | if !check.NotIn(t, want, slice, opts...) { 124 | t.FailNow() 125 | } 126 | } 127 | 128 | // Nil passes if the val == nil, otherwise immediately failing the test. 129 | func Nil(t common.T, val any) { 130 | t.Helper() 131 | if !check.Nil(t, val) { 132 | t.FailNow() 133 | } 134 | } 135 | 136 | // NotNil passes if the val != nil, otherwise immediately failing the test. 137 | func NotNil(t common.T, val any) { 138 | t.Helper() 139 | if !check.NotNil(t, val) { 140 | t.FailNow() 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /assert/assert_test.go: -------------------------------------------------------------------------------- 1 | package assert_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | "unsafe" 8 | 9 | "github.com/google/go-cmp/cmp" 10 | 11 | "github.com/peterldowns/testy/assert" 12 | "github.com/peterldowns/testy/check" 13 | "github.com/peterldowns/testy/common" 14 | ) 15 | 16 | func TestTrue(t *testing.T) { 17 | t.Parallel() 18 | assert.True(t, true) 19 | 20 | mt := &common.MockT{} 21 | assert.True(mt, false) 22 | check.True(t, mt.Failed()) 23 | check.True(t, mt.FailedNow()) 24 | } 25 | 26 | func TestFalse(t *testing.T) { 27 | t.Parallel() 28 | assert.False(t, false) 29 | 30 | mt := &common.MockT{} 31 | assert.False(mt, true) 32 | check.True(t, mt.Failed()) 33 | check.True(t, mt.FailedNow()) 34 | } 35 | 36 | type person struct { 37 | Name string 38 | } 39 | 40 | type hiddenPerson struct { 41 | Name string 42 | // When using Equal / NotEqual, this unexported field will require a custom 43 | // cmp.Option. 44 | hidden bool 45 | } 46 | 47 | func TestEqual(t *testing.T) { 48 | t.Parallel() 49 | 50 | t.Run("ints", func(t *testing.T) { 51 | t.Parallel() 52 | assert.Equal(t, 1, 1) 53 | 54 | mt := &common.MockT{} 55 | assert.Equal(mt, 1, 2) 56 | check.True(t, mt.Failed()) 57 | check.True(t, mt.FailedNow()) 58 | }) 59 | t.Run("slices", func(t *testing.T) { 60 | t.Parallel() 61 | emptySlice := []string{} 62 | assert.Equal(t, emptySlice, emptySlice) 63 | 64 | mt := &common.MockT{} 65 | assert.Equal(mt, emptySlice, []string{"hello"}) 66 | check.True(t, mt.Failed()) 67 | check.True(t, mt.FailedNow()) 68 | }) 69 | t.Run("custom structs", func(t *testing.T) { 70 | t.Parallel() 71 | customStruct := person{Name: "peter"} 72 | assert.Equal(t, customStruct, customStruct) 73 | 74 | mt := &common.MockT{} 75 | assert.Equal(mt, customStruct, person{Name: "bob"}) 76 | check.True(t, mt.Failed()) 77 | check.True(t, mt.FailedNow()) 78 | }) 79 | 80 | t.Run("maps", func(t *testing.T) { 81 | t.Parallel() 82 | mapdata := map[string]int{ 83 | "hello": 1, 84 | "goodbye": 9, 85 | } 86 | assert.Equal(t, mapdata, mapdata) 87 | 88 | mt := &common.MockT{} 89 | assert.Equal(mt, mapdata, map[string]int{"hello": 1}) 90 | check.True(t, mt.Failed()) 91 | check.True(t, mt.FailedNow()) 92 | }) 93 | 94 | t.Run("hidden struct fields with cmp.opts", func(t *testing.T) { 95 | t.Parallel() 96 | // Equal works with types that have hidden fields, you just need to use a 97 | // cmp.Option. 98 | customStructWithHiddenField := hiddenPerson{Name: "Peter", hidden: true} 99 | assert.Equal( 100 | t, 101 | customStructWithHiddenField, 102 | customStructWithHiddenField, 103 | cmp.AllowUnexported(hiddenPerson{}), 104 | ) 105 | 106 | mt := &common.MockT{} 107 | assert.Equal( 108 | mt, 109 | hiddenPerson{Name: "Peter", hidden: true}, 110 | hiddenPerson{Name: "Peter", hidden: false}, 111 | cmp.AllowUnexported(hiddenPerson{}), 112 | ) 113 | check.True(t, mt.Failed()) 114 | check.True(t, mt.FailedNow()) 115 | }) 116 | 117 | t.Run("time.time structs with custom .equals", func(t *testing.T) { 118 | t.Parallel() 119 | // Shows that a custom .Equal method works -- a time.Time will be .Equal() 120 | // to a version of itself in a different timezone, even though the structs 121 | // contain strictly different values. 122 | timeStruct := time.Now() 123 | assert.Equal(t, timeStruct, timeStruct.UTC()) 124 | 125 | mt := &common.MockT{} 126 | assert.Equal( 127 | mt, 128 | timeStruct, 129 | timeStruct.Add(1*time.Hour), 130 | cmp.AllowUnexported(hiddenPerson{}), 131 | ) 132 | check.True(t, mt.Failed()) 133 | check.True(t, mt.FailedNow()) 134 | }) 135 | } 136 | 137 | func TestNotEqual(t *testing.T) { 138 | t.Parallel() 139 | 140 | t.Run("ints", func(t *testing.T) { 141 | t.Parallel() 142 | assert.NotEqual(t, 1, 2) 143 | 144 | mt := &common.MockT{} 145 | assert.NotEqual(mt, 1, 1) 146 | check.True(t, mt.Failed()) 147 | check.True(t, mt.FailedNow()) 148 | }) 149 | 150 | t.Run("slices", func(t *testing.T) { 151 | t.Parallel() 152 | assert.NotEqual(t, []string{}, []string{"hello"}) 153 | 154 | mt := &common.MockT{} 155 | emptySlice := []string{} 156 | assert.NotEqual(mt, emptySlice, emptySlice) 157 | check.True(t, mt.Failed()) 158 | check.True(t, mt.FailedNow()) 159 | }) 160 | 161 | t.Run("custom structs", func(t *testing.T) { 162 | t.Parallel() 163 | assert.NotEqual(t, person{Name: "peter"}, person{Name: "bob"}) 164 | 165 | mt := &common.MockT{} 166 | customStruct := person{Name: "peter"} 167 | assert.NotEqual(mt, customStruct, customStruct) 168 | check.True(t, mt.Failed()) 169 | check.True(t, mt.FailedNow()) 170 | }) 171 | 172 | t.Run("maps", func(t *testing.T) { 173 | t.Parallel() 174 | mapdata := map[string]int{ 175 | "hello": 1, 176 | "goodbye": 9, 177 | } 178 | assert.NotEqual(t, mapdata, map[string]int{"hello": 1}) 179 | 180 | mt := &common.MockT{} 181 | assert.NotEqual(mt, mapdata, mapdata) 182 | check.True(t, mt.Failed()) 183 | check.True(t, mt.FailedNow()) 184 | }) 185 | 186 | t.Run("hidden struct fields with cmp.opts", func(t *testing.T) { 187 | t.Parallel() 188 | // Equal works with types that have hidden fields, you just need to use a 189 | // cmp.Option. 190 | customStructWithHiddenField := hiddenPerson{Name: "Peter", hidden: true} 191 | assert.NotEqual( 192 | t, 193 | customStructWithHiddenField, 194 | hiddenPerson{Name: "Peter", hidden: false}, 195 | cmp.AllowUnexported(hiddenPerson{}), 196 | ) 197 | 198 | mt := &common.MockT{} 199 | assert.NotEqual( 200 | mt, 201 | customStructWithHiddenField, 202 | customStructWithHiddenField, 203 | cmp.AllowUnexported(hiddenPerson{}), 204 | ) 205 | check.True(t, mt.Failed()) 206 | check.True(t, mt.FailedNow()) 207 | }) 208 | 209 | t.Run("time.time structs with custom .equals", func(t *testing.T) { 210 | t.Parallel() 211 | // Shows that a custom .Equal method works -- a time.Time will be .Equal() 212 | // to a version of itself in a different timezone, even though the structs 213 | // contain strictly different values. 214 | timeStruct := time.Now() 215 | assert.NotEqual(t, timeStruct, timeStruct.Add(1*time.Hour)) 216 | 217 | mt := &common.MockT{} 218 | assert.NotEqual( 219 | mt, 220 | timeStruct, 221 | timeStruct.UTC(), 222 | cmp.AllowUnexported(hiddenPerson{}), 223 | ) 224 | check.True(t, mt.Failed()) 225 | check.True(t, mt.FailedNow()) 226 | }) 227 | } 228 | 229 | func TestLess(t *testing.T) { 230 | t.Parallel() 231 | t.Run("float", func(t *testing.T) { 232 | t.Parallel() 233 | assert.LessThan(t, 1.0, 2.0) 234 | assert.LessThanOrEqual(t, 1.0, 2.0) 235 | assert.LessThanOrEqual(t, 1.0, 1.0) 236 | 237 | mt := &common.MockT{} 238 | assert.LessThan(mt, 1.0, 1.0) 239 | check.True(t, mt.Failed()) 240 | check.True(t, mt.FailedNow()) 241 | 242 | mt = &common.MockT{} 243 | assert.LessThanOrEqual(mt, 2.0, 1.0) 244 | check.True(t, mt.Failed()) 245 | check.True(t, mt.FailedNow()) 246 | }) 247 | t.Run("int", func(t *testing.T) { 248 | t.Parallel() 249 | assert.LessThan(t, 1, 2) 250 | assert.LessThanOrEqual(t, 1, 2) 251 | assert.LessThanOrEqual(t, 1, 1) 252 | 253 | mt := &common.MockT{} 254 | assert.LessThan(mt, 1, 1) 255 | check.True(t, mt.Failed()) 256 | check.True(t, mt.FailedNow()) 257 | 258 | mt = &common.MockT{} 259 | assert.LessThanOrEqual(mt, 2, 1) 260 | check.True(t, mt.Failed()) 261 | check.True(t, mt.FailedNow()) 262 | }) 263 | t.Run("string", func(t *testing.T) { 264 | t.Parallel() 265 | assert.LessThan(t, "aaa", "bbb") 266 | assert.LessThanOrEqual(t, "aaa", "bbb") 267 | assert.LessThanOrEqual(t, "aaa", "aaa") 268 | 269 | mt := &common.MockT{} 270 | assert.LessThan(mt, "aaa", "aaa") 271 | check.True(t, mt.Failed()) 272 | check.True(t, mt.FailedNow()) 273 | 274 | mt = &common.MockT{} 275 | assert.LessThanOrEqual(mt, "bbb", "aaa") 276 | check.True(t, mt.Failed()) 277 | check.True(t, mt.FailedNow()) 278 | }) 279 | t.Run("rune", func(t *testing.T) { 280 | t.Parallel() 281 | assert.LessThan(t, 'a', 'b') 282 | assert.LessThanOrEqual(t, 'a', 'b') 283 | assert.LessThanOrEqual(t, 'a', 'a') 284 | 285 | mt := &common.MockT{} 286 | assert.LessThan(mt, 'a', 'a') 287 | check.True(t, mt.Failed()) 288 | check.True(t, mt.FailedNow()) 289 | 290 | mt = &common.MockT{} 291 | assert.LessThanOrEqual(mt, 'b', 'a') 292 | check.True(t, mt.Failed()) 293 | check.True(t, mt.FailedNow()) 294 | }) 295 | } 296 | 297 | func TestGreater(t *testing.T) { 298 | t.Parallel() 299 | t.Run("float", func(t *testing.T) { 300 | t.Parallel() 301 | assert.GreaterThan(t, 2.0, 1.0) 302 | assert.GreaterThanOrEqual(t, 2.0, 1.0) 303 | assert.GreaterThanOrEqual(t, 2.0, 2.0) 304 | 305 | mt := &common.MockT{} 306 | assert.GreaterThan(mt, 2.0, 2.0) 307 | check.True(t, mt.Failed()) 308 | check.True(t, mt.FailedNow()) 309 | 310 | mt = &common.MockT{} 311 | assert.GreaterThanOrEqual(mt, 1.0, 2.0) 312 | check.True(t, mt.Failed()) 313 | check.True(t, mt.FailedNow()) 314 | }) 315 | t.Run("int", func(t *testing.T) { 316 | t.Parallel() 317 | assert.GreaterThan(t, 2, 1) 318 | assert.GreaterThanOrEqual(t, 2, 1) 319 | assert.GreaterThanOrEqual(t, 1, 1) 320 | 321 | mt := &common.MockT{} 322 | assert.GreaterThan(mt, 2, 2) 323 | check.True(t, mt.Failed()) 324 | check.True(t, mt.FailedNow()) 325 | 326 | mt = &common.MockT{} 327 | assert.GreaterThanOrEqual(mt, 1, 2) 328 | check.True(t, mt.Failed()) 329 | check.True(t, mt.FailedNow()) 330 | }) 331 | t.Run("string", func(t *testing.T) { 332 | t.Parallel() 333 | assert.GreaterThan(t, "bbb", "aaa") 334 | assert.GreaterThanOrEqual(t, "bbb", "aaa") 335 | assert.GreaterThanOrEqual(t, "bbb", "bbb") 336 | 337 | mt := &common.MockT{} 338 | assert.GreaterThan(mt, "bbb", "bbb") 339 | check.True(t, mt.Failed()) 340 | check.True(t, mt.FailedNow()) 341 | 342 | mt = &common.MockT{} 343 | assert.GreaterThanOrEqual(mt, "aaa", "bbb") 344 | check.True(t, mt.Failed()) 345 | check.True(t, mt.FailedNow()) 346 | }) 347 | t.Run("rune", func(t *testing.T) { 348 | t.Parallel() 349 | assert.GreaterThan(t, 'b', 'a') 350 | assert.GreaterThanOrEqual(t, 'b', 'a') 351 | assert.GreaterThanOrEqual(t, 'b', 'b') 352 | 353 | mt := &common.MockT{} 354 | assert.GreaterThan(mt, 'b', 'b') 355 | check.True(t, mt.Failed()) 356 | check.True(t, mt.FailedNow()) 357 | 358 | mt = &common.MockT{} 359 | assert.GreaterThanOrEqual(mt, 'a', 'b') 360 | check.True(t, mt.Failed()) 361 | check.True(t, mt.FailedNow()) 362 | }) 363 | } 364 | 365 | func TestError(t *testing.T) { 366 | t.Parallel() 367 | t.Run("error", func(t *testing.T) { 368 | t.Parallel() 369 | assert.Error(t, fmt.Errorf("new error")) 370 | 371 | mt := &common.MockT{} 372 | assert.Error(mt, nil) 373 | check.True(t, mt.Failed()) 374 | check.True(t, mt.FailedNow()) 375 | }) 376 | t.Run("nil", func(t *testing.T) { 377 | t.Parallel() 378 | assert.Nil(t, nil) 379 | 380 | mt := &common.MockT{} 381 | assert.Nil(mt, fmt.Errorf("new error")) 382 | check.True(t, mt.Failed()) 383 | check.True(t, mt.FailedNow()) 384 | }) 385 | t.Run("noerror", func(t *testing.T) { 386 | t.Parallel() 387 | assert.NoError(t, nil) 388 | 389 | mt := &common.MockT{} 390 | assert.NoError(mt, fmt.Errorf("new error")) 391 | check.True(t, mt.Failed()) 392 | check.True(t, mt.FailedNow()) 393 | }) 394 | } 395 | 396 | func TestIn(t *testing.T) { 397 | t.Parallel() 398 | t.Run("int", func(t *testing.T) { 399 | t.Parallel() 400 | assert.In(t, 1, []int{1, 2, 3}) 401 | 402 | mt := &common.MockT{} 403 | assert.In(mt, 1, []int{4, 5, 6}) 404 | check.True(t, mt.Failed()) 405 | check.True(t, mt.FailedNow()) 406 | }) 407 | t.Run("nil equality", func(t *testing.T) { 408 | t.Parallel() 409 | assert.In(t, nil, []any{nil}) 410 | 411 | mt := &common.MockT{} 412 | assert.In(mt, nil, []any{}) 413 | check.True(t, mt.Failed()) 414 | check.True(t, mt.FailedNow()) 415 | }) 416 | t.Run("strings", func(t *testing.T) { 417 | t.Parallel() 418 | assert.In(t, "world", []string{"hello", "world"}) 419 | 420 | mt := &common.MockT{} 421 | assert.In(mt, "world", []string{"hello world"}) 422 | check.True(t, mt.Failed()) 423 | check.True(t, mt.FailedNow()) 424 | }) 425 | t.Run("hidden struct fields with cmp.opts", func(t *testing.T) { 426 | t.Parallel() 427 | // Equal works with types that have hidden fields, you just need to use a 428 | // cmp.Option. 429 | customStructWithHiddenField := hiddenPerson{Name: "Peter", hidden: true} 430 | assert.In( 431 | t, 432 | customStructWithHiddenField, 433 | []hiddenPerson{ 434 | customStructWithHiddenField, 435 | }, 436 | cmp.AllowUnexported(hiddenPerson{}), 437 | ) 438 | 439 | mt := &common.MockT{} 440 | assert.In( 441 | mt, 442 | hiddenPerson{Name: "Peter", hidden: true}, 443 | []hiddenPerson{ 444 | {Name: "Peter", hidden: false}, 445 | }, 446 | cmp.AllowUnexported(hiddenPerson{}), 447 | ) 448 | check.True(t, mt.Failed()) 449 | check.True(t, mt.FailedNow()) 450 | }) 451 | 452 | t.Run("time.time structs with custom .equals", func(t *testing.T) { 453 | t.Parallel() 454 | t1 := time.Now() 455 | t2 := t1.UTC() 456 | assert.In(t, &t1, []*time.Time{nil, &t2}) 457 | 458 | mt := &common.MockT{} 459 | assert.In( 460 | mt, 461 | t1, 462 | []time.Time{ 463 | t1.Add(1 * time.Hour), 464 | }, 465 | cmp.AllowUnexported(hiddenPerson{}), 466 | ) 467 | check.True(t, mt.Failed()) 468 | check.True(t, mt.FailedNow()) 469 | }) 470 | 471 | t.Run("empty slice", func(t *testing.T) { 472 | t.Parallel() 473 | mt := &common.MockT{} 474 | assert.In(mt, 1, []int{}) 475 | check.True(t, mt.Failed()) 476 | check.True(t, mt.FailedNow()) 477 | 478 | mt = &common.MockT{} 479 | assert.In(mt, 1, nil) 480 | check.True(t, mt.Failed()) 481 | check.True(t, mt.FailedNow()) 482 | }) 483 | } 484 | 485 | func TestNotIn(t *testing.T) { 486 | t.Parallel() 487 | t.Run("int", func(t *testing.T) { 488 | t.Parallel() 489 | assert.NotIn(t, 1, []int{4, 5, 6}) 490 | 491 | mt := &common.MockT{} 492 | assert.NotIn(mt, 1, []int{1, 2, 3}) 493 | check.True(t, mt.Failed()) 494 | check.True(t, mt.FailedNow()) 495 | }) 496 | t.Run("nil equality", func(t *testing.T) { 497 | t.Parallel() 498 | assert.NotIn(t, nil, []any{}) 499 | 500 | mt := &common.MockT{} 501 | assert.NotIn(mt, nil, []any{nil}) 502 | check.True(t, mt.Failed()) 503 | check.True(t, mt.FailedNow()) 504 | }) 505 | t.Run("strings", func(t *testing.T) { 506 | t.Parallel() 507 | assert.NotIn(t, "world", []string{"hello world"}) 508 | 509 | mt := &common.MockT{} 510 | assert.NotIn(mt, "world", []string{"hello", "world"}) 511 | check.True(t, mt.Failed()) 512 | check.True(t, mt.FailedNow()) 513 | }) 514 | t.Run("hidden struct fields with cmp.opts", func(t *testing.T) { 515 | t.Parallel() 516 | // Equal works with types that have hidden fields, you just need to use a 517 | // cmp.Option. 518 | customStructWithHiddenField := hiddenPerson{Name: "Peter", hidden: true} 519 | assert.NotIn( 520 | t, 521 | hiddenPerson{Name: "Peter", hidden: true}, 522 | []hiddenPerson{ 523 | {Name: "Peter", hidden: false}, 524 | }, 525 | cmp.AllowUnexported(hiddenPerson{}), 526 | ) 527 | 528 | mt := &common.MockT{} 529 | assert.NotIn( 530 | mt, 531 | customStructWithHiddenField, 532 | []hiddenPerson{ 533 | customStructWithHiddenField, 534 | }, 535 | cmp.AllowUnexported(hiddenPerson{}), 536 | ) 537 | check.True(t, mt.Failed()) 538 | check.True(t, mt.FailedNow()) 539 | }) 540 | 541 | t.Run("time.time structs with custom .equals", func(t *testing.T) { 542 | t.Parallel() 543 | t1 := time.Now() 544 | assert.NotIn( 545 | t, 546 | t1, 547 | []time.Time{ 548 | t1.Add(1 * time.Hour), 549 | }, 550 | ) 551 | 552 | mt := &common.MockT{} 553 | t2 := t1.UTC() 554 | assert.NotIn(mt, t1, []time.Time{t1, t2}) 555 | check.True(t, mt.Failed()) 556 | check.True(t, mt.FailedNow()) 557 | }) 558 | 559 | t.Run("empty slice", func(t *testing.T) { 560 | t.Parallel() 561 | assert.NotIn(t, 1, []int{}) 562 | assert.NotIn(t, 1, nil) 563 | }) 564 | } 565 | 566 | func TestNil(t *testing.T) { 567 | t.Parallel() 568 | 569 | t.Run("error", func(t *testing.T) { 570 | t.Parallel() 571 | var val error 572 | assert.True(t, val == nil) 573 | assert.Nil(t, val) 574 | 575 | val = fmt.Errorf("new error") 576 | assert.True(t, val != nil) 577 | 578 | mt := &common.MockT{} 579 | assert.Nil(mt, val) 580 | assert.True(t, mt.Failed()) 581 | assert.True(t, mt.FailedNow()) 582 | }) 583 | 584 | t.Run("chan", func(t *testing.T) { 585 | t.Parallel() 586 | var val chan int 587 | assert.True(t, val == nil) 588 | assert.Nil(t, val) 589 | 590 | val = make(chan int) 591 | assert.True(t, val != nil) 592 | 593 | mt := &common.MockT{} 594 | assert.Nil(mt, val) 595 | assert.True(t, mt.Failed()) 596 | assert.True(t, mt.FailedNow()) 597 | }) 598 | 599 | t.Run("func", func(t *testing.T) { 600 | t.Parallel() 601 | var val func() 602 | assert.True(t, val == nil) 603 | assert.Nil(t, val) 604 | 605 | val = func() {} 606 | assert.True(t, val != nil) 607 | 608 | mt := &common.MockT{} 609 | assert.Nil(mt, val) 610 | assert.True(t, mt.Failed()) 611 | assert.True(t, mt.FailedNow()) 612 | }) 613 | 614 | t.Run("interface", func(t *testing.T) { 615 | t.Parallel() 616 | var val any 617 | assert.True(t, val == nil) 618 | assert.Nil(t, val) 619 | 620 | val = any("hello") //nolint:staticcheck // intentional 621 | assert.True(t, val != nil) //nolint:staticcheck // intentional 622 | 623 | mt := &common.MockT{} 624 | assert.Nil(mt, val) 625 | assert.True(t, mt.Failed()) 626 | assert.True(t, mt.FailedNow()) 627 | }) 628 | 629 | t.Run("map", func(t *testing.T) { 630 | t.Parallel() 631 | var val map[string]int 632 | assert.True(t, val == nil) 633 | assert.Nil(t, val) 634 | 635 | val = map[string]int{} 636 | assert.True(t, val != nil) 637 | 638 | mt := &common.MockT{} 639 | assert.Nil(mt, val) 640 | assert.True(t, mt.Failed()) 641 | assert.True(t, mt.FailedNow()) 642 | }) 643 | 644 | t.Run("pointer", func(t *testing.T) { 645 | t.Parallel() 646 | var val *int 647 | assert.True(t, val == nil) 648 | assert.Nil(t, val) 649 | 650 | wrapped := 8 651 | val = &wrapped 652 | assert.True(t, val != nil) 653 | 654 | mt := &common.MockT{} 655 | assert.Nil(mt, val) 656 | assert.True(t, mt.Failed()) 657 | assert.True(t, mt.FailedNow()) 658 | }) 659 | 660 | t.Run("slice", func(t *testing.T) { 661 | t.Parallel() 662 | var val []int 663 | assert.True(t, val == nil) 664 | assert.Nil(t, val) 665 | 666 | // empty slice 667 | val = []int{} 668 | assert.True(t, val != nil) 669 | 670 | mt := &common.MockT{} 671 | assert.Nil(mt, val) 672 | assert.True(t, mt.Failed()) 673 | assert.True(t, mt.FailedNow()) 674 | 675 | // full slice 676 | val = []int{1, 2, 3, 4, 5} 677 | assert.True(t, val != nil) 678 | 679 | mt = &common.MockT{} 680 | assert.Nil(mt, val) 681 | assert.True(t, mt.Failed()) 682 | assert.True(t, mt.FailedNow()) 683 | }) 684 | 685 | t.Run("unsafe pointer", func(t *testing.T) { 686 | t.Parallel() 687 | var val unsafe.Pointer 688 | assert.True(t, val == nil) 689 | assert.Nil(t, val) 690 | 691 | wrapped := 8 692 | val = unsafe.Pointer(&wrapped) 693 | assert.True(t, val != nil) 694 | 695 | mt := &common.MockT{} 696 | assert.Nil(mt, val) 697 | assert.True(t, mt.Failed()) 698 | assert.True(t, mt.FailedNow()) 699 | }) 700 | } 701 | 702 | func TestNotNil(t *testing.T) { 703 | t.Parallel() 704 | 705 | t.Run("error", func(t *testing.T) { 706 | t.Parallel() 707 | val := fmt.Errorf("new error") 708 | assert.True(t, val != nil) 709 | assert.NotNil(t, val) 710 | 711 | mt := &common.MockT{} 712 | val = nil 713 | assert.NotNil(mt, val) 714 | assert.True(t, mt.Failed()) 715 | assert.True(t, mt.FailedNow()) 716 | }) 717 | 718 | t.Run("error", func(t *testing.T) { 719 | t.Parallel() 720 | val := make(chan int) 721 | assert.True(t, val != nil) 722 | assert.NotNil(t, val) 723 | 724 | mt := &common.MockT{} 725 | val = nil 726 | assert.NotNil(mt, val) 727 | assert.True(t, mt.Failed()) 728 | assert.True(t, mt.FailedNow()) 729 | }) 730 | 731 | t.Run("func", func(t *testing.T) { 732 | t.Parallel() 733 | val := func() {} 734 | assert.True(t, val != nil) 735 | assert.NotNil(t, val) 736 | 737 | mt := &common.MockT{} 738 | val = nil 739 | assert.NotNil(mt, val) 740 | assert.True(t, mt.Failed()) 741 | assert.True(t, mt.FailedNow()) 742 | }) 743 | 744 | t.Run("interface", func(t *testing.T) { 745 | t.Parallel() 746 | val := any("hello") //nolint:staticcheck // intentional 747 | assert.True(t, val != nil) //nolint:staticcheck // intentional 748 | assert.NotNil(t, val) 749 | 750 | mt := &common.MockT{} 751 | val = nil 752 | assert.NotNil(mt, val) 753 | assert.True(t, mt.Failed()) 754 | assert.True(t, mt.FailedNow()) 755 | }) 756 | 757 | t.Run("map", func(t *testing.T) { 758 | t.Parallel() 759 | val := map[string]int{} 760 | assert.True(t, val != nil) 761 | assert.NotNil(t, val) 762 | 763 | mt := &common.MockT{} 764 | val = nil 765 | assert.NotNil(mt, val) 766 | assert.True(t, mt.Failed()) 767 | assert.True(t, mt.FailedNow()) 768 | }) 769 | 770 | t.Run("pointer", func(t *testing.T) { 771 | t.Parallel() 772 | wrapped := 8 773 | val := &wrapped 774 | assert.True(t, val != nil) 775 | assert.NotNil(t, val) 776 | 777 | mt := &common.MockT{} 778 | val = nil 779 | assert.NotNil(mt, val) 780 | assert.True(t, mt.Failed()) 781 | assert.True(t, mt.FailedNow()) 782 | }) 783 | 784 | t.Run("slice", func(t *testing.T) { 785 | t.Parallel() 786 | // empty slice 787 | val := []int{} 788 | assert.True(t, val != nil) 789 | assert.NotNil(t, val) 790 | 791 | // full slice 792 | val = []int{1, 2, 3, 4, 5} 793 | assert.True(t, val != nil) 794 | assert.NotNil(t, val) 795 | 796 | // nil 797 | val = nil 798 | mt := &common.MockT{} 799 | assert.NotNil(mt, val) 800 | assert.True(t, mt.Failed()) 801 | assert.True(t, mt.FailedNow()) 802 | }) 803 | 804 | t.Run("unsafe pointer", func(t *testing.T) { 805 | t.Parallel() 806 | wrapped := 8 807 | val := unsafe.Pointer(&wrapped) 808 | assert.True(t, val != nil) 809 | assert.NotNil(t, val) 810 | 811 | val = nil 812 | mt := &common.MockT{} 813 | assert.NotNil(mt, val) 814 | assert.True(t, mt.Failed()) 815 | assert.True(t, mt.FailedNow()) 816 | }) 817 | } 818 | -------------------------------------------------------------------------------- /assert/nofailures.go: -------------------------------------------------------------------------------- 1 | package assert 2 | 3 | import ( 4 | "github.com/peterldowns/testy/check" 5 | "github.com/peterldowns/testy/common" 6 | ) 7 | 8 | // NoFailures will exit the test immediately if any checks have previously 9 | // failed. This includes any calls to `t.Fail` or `t.Error`, not just those from 10 | // Testy. 11 | func NoFailures(t common.T, thunks ...(func())) { 12 | t.Helper() 13 | for _, thunk := range thunks { 14 | thunk() 15 | if failNowIfFailed(t) { 16 | break 17 | } 18 | } 19 | failNowIfFailed(t) 20 | } 21 | 22 | func NoErrors(t common.T, thunks ...(func() error)) { 23 | t.Helper() 24 | for _, thunk := range thunks { 25 | check.Nil(t, thunk()) 26 | if failNowIfFailed(t) { 27 | break 28 | } 29 | } 30 | failNowIfFailed(t) 31 | } 32 | 33 | // failNowIfFailed immediately stops test execution if it has failed at any 34 | // point. Returns a boolean for the purposes of testing that this framework is 35 | // working correrctly; when we call `t.FailNow()` on a `*testing.T` the code is 36 | // guaranteed to stop executing at that point. 37 | func failNowIfFailed(t common.T) bool { 38 | t.Helper() 39 | if t.Failed() { 40 | t.FailNow() 41 | return true // normally unreachable, used in testing 42 | } 43 | return false 44 | } 45 | -------------------------------------------------------------------------------- /assert/nofailures_test.go: -------------------------------------------------------------------------------- 1 | package assert_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/peterldowns/testy/assert" 8 | "github.com/peterldowns/testy/check" 9 | "github.com/peterldowns/testy/common" 10 | ) 11 | 12 | func dummyAdd(a, b int) (int, error) { 13 | if a == 0 || b == 0 { 14 | return 0, fmt.Errorf("cannot calculate with zero") 15 | } 16 | return a + b, nil 17 | } 18 | 19 | func TestNoFailuresPasses(t *testing.T) { 20 | t.Parallel() 21 | // NoFailures() should pass on a brand new *testing.T 22 | assert.NoFailures(t) 23 | 24 | // NoFailures() should pass on a test that has only had successful assertions 25 | check.True(t, 2 == 1+1) 26 | check.False(t, 2 == 1-9) 27 | assert.NoFailures(t) 28 | } 29 | 30 | func TestNoErrorsPasses(t *testing.T) { 31 | t.Parallel() 32 | // NoErrors() should pass on a brand new *testing.T 33 | assert.NoErrors(t) 34 | 35 | // NoErrors() should pass on a test that has only had successful assertions 36 | check.True(t, 2 == 1+1) 37 | check.False(t, 2 == 1-9) 38 | assert.NoErrors(t) 39 | } 40 | 41 | func TestNoFailuresDetectsNonCheckFailures(t *testing.T) { 42 | t.Parallel() 43 | // NoFailures() should call FailNow() even if the failure 44 | // on the test was reported by a different framework. 45 | mt := &common.MockT{} 46 | mt.Error("something went wrong") 47 | assert.NoFailures(mt) 48 | check.True(t, mt.FailedNow()) 49 | } 50 | 51 | func TestNoErrorsDetectsNonCheckFailures(t *testing.T) { 52 | t.Parallel() 53 | // NoErrors() should call FailNow() even if the failure 54 | // on the test was reported by a different framework. 55 | mt := &common.MockT{} 56 | mt.Error("something went wrong") 57 | assert.NoErrors(mt) 58 | check.True(t, mt.FailedNow()) 59 | } 60 | 61 | func TestNoFailuresCallsFailedNow(t *testing.T) { 62 | t.Parallel() 63 | // Initially, the test hasn't failed, so NoFailures() doesn't call FailNow() 64 | mt := &common.MockT{} 65 | check.False(t, mt.Failed()) 66 | assert.NoFailures(mt) 67 | check.False(t, mt.FailedNow()) 68 | 69 | // Now cause the test to fail. 70 | check.True(mt, false) 71 | check.True(t, mt.Failed()) 72 | check.False(t, mt.FailedNow()) 73 | 74 | // NoFailures() should have called FailNow() 75 | assert.NoFailures(mt) 76 | check.True(t, mt.Failed()) 77 | check.True(t, mt.FailedNow()) 78 | } 79 | 80 | func TestNoErrorsCallsFailedNow(t *testing.T) { 81 | t.Parallel() 82 | // Initially, the test hasn't failed, so NoErrors() doesn't call FailNow() 83 | mt := &common.MockT{} 84 | check.False(t, mt.Failed()) 85 | assert.NoErrors(mt) 86 | check.False(t, mt.FailedNow()) 87 | 88 | // Now cause the test to fail. 89 | check.True(mt, false) 90 | check.True(t, mt.Failed()) 91 | check.False(t, mt.FailedNow()) 92 | 93 | // NoErrors() should have called FailNow() 94 | assert.NoErrors(mt) 95 | check.True(t, mt.Failed()) 96 | check.True(t, mt.FailedNow()) 97 | } 98 | 99 | func TestNoFailuresCallsThunks(t *testing.T) { 100 | t.Parallel() 101 | t.Run("no failures", func(t *testing.T) { 102 | t.Parallel() 103 | calledFirst := false 104 | calledSecond := false 105 | assert.NoFailures(t, func() { 106 | _, err := dummyAdd(1, 2) 107 | check.Nil(t, err) 108 | res, err := dummyAdd(-1, 1) 109 | check.Nil(t, err) 110 | check.True(t, 0 == res) 111 | calledFirst = true 112 | }, func() { 113 | calledSecond = true 114 | }) 115 | check.True(t, calledFirst) 116 | check.True(t, calledSecond) 117 | }) 118 | t.Run("failure interrupts", func(t *testing.T) { 119 | t.Parallel() 120 | 121 | calledFirst := false 122 | calledSecond := false 123 | mt := &common.MockT{} 124 | assert.NoFailures(mt, func() { 125 | check.True(mt, false) // intentional failure 126 | calledFirst = true 127 | }, func() { 128 | // Never reached because of the check failure in the first thunk 129 | calledSecond = true 130 | }) 131 | check.True(t, calledFirst) 132 | check.False(t, calledSecond) 133 | }) 134 | } 135 | 136 | func TestNoErrorsCallsThunks(t *testing.T) { 137 | t.Parallel() 138 | t.Run("no failures", func(t *testing.T) { 139 | t.Parallel() 140 | calledFirst := false 141 | calledSecond := false 142 | assert.NoErrors(t, func() error { 143 | if _, err := dummyAdd(1, 2); err != nil { 144 | return err 145 | } 146 | res, err := dummyAdd(-1, 1) 147 | if err != nil { 148 | return err 149 | } 150 | check.True(t, 0 == res) 151 | 152 | calledFirst = true 153 | return nil 154 | }, func() error { 155 | calledSecond = true 156 | return nil 157 | }) 158 | check.True(t, calledFirst) 159 | check.True(t, calledSecond) 160 | }) 161 | t.Run("failure interrupts", func(t *testing.T) { 162 | t.Parallel() 163 | 164 | calledFirst := false 165 | calledSecond := false 166 | mt := &common.MockT{} 167 | assert.NoErrors(mt, func() error { 168 | calledFirst = true 169 | return fmt.Errorf("intentional failure") 170 | }, func() error { 171 | // Never reached because of the check failure in the first thunk 172 | calledSecond = true 173 | return nil 174 | }) 175 | check.True(t, calledFirst) 176 | check.False(t, calledSecond) 177 | }) 178 | } 179 | 180 | func TestNoErrorsNestsJustFine(t *testing.T) { 181 | t.Parallel() 182 | // NoErrors() allows nesting, so you can stage your tests as you'd like 183 | // if it helps you make the test more readable. 184 | assert.NoErrors(t) 185 | assert.NoErrors(t, func() error { 186 | check.True(t, true) 187 | check.False(t, false) 188 | assert.NoErrors(t) 189 | 190 | assert.NoErrors(t, func() error { 191 | check.Nil(t, nil) 192 | return nil 193 | }) 194 | 195 | return nil 196 | }) 197 | } 198 | -------------------------------------------------------------------------------- /check/check.go: -------------------------------------------------------------------------------- 1 | package check 2 | 3 | import ( 4 | "cmp" 5 | "fmt" 6 | "reflect" 7 | 8 | gocmp "github.com/google/go-cmp/cmp" 9 | 10 | "github.com/peterldowns/testy/common" 11 | ) 12 | 13 | // True returns true if x == true, otherwise marks the test as failed and returns false. 14 | func True(t common.T, x bool) bool { 15 | t.Helper() 16 | if x { 17 | return true 18 | } 19 | t.Error("expected true") 20 | return false 21 | } 22 | 23 | // False returns true if x == false, otherwise marks the test as failed and returns false. 24 | func False(t common.T, x bool) bool { 25 | t.Helper() 26 | if !x { 27 | return true 28 | } 29 | t.Error("expected false") 30 | return false 31 | } 32 | 33 | // Equal returns true if want == got, otherwise marks the test as failed and returns false. 34 | // 35 | // This is a typesafe check for inequality using go-cmp, allowing arguments only 36 | // of the same type to be compared. Most of the time, this is the 37 | // equality-checking method that you want. 38 | // 39 | // You can change the behavior of the equality checking using the go-cmp/cmp 40 | // Options system. For more information, see [the go-cmp documentation](https://pkg.go.dev/github.com/google/go-cmp/cmp#Equal). 41 | func Equal[Type any](t common.T, want Type, got Type, opts ...gocmp.Option) bool { 42 | t.Helper() 43 | diff := gocmp.Diff(want, got, opts...) 44 | if diff == "" { 45 | return true 46 | } 47 | t.Error(fmt.Sprintf("expected want == got\n--- want\n+++ got\n%s", diff)) 48 | return false 49 | } 50 | 51 | // NotEqual returns true if want != got, otherwise marks the test as failed and returns false. 52 | // 53 | // This is a typesafe check for inequality using go-cmp, allowing arguments only 54 | // of the same type to be compared. Most of the time, this is the 55 | // inequality-checking method that you want. 56 | // 57 | // You can change the behavior of the equality checking using the go-cmp/cmp 58 | // Options system. For more information, see [the go-cmp documentation](https://pkg.go.dev/github.com/google/go-cmp/cmp#Equal). 59 | func NotEqual[Type any](t common.T, want Type, got Type, opts ...gocmp.Option) bool { 60 | t.Helper() 61 | if !gocmp.Equal(want, got, opts...) { 62 | return true 63 | } 64 | t.Error(fmt.Sprintf("expected want != got\nwant: %+v\n got: %+v", want, got)) 65 | return false 66 | } 67 | 68 | // LessThan returns true if small < big, otherwise marks the test as failed and returns false. 69 | func LessThan[Type cmp.Ordered](t common.T, small Type, big Type) bool { 70 | t.Helper() 71 | if small < big { 72 | return true 73 | } 74 | t.Error(fmt.Sprintf("expected %v < %v", small, big)) 75 | return false 76 | } 77 | 78 | // LessThanOrEqual returns true if small <= big, otherwise marks the test as failed and returns false. 79 | func LessThanOrEqual[Type cmp.Ordered](t common.T, small Type, big Type) bool { 80 | t.Helper() 81 | if small <= big { 82 | return true 83 | } 84 | t.Error(fmt.Sprintf("expected %v <= %v", small, big)) 85 | return false 86 | } 87 | 88 | // GreaterThan returns true if big > small, otherwise marks the test as failed and returns false. 89 | func GreaterThan[Type cmp.Ordered](t common.T, big Type, small Type) bool { 90 | t.Helper() 91 | if big > small { 92 | return true 93 | } 94 | t.Error(fmt.Sprintf("expected %v > %v", big, small)) 95 | return false 96 | } 97 | 98 | // GreaterThanOrEqual returns true if big >= small, otherwise marks the test as failed and returns false. 99 | func GreaterThanOrEqual[Type cmp.Ordered](t common.T, big Type, small Type) bool { 100 | t.Helper() 101 | if big >= small { 102 | return true 103 | } 104 | t.Error(fmt.Sprintf("expected %v >= %v", big, small)) 105 | return false 106 | } 107 | 108 | // Error returns true if err != nil, otherwise marks the test as failed and returns false. 109 | func Error(t common.T, err error) bool { 110 | t.Helper() 111 | if err != nil { 112 | return true 113 | } 114 | t.Error("expected error, received ") 115 | return false 116 | } 117 | 118 | // NoError returned true if err == nil, otherwise marks the test as failed and returns false. 119 | // 120 | // NoError is an alias for [Nil] 121 | func NoError(t common.T, err error) bool { 122 | t.Helper() 123 | if err == nil { 124 | return true 125 | } 126 | t.Error("expected error, received %v", err) 127 | return false 128 | } 129 | 130 | // In returns true if element is in slice, otherwise marks the test as failed 131 | // and returns false. 132 | func In[Type any](t common.T, element Type, slice []Type, opts ...gocmp.Option) bool { 133 | t.Helper() 134 | for _, value := range slice { 135 | if gocmp.Equal(element, value, opts...) { 136 | return true 137 | } 138 | } 139 | t.Error(fmt.Sprintf("expected slice to contain element:\nelement: %+v\n", element)) 140 | return false 141 | } 142 | 143 | // NotIn returns true if element is not in slice, otherwise marks the test as 144 | // failed and returns false. 145 | func NotIn[Type any](t common.T, element Type, slice []Type, opts ...gocmp.Option) bool { 146 | t.Helper() 147 | for _, value := range slice { 148 | if gocmp.Equal(element, value, opts...) { 149 | t.Error(fmt.Sprintf("expected slice to not contain element\nelement: %+v\n found: %+v", element, value)) 150 | return false 151 | } 152 | } 153 | return true 154 | } 155 | 156 | // Nil returns true if the value == nil, otherwise marks the test as failed and returns false. 157 | // 158 | // Uses reflection because Go doesn't have a type constraint for "nilable". 159 | // Can return false for the following types: 160 | // 161 | // - error 162 | // - pointer 163 | // - interface 164 | // - map 165 | // - slice 166 | // - channel 167 | // - function 168 | // - unsafe.Pointer 169 | func Nil(t common.T, v any) bool { 170 | t.Helper() 171 | if isNil(v) { 172 | return true 173 | } 174 | t.Error(fmt.Sprintf("expected , received %v", v)) 175 | return false 176 | } 177 | 178 | // NotNil returns true if the value != nil, otherwise marks the test as failed and returns false. 179 | // 180 | // Uses reflection because Go doesn't have a type constraint for "nilable". 181 | // Can return false for the following types: 182 | // 183 | // - error 184 | // - pointer 185 | // - interface 186 | // - map 187 | // - slice 188 | // - channel 189 | // - function 190 | // - unsafe.Pointer 191 | func NotNil(t common.T, v any) bool { 192 | t.Helper() 193 | if !isNil(v) { 194 | return true 195 | } 196 | t.Error("expected non- value, received : %v", v) 197 | return false 198 | } 199 | 200 | // reflection-based implementation 201 | func isNil(object any) bool { 202 | if object == nil { 203 | return true 204 | } 205 | 206 | value := reflect.ValueOf(object) 207 | switch value.Kind() { 208 | case 209 | reflect.Chan, 210 | reflect.Func, 211 | reflect.Interface, 212 | reflect.Map, 213 | reflect.Ptr, 214 | reflect.Slice, 215 | reflect.UnsafePointer: 216 | return value.IsNil() 217 | default: 218 | return false 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /check/check_test.go: -------------------------------------------------------------------------------- 1 | package check_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | "unsafe" 8 | 9 | "github.com/google/go-cmp/cmp" 10 | 11 | "github.com/peterldowns/testy/check" 12 | "github.com/peterldowns/testy/common" 13 | ) 14 | 15 | func TestTrue(t *testing.T) { 16 | t.Parallel() 17 | check.True(t, true) 18 | 19 | mt := &common.MockT{} 20 | check.True(mt, false) 21 | check.True(t, mt.Failed()) 22 | check.False(t, mt.FailedNow()) 23 | } 24 | 25 | func TestFalse(t *testing.T) { 26 | t.Parallel() 27 | check.False(t, false) 28 | 29 | mt := &common.MockT{} 30 | check.False(mt, true) 31 | check.True(t, mt.Failed()) 32 | check.False(t, mt.FailedNow()) 33 | } 34 | 35 | type person struct { 36 | Name string 37 | } 38 | 39 | type hiddenPerson struct { 40 | Name string 41 | // When using Equal / NotEqual, this unexported field will require a custom 42 | // cmp.Option. 43 | hidden bool 44 | } 45 | 46 | func TestEqual(t *testing.T) { 47 | t.Parallel() 48 | 49 | t.Run("ints", func(t *testing.T) { 50 | t.Parallel() 51 | check.Equal(t, 1, 1) 52 | 53 | mt := &common.MockT{} 54 | check.Equal(mt, 1, 2) 55 | check.True(t, mt.Failed()) 56 | check.False(t, mt.FailedNow()) 57 | }) 58 | t.Run("slices", func(t *testing.T) { 59 | t.Parallel() 60 | emptySlice := []string{} 61 | check.Equal(t, emptySlice, emptySlice) 62 | 63 | mt := &common.MockT{} 64 | check.Equal(mt, emptySlice, []string{"hello"}) 65 | check.True(t, mt.Failed()) 66 | check.False(t, mt.FailedNow()) 67 | }) 68 | t.Run("custom structs", func(t *testing.T) { 69 | t.Parallel() 70 | customStruct := person{Name: "peter"} 71 | check.Equal(t, customStruct, customStruct) 72 | 73 | mt := &common.MockT{} 74 | check.Equal(mt, customStruct, person{Name: "bob"}) 75 | check.True(t, mt.Failed()) 76 | check.False(t, mt.FailedNow()) 77 | }) 78 | 79 | t.Run("maps", func(t *testing.T) { 80 | t.Parallel() 81 | mapdata := map[string]int{ 82 | "hello": 1, 83 | "goodbye": 9, 84 | } 85 | check.Equal(t, mapdata, mapdata) 86 | 87 | mt := &common.MockT{} 88 | check.Equal(mt, mapdata, map[string]int{"hello": 1}) 89 | check.True(t, mt.Failed()) 90 | check.False(t, mt.FailedNow()) 91 | }) 92 | 93 | t.Run("hidden struct fields with cmp.opts", func(t *testing.T) { 94 | t.Parallel() 95 | // Equal works with types that have hidden fields, you just need to use a 96 | // cmp.Option. 97 | customStructWithHiddenField := hiddenPerson{Name: "Peter", hidden: true} 98 | check.Equal( 99 | t, 100 | customStructWithHiddenField, 101 | customStructWithHiddenField, 102 | cmp.AllowUnexported(hiddenPerson{}), 103 | ) 104 | 105 | mt := &common.MockT{} 106 | check.Equal( 107 | mt, 108 | hiddenPerson{Name: "Peter", hidden: true}, 109 | hiddenPerson{Name: "Peter", hidden: false}, 110 | cmp.AllowUnexported(hiddenPerson{}), 111 | ) 112 | check.True(t, mt.Failed()) 113 | check.False(t, mt.FailedNow()) 114 | }) 115 | 116 | t.Run("time.time structs with custom .equals", func(t *testing.T) { 117 | t.Parallel() 118 | // Shows that a custom .Equal method works -- a time.Time will be .Equal() 119 | // to a version of itself in a different timezone, even though the structs 120 | // contain strictly different values. 121 | timeStruct := time.Now() 122 | check.Equal(t, timeStruct, timeStruct.UTC()) 123 | 124 | mt := &common.MockT{} 125 | check.Equal( 126 | mt, 127 | timeStruct, 128 | timeStruct.Add(1*time.Hour), 129 | cmp.AllowUnexported(hiddenPerson{}), 130 | ) 131 | check.True(t, mt.Failed()) 132 | check.False(t, mt.FailedNow()) 133 | }) 134 | } 135 | 136 | func TestNotEqual(t *testing.T) { 137 | t.Parallel() 138 | 139 | t.Run("ints", func(t *testing.T) { 140 | t.Parallel() 141 | check.NotEqual(t, 1, 2) 142 | 143 | mt := &common.MockT{} 144 | check.NotEqual(mt, 1, 1) 145 | check.True(t, mt.Failed()) 146 | check.False(t, mt.FailedNow()) 147 | }) 148 | 149 | t.Run("slices", func(t *testing.T) { 150 | t.Parallel() 151 | check.NotEqual(t, []string{}, []string{"hello"}) 152 | 153 | mt := &common.MockT{} 154 | emptySlice := []string{} 155 | check.NotEqual(mt, emptySlice, emptySlice) 156 | check.True(t, mt.Failed()) 157 | check.False(t, mt.FailedNow()) 158 | }) 159 | 160 | t.Run("custom structs", func(t *testing.T) { 161 | t.Parallel() 162 | check.NotEqual(t, person{Name: "peter"}, person{Name: "bob"}) 163 | 164 | mt := &common.MockT{} 165 | customStruct := person{Name: "peter"} 166 | check.NotEqual(mt, customStruct, customStruct) 167 | check.True(t, mt.Failed()) 168 | check.False(t, mt.FailedNow()) 169 | }) 170 | 171 | t.Run("maps", func(t *testing.T) { 172 | t.Parallel() 173 | mapdata := map[string]int{ 174 | "hello": 1, 175 | "goodbye": 9, 176 | } 177 | check.NotEqual(t, mapdata, map[string]int{"hello": 1}) 178 | 179 | mt := &common.MockT{} 180 | check.NotEqual(mt, mapdata, mapdata) 181 | check.True(t, mt.Failed()) 182 | check.False(t, mt.FailedNow()) 183 | }) 184 | 185 | t.Run("hidden struct fields with cmp.opts", func(t *testing.T) { 186 | t.Parallel() 187 | // Equal works with types that have hidden fields, you just need to use a 188 | // cmp.Option. 189 | customStructWithHiddenField := hiddenPerson{Name: "Peter", hidden: true} 190 | check.NotEqual( 191 | t, 192 | customStructWithHiddenField, 193 | hiddenPerson{Name: "Peter", hidden: false}, 194 | cmp.AllowUnexported(hiddenPerson{}), 195 | ) 196 | 197 | mt := &common.MockT{} 198 | check.NotEqual( 199 | mt, 200 | customStructWithHiddenField, 201 | customStructWithHiddenField, 202 | cmp.AllowUnexported(hiddenPerson{}), 203 | ) 204 | check.True(t, mt.Failed()) 205 | check.False(t, mt.FailedNow()) 206 | }) 207 | 208 | t.Run("time.time structs with custom .equals", func(t *testing.T) { 209 | t.Parallel() 210 | // Shows that a custom .Equal method works -- a time.Time will be .Equal() 211 | // to a version of itself in a different timezone, even though the structs 212 | // contain strictly different values. 213 | timeStruct := time.Now() 214 | check.NotEqual(t, timeStruct, timeStruct.Add(1*time.Hour)) 215 | 216 | mt := &common.MockT{} 217 | check.NotEqual( 218 | mt, 219 | timeStruct, 220 | timeStruct.UTC(), 221 | cmp.AllowUnexported(hiddenPerson{}), 222 | ) 223 | check.True(t, mt.Failed()) 224 | check.False(t, mt.FailedNow()) 225 | }) 226 | } 227 | 228 | func TestLess(t *testing.T) { 229 | t.Parallel() 230 | t.Run("float", func(t *testing.T) { 231 | t.Parallel() 232 | check.LessThan(t, 1.0, 2.0) 233 | check.LessThanOrEqual(t, 1.0, 2.0) 234 | check.LessThanOrEqual(t, 1.0, 1.0) 235 | 236 | mt := &common.MockT{} 237 | check.LessThan(mt, 1.0, 1.0) 238 | check.True(t, mt.Failed()) 239 | check.False(t, mt.FailedNow()) 240 | 241 | mt = &common.MockT{} 242 | check.LessThanOrEqual(mt, 2.0, 1.0) 243 | check.True(t, mt.Failed()) 244 | check.False(t, mt.FailedNow()) 245 | }) 246 | t.Run("int", func(t *testing.T) { 247 | t.Parallel() 248 | check.LessThan(t, 1, 2) 249 | check.LessThanOrEqual(t, 1, 2) 250 | check.LessThanOrEqual(t, 1, 1) 251 | 252 | mt := &common.MockT{} 253 | check.LessThan(mt, 1, 1) 254 | check.True(t, mt.Failed()) 255 | check.False(t, mt.FailedNow()) 256 | 257 | mt = &common.MockT{} 258 | check.LessThanOrEqual(mt, 2, 1) 259 | check.True(t, mt.Failed()) 260 | check.False(t, mt.FailedNow()) 261 | }) 262 | t.Run("string", func(t *testing.T) { 263 | t.Parallel() 264 | check.LessThan(t, "aaa", "bbb") 265 | check.LessThanOrEqual(t, "aaa", "bbb") 266 | check.LessThanOrEqual(t, "aaa", "aaa") 267 | 268 | mt := &common.MockT{} 269 | check.LessThan(mt, "aaa", "aaa") 270 | check.True(t, mt.Failed()) 271 | check.False(t, mt.FailedNow()) 272 | 273 | mt = &common.MockT{} 274 | check.LessThanOrEqual(mt, "bbb", "aaa") 275 | check.True(t, mt.Failed()) 276 | check.False(t, mt.FailedNow()) 277 | }) 278 | t.Run("rune", func(t *testing.T) { 279 | t.Parallel() 280 | check.LessThan(t, 'a', 'b') 281 | check.LessThanOrEqual(t, 'a', 'b') 282 | check.LessThanOrEqual(t, 'a', 'a') 283 | 284 | mt := &common.MockT{} 285 | check.LessThan(mt, 'a', 'a') 286 | check.True(t, mt.Failed()) 287 | check.False(t, mt.FailedNow()) 288 | 289 | mt = &common.MockT{} 290 | check.LessThanOrEqual(mt, 'b', 'a') 291 | check.True(t, mt.Failed()) 292 | check.False(t, mt.FailedNow()) 293 | }) 294 | } 295 | 296 | func TestGreater(t *testing.T) { 297 | t.Parallel() 298 | t.Run("float", func(t *testing.T) { 299 | t.Parallel() 300 | check.GreaterThan(t, 2.0, 1.0) 301 | check.GreaterThanOrEqual(t, 2.0, 1.0) 302 | check.GreaterThanOrEqual(t, 2.0, 2.0) 303 | 304 | mt := &common.MockT{} 305 | check.GreaterThan(mt, 2.0, 2.0) 306 | check.True(t, mt.Failed()) 307 | check.False(t, mt.FailedNow()) 308 | 309 | mt = &common.MockT{} 310 | check.GreaterThanOrEqual(mt, 1.0, 2.0) 311 | check.True(t, mt.Failed()) 312 | check.False(t, mt.FailedNow()) 313 | }) 314 | t.Run("int", func(t *testing.T) { 315 | t.Parallel() 316 | check.GreaterThan(t, 2, 1) 317 | check.GreaterThanOrEqual(t, 2, 1) 318 | check.GreaterThanOrEqual(t, 1, 1) 319 | 320 | mt := &common.MockT{} 321 | check.GreaterThan(mt, 2, 2) 322 | check.True(t, mt.Failed()) 323 | check.False(t, mt.FailedNow()) 324 | 325 | mt = &common.MockT{} 326 | check.GreaterThanOrEqual(mt, 1, 2) 327 | check.True(t, mt.Failed()) 328 | check.False(t, mt.FailedNow()) 329 | }) 330 | t.Run("string", func(t *testing.T) { 331 | t.Parallel() 332 | check.GreaterThan(t, "bbb", "aaa") 333 | check.GreaterThanOrEqual(t, "bbb", "aaa") 334 | check.GreaterThanOrEqual(t, "bbb", "bbb") 335 | 336 | mt := &common.MockT{} 337 | check.GreaterThan(mt, "bbb", "bbb") 338 | check.True(t, mt.Failed()) 339 | check.False(t, mt.FailedNow()) 340 | 341 | mt = &common.MockT{} 342 | check.GreaterThanOrEqual(mt, "aaa", "bbb") 343 | check.True(t, mt.Failed()) 344 | check.False(t, mt.FailedNow()) 345 | }) 346 | t.Run("rune", func(t *testing.T) { 347 | t.Parallel() 348 | check.GreaterThan(t, 'b', 'a') 349 | check.GreaterThanOrEqual(t, 'b', 'a') 350 | check.GreaterThanOrEqual(t, 'b', 'b') 351 | 352 | mt := &common.MockT{} 353 | check.GreaterThan(mt, 'b', 'b') 354 | check.True(t, mt.Failed()) 355 | check.False(t, mt.FailedNow()) 356 | 357 | mt = &common.MockT{} 358 | check.GreaterThanOrEqual(mt, 'a', 'b') 359 | check.True(t, mt.Failed()) 360 | check.False(t, mt.FailedNow()) 361 | }) 362 | } 363 | 364 | func TestError(t *testing.T) { 365 | t.Parallel() 366 | t.Run("error", func(t *testing.T) { 367 | t.Parallel() 368 | check.Error(t, fmt.Errorf("new error")) 369 | 370 | mt := &common.MockT{} 371 | check.Error(mt, nil) 372 | check.True(t, mt.Failed()) 373 | check.False(t, mt.FailedNow()) 374 | }) 375 | t.Run("nil", func(t *testing.T) { 376 | t.Parallel() 377 | check.Nil(t, nil) 378 | 379 | mt := &common.MockT{} 380 | check.Nil(mt, fmt.Errorf("new error")) 381 | check.True(t, mt.Failed()) 382 | check.False(t, mt.FailedNow()) 383 | }) 384 | t.Run("noerror", func(t *testing.T) { 385 | t.Parallel() 386 | check.NoError(t, nil) 387 | 388 | mt := &common.MockT{} 389 | check.NoError(mt, fmt.Errorf("new error")) 390 | check.True(t, mt.Failed()) 391 | check.False(t, mt.FailedNow()) 392 | }) 393 | } 394 | 395 | func TestIn(t *testing.T) { 396 | t.Parallel() 397 | t.Run("int", func(t *testing.T) { 398 | t.Parallel() 399 | check.In(t, 1, []int{1, 2, 3}) 400 | 401 | mt := &common.MockT{} 402 | check.In(mt, 1, []int{4, 5, 6}) 403 | check.True(t, mt.Failed()) 404 | check.False(t, mt.FailedNow()) 405 | }) 406 | t.Run("nil equality", func(t *testing.T) { 407 | t.Parallel() 408 | check.In(t, nil, []any{nil}) 409 | 410 | mt := &common.MockT{} 411 | check.In(mt, nil, []any{}) 412 | check.True(t, mt.Failed()) 413 | check.False(t, mt.FailedNow()) 414 | }) 415 | t.Run("strings", func(t *testing.T) { 416 | t.Parallel() 417 | check.In(t, "world", []string{"hello", "world"}) 418 | 419 | mt := &common.MockT{} 420 | check.In(mt, "world", []string{"hello world"}) 421 | check.True(t, mt.Failed()) 422 | check.False(t, mt.FailedNow()) 423 | }) 424 | t.Run("hidden struct fields with cmp.opts", func(t *testing.T) { 425 | t.Parallel() 426 | // Equal works with types that have hidden fields, you just need to use a 427 | // cmp.Option. 428 | customStructWithHiddenField := hiddenPerson{Name: "Peter", hidden: true} 429 | check.In( 430 | t, 431 | customStructWithHiddenField, 432 | []hiddenPerson{ 433 | customStructWithHiddenField, 434 | }, 435 | cmp.AllowUnexported(hiddenPerson{}), 436 | ) 437 | 438 | mt := &common.MockT{} 439 | check.In( 440 | mt, 441 | hiddenPerson{Name: "Peter", hidden: true}, 442 | []hiddenPerson{ 443 | {Name: "Peter", hidden: false}, 444 | }, 445 | cmp.AllowUnexported(hiddenPerson{}), 446 | ) 447 | check.True(t, mt.Failed()) 448 | check.False(t, mt.FailedNow()) 449 | }) 450 | 451 | t.Run("time.time structs with custom .equals", func(t *testing.T) { 452 | t.Parallel() 453 | t1 := time.Now() 454 | t2 := t1.UTC() 455 | check.In(t, &t1, []*time.Time{nil, &t2}) 456 | 457 | mt := &common.MockT{} 458 | check.In( 459 | mt, 460 | t1, 461 | []time.Time{ 462 | t1.Add(1 * time.Hour), 463 | }, 464 | cmp.AllowUnexported(hiddenPerson{}), 465 | ) 466 | check.True(t, mt.Failed()) 467 | check.False(t, mt.FailedNow()) 468 | }) 469 | 470 | t.Run("empty slice", func(t *testing.T) { 471 | t.Parallel() 472 | mt := &common.MockT{} 473 | check.In(mt, 1, []int{}) 474 | check.True(t, mt.Failed()) 475 | check.False(t, mt.FailedNow()) 476 | 477 | mt = &common.MockT{} 478 | check.In(mt, 1, nil) 479 | check.True(t, mt.Failed()) 480 | check.False(t, mt.FailedNow()) 481 | }) 482 | } 483 | 484 | func TestNotIn(t *testing.T) { 485 | t.Parallel() 486 | t.Run("int", func(t *testing.T) { 487 | t.Parallel() 488 | res := check.NotIn(t, 1, []int{4, 5, 6}) 489 | check.True(t, res) 490 | 491 | mt := &common.MockT{} 492 | res = check.NotIn(mt, 1, []int{1, 2, 3}) 493 | check.False(t, res) 494 | check.True(t, mt.Failed()) 495 | check.False(t, mt.FailedNow()) 496 | }) 497 | t.Run("nil equality", func(t *testing.T) { 498 | t.Parallel() 499 | res := check.NotIn(t, nil, []any{}) 500 | check.True(t, res) 501 | 502 | mt := &common.MockT{} 503 | res = check.NotIn(mt, nil, []any{nil}) 504 | check.False(t, res) 505 | check.True(t, mt.Failed()) 506 | check.False(t, mt.FailedNow()) 507 | }) 508 | t.Run("strings", func(t *testing.T) { 509 | t.Parallel() 510 | check.NotIn(t, "world", []string{"hello world"}) 511 | 512 | mt := &common.MockT{} 513 | check.NotIn(mt, "world", []string{"hello", "world"}) 514 | check.True(t, mt.Failed()) 515 | check.False(t, mt.FailedNow()) 516 | }) 517 | t.Run("hidden struct fields with cmp.opts", func(t *testing.T) { 518 | t.Parallel() 519 | // Equal works with types that have hidden fields, you just need to use a 520 | // cmp.Option. 521 | customStructWithHiddenField := hiddenPerson{Name: "Peter", hidden: true} 522 | check.NotIn( 523 | t, 524 | hiddenPerson{Name: "Peter", hidden: true}, 525 | []hiddenPerson{ 526 | {Name: "Peter", hidden: false}, 527 | }, 528 | cmp.AllowUnexported(hiddenPerson{}), 529 | ) 530 | 531 | mt := &common.MockT{} 532 | check.NotIn( 533 | mt, 534 | customStructWithHiddenField, 535 | []hiddenPerson{ 536 | customStructWithHiddenField, 537 | }, 538 | cmp.AllowUnexported(hiddenPerson{}), 539 | ) 540 | check.True(t, mt.Failed()) 541 | check.False(t, mt.FailedNow()) 542 | }) 543 | 544 | t.Run("time.time structs with custom .equals", func(t *testing.T) { 545 | t.Parallel() 546 | t1 := time.Now() 547 | check.NotIn( 548 | t, 549 | t1, 550 | []time.Time{ 551 | t1.Add(1 * time.Hour), 552 | }, 553 | ) 554 | 555 | mt := &common.MockT{} 556 | t2 := t1.UTC() 557 | check.NotIn(mt, t1, []time.Time{t1, t2}) 558 | check.True(t, mt.Failed()) 559 | check.False(t, mt.FailedNow()) 560 | }) 561 | 562 | t.Run("empty slice", func(t *testing.T) { 563 | t.Parallel() 564 | check.NotIn(t, 1, []int{}) 565 | check.NotIn(t, 1, nil) 566 | }) 567 | } 568 | 569 | func TestNil(t *testing.T) { 570 | t.Parallel() 571 | 572 | t.Run("error", func(t *testing.T) { 573 | t.Parallel() 574 | var val error 575 | check.True(t, val == nil) 576 | check.Nil(t, val) 577 | 578 | val = fmt.Errorf("new error") 579 | check.True(t, val != nil) 580 | 581 | mt := &common.MockT{} 582 | check.Nil(mt, val) 583 | check.True(t, mt.Failed()) 584 | check.False(t, mt.FailedNow()) 585 | }) 586 | 587 | t.Run("chan", func(t *testing.T) { 588 | t.Parallel() 589 | var val chan int 590 | check.True(t, val == nil) 591 | check.Nil(t, val) 592 | 593 | val = make(chan int) 594 | check.True(t, val != nil) 595 | 596 | mt := &common.MockT{} 597 | check.Nil(mt, val) 598 | check.True(t, mt.Failed()) 599 | check.False(t, mt.FailedNow()) 600 | }) 601 | 602 | t.Run("func", func(t *testing.T) { 603 | t.Parallel() 604 | var val func() 605 | check.True(t, val == nil) 606 | check.Nil(t, val) 607 | 608 | val = func() {} 609 | check.True(t, val != nil) 610 | 611 | mt := &common.MockT{} 612 | check.Nil(mt, val) 613 | check.True(t, mt.Failed()) 614 | check.False(t, mt.FailedNow()) 615 | }) 616 | 617 | t.Run("interface", func(t *testing.T) { 618 | t.Parallel() 619 | var val any 620 | check.True(t, val == nil) 621 | check.Nil(t, val) 622 | 623 | val = any("hello") //nolint:staticcheck // intentional 624 | check.True(t, val != nil) //nolint:staticcheck // intentional 625 | 626 | mt := &common.MockT{} 627 | check.Nil(mt, val) 628 | check.True(t, mt.Failed()) 629 | check.False(t, mt.FailedNow()) 630 | }) 631 | 632 | t.Run("map", func(t *testing.T) { 633 | t.Parallel() 634 | var val map[string]int 635 | check.True(t, val == nil) 636 | check.Nil(t, val) 637 | 638 | val = map[string]int{} 639 | check.True(t, val != nil) 640 | 641 | mt := &common.MockT{} 642 | check.Nil(mt, val) 643 | check.True(t, mt.Failed()) 644 | check.False(t, mt.FailedNow()) 645 | }) 646 | 647 | t.Run("pointer", func(t *testing.T) { 648 | t.Parallel() 649 | var val *int 650 | check.True(t, val == nil) 651 | check.Nil(t, val) 652 | 653 | wrapped := 8 654 | val = &wrapped 655 | check.True(t, val != nil) 656 | 657 | mt := &common.MockT{} 658 | check.Nil(mt, val) 659 | check.True(t, mt.Failed()) 660 | check.False(t, mt.FailedNow()) 661 | }) 662 | 663 | t.Run("slice", func(t *testing.T) { 664 | t.Parallel() 665 | var val []int 666 | check.True(t, val == nil) 667 | check.Nil(t, val) 668 | 669 | // empty slice 670 | val = []int{} 671 | check.True(t, val != nil) 672 | 673 | mt := &common.MockT{} 674 | check.Nil(mt, val) 675 | check.True(t, mt.Failed()) 676 | check.False(t, mt.FailedNow()) 677 | 678 | // full slice 679 | val = []int{1, 2, 3, 4, 5} 680 | check.True(t, val != nil) 681 | 682 | mt = &common.MockT{} 683 | check.Nil(mt, val) 684 | check.True(t, mt.Failed()) 685 | check.False(t, mt.FailedNow()) 686 | }) 687 | 688 | t.Run("unsafe pointer", func(t *testing.T) { 689 | t.Parallel() 690 | var val unsafe.Pointer 691 | check.True(t, val == nil) 692 | check.Nil(t, val) 693 | 694 | wrapped := 8 695 | val = unsafe.Pointer(&wrapped) 696 | check.True(t, val != nil) 697 | 698 | mt := &common.MockT{} 699 | check.Nil(mt, val) 700 | check.True(t, mt.Failed()) 701 | check.False(t, mt.FailedNow()) 702 | }) 703 | } 704 | 705 | func TestNotNil(t *testing.T) { 706 | t.Parallel() 707 | 708 | t.Run("error", func(t *testing.T) { 709 | t.Parallel() 710 | val := fmt.Errorf("new error") 711 | check.True(t, val != nil) 712 | check.NotNil(t, val) 713 | 714 | mt := &common.MockT{} 715 | val = nil 716 | check.NotNil(mt, val) 717 | check.True(t, mt.Failed()) 718 | check.False(t, mt.FailedNow()) 719 | }) 720 | 721 | t.Run("error", func(t *testing.T) { 722 | t.Parallel() 723 | val := make(chan int) 724 | check.True(t, val != nil) 725 | check.NotNil(t, val) 726 | 727 | mt := &common.MockT{} 728 | val = nil 729 | check.NotNil(mt, val) 730 | check.True(t, mt.Failed()) 731 | check.False(t, mt.FailedNow()) 732 | }) 733 | 734 | t.Run("func", func(t *testing.T) { 735 | t.Parallel() 736 | val := func() {} 737 | check.True(t, val != nil) 738 | check.NotNil(t, val) 739 | 740 | mt := &common.MockT{} 741 | val = nil 742 | check.NotNil(mt, val) 743 | check.True(t, mt.Failed()) 744 | check.False(t, mt.FailedNow()) 745 | }) 746 | 747 | t.Run("interface", func(t *testing.T) { 748 | t.Parallel() 749 | val := any("hello") //nolint:staticcheck // intentional 750 | check.True(t, val != nil) //nolint:staticcheck // intentional 751 | check.NotNil(t, val) 752 | 753 | mt := &common.MockT{} 754 | val = nil 755 | check.NotNil(mt, val) 756 | check.True(t, mt.Failed()) 757 | check.False(t, mt.FailedNow()) 758 | }) 759 | 760 | t.Run("map", func(t *testing.T) { 761 | t.Parallel() 762 | val := map[string]int{} 763 | check.True(t, val != nil) 764 | check.NotNil(t, val) 765 | 766 | mt := &common.MockT{} 767 | val = nil 768 | check.NotNil(mt, val) 769 | check.True(t, mt.Failed()) 770 | check.False(t, mt.FailedNow()) 771 | }) 772 | 773 | t.Run("pointer", func(t *testing.T) { 774 | t.Parallel() 775 | wrapped := 8 776 | val := &wrapped 777 | check.True(t, val != nil) 778 | check.NotNil(t, val) 779 | 780 | mt := &common.MockT{} 781 | val = nil 782 | check.NotNil(mt, val) 783 | check.True(t, mt.Failed()) 784 | check.False(t, mt.FailedNow()) 785 | }) 786 | 787 | t.Run("slice", func(t *testing.T) { 788 | t.Parallel() 789 | // empty slice 790 | val := []int{} 791 | check.True(t, val != nil) 792 | check.NotNil(t, val) 793 | 794 | // full slice 795 | val = []int{1, 2, 3, 4, 5} 796 | check.True(t, val != nil) 797 | check.NotNil(t, val) 798 | 799 | // nil 800 | val = nil 801 | mt := &common.MockT{} 802 | check.NotNil(mt, val) 803 | check.True(t, mt.Failed()) 804 | check.False(t, mt.FailedNow()) 805 | }) 806 | 807 | t.Run("unsafe pointer", func(t *testing.T) { 808 | t.Parallel() 809 | wrapped := 8 810 | val := unsafe.Pointer(&wrapped) 811 | check.True(t, val != nil) 812 | check.NotNil(t, val) 813 | 814 | val = nil 815 | mt := &common.MockT{} 816 | check.NotNil(mt, val) 817 | check.True(t, mt.Failed()) 818 | check.False(t, mt.FailedNow()) 819 | }) 820 | } 821 | -------------------------------------------------------------------------------- /common/t.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | // T is an interface implemented by *testing.T, for compatibility 4 | // and (lol) testing purposes. 5 | type T interface { 6 | // These are the methods on *testing.T that Testy actually uses. 7 | Error(args ...any) // log a message, mark as failed, continue 8 | Fail() // mark as failed, continue 9 | FailNow() // mark as failed, exit 10 | 11 | // These methods are needed to test Testy. (Say that three times fast) 12 | Helper() // mark as a helper 13 | Failed() bool // yes if the test has failed 14 | } 15 | 16 | // MockT is designed to be used in tests to make sure that Testy fails in the 17 | // appropriate ways. 18 | type MockT struct { 19 | failed bool 20 | failednow bool 21 | } 22 | 23 | func (t *MockT) Failed() bool { 24 | return t.failed 25 | } 26 | 27 | func (t *MockT) FailedNow() bool { 28 | return t.failednow 29 | } 30 | 31 | func (t *MockT) Fail() { 32 | t.failed = true 33 | } 34 | 35 | func (t *MockT) FailNow() { 36 | t.Fail() 37 | t.failednow = true 38 | } 39 | 40 | func (*MockT) Log(_ ...any) { 41 | // no-op 42 | } 43 | 44 | func (t *MockT) Error(_ ...any) { 45 | t.Fail() 46 | } 47 | 48 | func (*MockT) Helper() { 49 | // no-op 50 | } 51 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | # This is a shim that allows non-flake Nix users to build this project, using 2 | # the standard compatibility tools. 3 | # 4 | # From https://nixos.wiki/wiki/Flakes#Using_flakes_project_from_a_legacy_Nix 5 | (import 6 | ( 7 | let lock = builtins.fromJSON (builtins.readFile ./flake.lock); 8 | in fetchTarball { 9 | url = 10 | "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; 11 | sha256 = lock.nodes.flake-compat.locked.narHash; 12 | } 13 | ) 14 | { src = ./.; }).defaultNix 15 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package main_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/peterldowns/testy/assert" 8 | "github.com/peterldowns/testy/check" 9 | ) 10 | 11 | func TestChecks(t *testing.T) { 12 | t.Parallel() 13 | check.True(t, true) 14 | check.False(t, false) 15 | check.Equal(t, []string{"hello"}, []string{"hello"}) 16 | check.NotEqual(t, 17 | map[string]int{"hello": 1}, 18 | map[string]int{"goodbye": 2}, 19 | ) 20 | check.LessThan(t, 1, 4) 21 | check.LessThanOrEqual(t, 4, 4) 22 | check.GreaterThan(t, 8, 6) 23 | check.GreaterThanOrEqual(t, 6, 6) 24 | check.Error(t, fmt.Errorf("oh no")) 25 | check.NoError(t, nil) 26 | check.In(t, 4, []int{2, 3, 4, 5}) 27 | check.NotIn(t, "hello", []string{"goodbye", "world"}) 28 | 29 | var nilm map[string]string 30 | check.Nil(t, nilm) 31 | nilm = map[string]string{"hello": "world"} 32 | check.NotNil(t, nilm) 33 | } 34 | 35 | func TestAsserts(t *testing.T) { 36 | t.Parallel() 37 | assert.True(t, true) 38 | assert.False(t, false) 39 | assert.Equal(t, []string{"hello"}, []string{"hello"}) 40 | assert.NotEqual(t, 41 | map[string]int{"hello": 1}, 42 | map[string]int{"goodbye": 2}, 43 | ) 44 | assert.LessThan(t, 1, 4) 45 | assert.LessThanOrEqual(t, 4, 4) 46 | assert.GreaterThan(t, 8, 6) 47 | assert.GreaterThanOrEqual(t, 6, 6) 48 | assert.Error(t, fmt.Errorf("oh no")) 49 | assert.NoError(t, nil) 50 | assert.In(t, 4, []int{2, 3, 4, 5}) 51 | assert.NotIn(t, "hello", []string{"goodbye", "world"}) 52 | 53 | var nilm map[string]string 54 | assert.Nil(t, nilm) 55 | nilm = map[string]string{"hello": "world"} 56 | assert.NotNil(t, nilm) 57 | } 58 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-compat": { 4 | "flake": false, 5 | "locked": { 6 | "lastModified": 1673956053, 7 | "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", 8 | "owner": "edolstra", 9 | "repo": "flake-compat", 10 | "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", 11 | "type": "github" 12 | }, 13 | "original": { 14 | "owner": "edolstra", 15 | "repo": "flake-compat", 16 | "type": "github" 17 | } 18 | }, 19 | "flake-utils": { 20 | "inputs": { 21 | "systems": "systems" 22 | }, 23 | "locked": { 24 | "lastModified": 1681202837, 25 | "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", 26 | "owner": "numtide", 27 | "repo": "flake-utils", 28 | "rev": "cfacdce06f30d2b68473a46042957675eebb3401", 29 | "type": "github" 30 | }, 31 | "original": { 32 | "owner": "numtide", 33 | "repo": "flake-utils", 34 | "type": "github" 35 | } 36 | }, 37 | "nixpkgs": { 38 | "locked": { 39 | "lastModified": 1739740471, 40 | "narHash": "sha256-5QO5/GdwYcOkAlXl579JefJ0IaUsTrQfirVbLRCmZFc=", 41 | "owner": "NixOS", 42 | "repo": "nixpkgs", 43 | "rev": "888ab7d2844c0578ef8c33b2b591f6c531292cb1", 44 | "type": "github" 45 | }, 46 | "original": { 47 | "owner": "nixos", 48 | "repo": "nixpkgs", 49 | "type": "github" 50 | } 51 | }, 52 | "root": { 53 | "inputs": { 54 | "flake-compat": "flake-compat", 55 | "flake-utils": "flake-utils", 56 | "nixpkgs": "nixpkgs" 57 | } 58 | }, 59 | "systems": { 60 | "locked": { 61 | "lastModified": 1681028828, 62 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 63 | "owner": "nix-systems", 64 | "repo": "default", 65 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 66 | "type": "github" 67 | }, 68 | "original": { 69 | "owner": "nix-systems", 70 | "repo": "default", 71 | "type": "github" 72 | } 73 | } 74 | }, 75 | "root": "root", 76 | "version": 7 77 | } 78 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "typesafe (generics) helpers for better golang tests"; 3 | inputs = { 4 | nixpkgs.url = "github:nixos/nixpkgs"; 5 | 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | 8 | flake-compat.url = "github:edolstra/flake-compat"; 9 | flake-compat.flake = false; 10 | }; 11 | 12 | outputs = { self, ... }@inputs: 13 | inputs.flake-utils.lib.eachDefaultSystem (system: 14 | let 15 | # standard nix definitions 16 | overlays = [ ]; 17 | pkgs = import inputs.nixpkgs { 18 | inherit system overlays; 19 | }; 20 | lib = pkgs.lib; 21 | # localias specific 22 | version = (builtins.readFile ./VERSION); 23 | in 24 | rec { 25 | packages = rec { }; 26 | apps = rec { }; 27 | devShells = rec { 28 | default = pkgs.mkShell { 29 | packages = with pkgs; 30 | [ 31 | # golang 32 | delve 33 | go-outline 34 | go_1_24 35 | golangci-lint 36 | gopkgs 37 | gopls 38 | gotools 39 | gotests 40 | gomodifytags 41 | impl 42 | # nix 43 | nixpkgs-fmt 44 | nil 45 | # xcode: this wrapper symlinks to the host system. 46 | # other tools 47 | just 48 | ]; 49 | 50 | shellHook = '' 51 | # The path to this repository 52 | shell_nix="''${IN_LORRI_SHELL:-$(pwd)/shell.nix}" 53 | workspace_root=$(dirname "$shell_nix") 54 | export WORKSPACE_ROOT="$workspace_root" 55 | 56 | # We put the $GOPATH/$GOCACHE/$GOENV in $TOOLCHAIN_ROOT, 57 | # and ensure that the GOPATH's bin dir is on our PATH so tools 58 | # can be installed with `go install`. 59 | # 60 | # Any tools installed explicitly with `go install` will take precedence 61 | # over versions installed by Nix due to the ordering here. 62 | export TOOLCHAIN_ROOT="$workspace_root/.toolchain" 63 | export GOROOT= 64 | export GOCACHE="$TOOLCHAIN_ROOT/go/cache" 65 | export GOENV="$TOOLCHAIN_ROOT/go/env" 66 | export GOPATH="$TOOLCHAIN_ROOT/go/path" 67 | export GOMODCACHE="$GOPATH/pkg/mod" 68 | export PATH=$(go env GOPATH)/bin:$PATH 69 | export CGO_ENABLED=1 70 | 71 | # Make it easy to test while developing; add the golang and nix 72 | # build outputs to the path. 73 | export PATH="$workspace_root/bin:$workspace_root/result/bin:$PATH" 74 | ''; 75 | 76 | # Need to disable fortify hardening because GCC is not built with -oO, 77 | # which means that if CGO_ENABLED=1 (which it is by default) then the golang 78 | # debugger fails. 79 | # see https://github.com/NixOS/nixpkgs/pull/12895/files 80 | hardeningDisable = [ "fortify" ]; 81 | }; 82 | }; 83 | }); 84 | } 85 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/peterldowns/testy 2 | 3 | go 1.21 4 | 5 | toolchain go1.24.0 6 | 7 | require github.com/google/go-cmp v0.7.0 8 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 2 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 3 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | "time" 8 | 9 | "github.com/google/go-cmp/cmp" 10 | 11 | "github.com/peterldowns/testy/assert" 12 | "github.com/peterldowns/testy/check" 13 | ) 14 | 15 | type person struct { 16 | Name string 17 | Age int32 18 | } 19 | 20 | func TestEquality(t *testing.T) { 21 | t.Parallel() 22 | // These structs are not equal under deep or strict equality, they are 23 | // simply different. 24 | peter := &person{Name: "peter", Age: int32(29)} 25 | johan := &person{Name: "johan", Age: int32(28)} 26 | assert.NotEqual(t, peter, johan) 27 | 28 | // These two Time objects are equal due to the logic in their .Equal() 29 | // method, but they are not strictly equal because the structs contain 30 | // fields with different values. 31 | t1 := time.Now() 32 | t2 := t1.UTC() 33 | assert.Equal(t, t1, t2) 34 | } 35 | 36 | func TestStructuringHelpers(t *testing.T) { 37 | t.Parallel() 38 | // This is one way to execute a series of checks 39 | // and exit the test if any of them have failed. 40 | check.Equal(t, 2, 2) 41 | check.LessThanOrEqual(t, 2, 3) 42 | check.GreaterThan(t, 3, 1) 43 | assert.NoFailures(t) 44 | 45 | // This is another, equivalent, way to do the same thing 46 | assert.NoFailures(t, func() { 47 | check.Equal(t, 2, 2) 48 | check.LessThanOrEqual(t, 2, 3) 49 | check.GreaterThan(t, 3, 1) 50 | }) 51 | 52 | // This is a way to execute a series of checks 53 | // and exit the test immediately when one fails. 54 | _, err := myHelper(1, 2) 55 | assert.Nil(t, err) 56 | _, err = myHelper(3, -1) 57 | assert.Nil(t, err) 58 | _, err = myHelper(5, 99) 59 | assert.Nil(t, err) 60 | x, err := myHelper(10, 0) 61 | assert.Nil(t, err) 62 | assert.Equal(t, 0, x) 63 | 64 | // This is another, equivalent, way to do the same thing, with a more 65 | // standard golang style. 66 | assert.NoErrors(t, func() error { 67 | if _, err := myHelper(1, 2); err != nil { 68 | return err 69 | } 70 | if _, err := myHelper(3, -1); err != nil { 71 | return err 72 | } 73 | if _, err := myHelper(5, 99); err != nil { 74 | return err 75 | } 76 | x, err := myHelper(10, 0) 77 | if err != nil { 78 | return err 79 | } 80 | assert.Equal(t, 0, x) 81 | return nil 82 | }) 83 | } 84 | 85 | func myHelper(a, b int) (int, error) { 86 | if a == 0 { 87 | return -1, fmt.Errorf("a ccannot be 0") 88 | } 89 | if b == 10 { 90 | return -2, fmt.Errorf("b cannot be 10") 91 | } 92 | return ((a + b) / (b - 10)) / a, nil 93 | } 94 | 95 | // You can use Equal and NotEqual to check if an error is valid or nil, and type 96 | // inference handles everything correctly. Because this is so common, you can 97 | // also use Nil and Error as shorthand. 98 | func TestErrorComparisons(t *testing.T) { 99 | t.Parallel() 100 | var x error 101 | check.Equal(t, nil, x) 102 | x = fmt.Errorf("something went wrong") 103 | check.NotEqual(t, nil, x) 104 | 105 | var y error 106 | check.Nil(t, y) 107 | y = fmt.Errorf("something went wrong") 108 | check.Error(t, y) 109 | } 110 | 111 | // You can use Equal and NotEqual to check if a pointer is valid or nil, 112 | // and type inference handles everything correctly. 113 | func TestPointerComparisons(t *testing.T) { 114 | t.Parallel() 115 | y := "hello" 116 | var x *string 117 | check.Equal(t, nil, x) 118 | x = &y 119 | check.NotEqual(t, nil, x) 120 | } 121 | 122 | func TestExample(t *testing.T) { 123 | t.Parallel() 124 | // If a given check fails, the test will be marked as failed but continue 125 | // executing. All failures are reported when the test stops executing, 126 | // either at the end of the test or when someone calls t.FailNow(). 127 | check.True(t, true) 128 | check.False(t, false) 129 | check.Equal(t, []string{"hello"}, []string{"hello"}) 130 | check.NotEqual(t, map[string]int{"hello": 1}, nil) 131 | check.LessThan(t, 1, 4) 132 | check.LessThanOrEqual(t, 4, 4) 133 | check.GreaterThan(t, 8, 6) 134 | check.GreaterThanOrEqual(t, 6, 6) 135 | check.Error(t, fmt.Errorf("oh no")) 136 | check.Nil(t, nil) 137 | // If a given assert fails, the test will immediately be marked as failed 138 | // stop executing, and report all failures. 139 | assert.True(t, true) 140 | assert.False(t, false) 141 | assert.Equal(t, []string{"hello"}, []string{"hello"}) 142 | assert.NotEqual(t, map[string]int{"hello": 1}, nil) 143 | assert.LessThan(t, 1, 4) 144 | assert.LessThanOrEqual(t, 4, 4) 145 | assert.GreaterThan(t, 8, 6) 146 | assert.GreaterThanOrEqual(t, 6, 6) 147 | assert.Error(t, fmt.Errorf("oh no")) 148 | assert.Nil(t, nil) 149 | } 150 | 151 | func TestByteEquality(t *testing.T) { 152 | t.Parallel() 153 | // This is an example from 154 | // https://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/index.html#compare_struct_arr_slice_map 155 | // showing how a go-cmp cmp.Option can be used to control equality behavior. 156 | // By default, a nil slice `nil` != an empty but non-nil slice `{}`. By 157 | // passing in a custom equals method `func[T any](a, b T) bool`, go-cmp will 158 | // use it. The `bytes.Equal` method is designed to handle exactly this case 159 | // and consider both byte slices equal. 160 | var b1 []byte 161 | b2 := []byte{} 162 | check.NotEqual(t, b1, b2) 163 | check.Equal(t, b1, b2, cmp.Comparer(bytes.Equal)) 164 | } 165 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | # This is a shim that allows non-flake Nix users to build this project, using 2 | # the standard compatibility tools. 3 | # 4 | # From https://nixos.wiki/wiki/Flakes#Using_flakes_project_from_a_legacy_Nix 5 | (import 6 | ( 7 | let lock = builtins.fromJSON (builtins.readFile ./flake.lock); 8 | in fetchTarball { 9 | url = 10 | "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; 11 | sha256 = lock.nodes.flake-compat.locked.narHash; 12 | } 13 | ) 14 | { src = ./.; }).shellNix.default 15 | --------------------------------------------------------------------------------