From 7346fe3ac9b6acce18ade81172cb8459a2e8391f Mon Sep 17 00:00:00 2001 From: Frederic BIDON Date: Tue, 27 Jan 2026 19:23:12 +0100 Subject: [PATCH] doc: update * added more examples and advanced usage guidance * reformatted go code in markdown * fixed broken links, misspellings * added presentation of the approach to testing Signed-off-by: Frederic BIDON --- .claude/CLAUDE.md | 48 +- README.md | 8 +- .../internal/generator/funcmaps/markdown.go | 1 - codegen/internal/model/model.go | 2 +- codegen/main.go | 6 +- docs/doc-site/_index.md | 12 +- docs/doc-site/project/APPROACH.md | 451 +++++++++++ .../project/maintainers/BENCHMARKS.md | 8 +- docs/doc-site/project/maintainers/CODEGEN.md | 51 +- docs/doc-site/project/maintainers/ORIGINAL.md | 1 - docs/doc-site/usage/EXAMPLES.md | 730 +++++++++++++----- docs/doc-site/usage/GENERICS.md | 237 +++--- docs/doc-site/usage/MIGRATION.md | 4 - docs/doc-site/usage/TUTORIAL.md | 518 +++++++------ docs/doc-site/usage/USAGE.md | 375 ++++++--- internal/testintegration/README.md | 56 +- 16 files changed, 1821 insertions(+), 687 deletions(-) create mode 100644 docs/doc-site/project/APPROACH.md diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 374c68847..47b49e760 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -121,9 +121,9 @@ go test ./codegen/internal/... # Scanner and generator tests go test ./internal/assertions -run TestEqual # Run with coverage -go test -cover ./internal/assertions # Should be 90%+ -go test -cover ./assert # Should be ~100% -go test -cover ./require # Should be ~100% +go test -cover ./internal/assertions # Should be 90%+ +go test -cover ./assert # Should be ~100% +go test -cover ./require # Should be ~100% go test -cover ./codegen/internal/scanner/... # Scanner tests # Run tests with verbose output @@ -160,6 +160,11 @@ cd hack/doc-site/hugo **Example - Adding a new assertion:** ```go +import ( + "fmt" + "strings" +) + // In internal/assertions/string.go // StartsWith asserts that the string starts with the given prefix. @@ -168,16 +173,16 @@ cd hack/doc-site/hugo // // Examples: // -// success: "hello world", "hello" -// failure: "hello world", "bye" +// success: "hello world", "hello" +// failure: "hello world", "bye" func StartsWith(t T, str, prefix string, msgAndArgs ...any) bool { - if h, ok := t.(H); ok { - h.Helper() - } - if !strings.HasPrefix(str, prefix) { - return Fail(t, fmt.Sprintf("Expected %q to start with %q", str, prefix), msgAndArgs...) - } - return true + if h, ok := t.(H); ok { + h.Helper() + } + if !strings.HasPrefix(str, prefix) { + return Fail(t, fmt.Sprintf("Expected %q to start with %q", str, prefix), msgAndArgs...) + } + return true } ``` @@ -233,10 +238,10 @@ The generator reads "Examples:" sections from doc comments: // // Examples: // -// success: 123, 123 -// failure: 123, 456 +// success: 123, 123 +// failure: 123, 456 func Equal(t T, expected, actual any, msgAndArgs ...any) bool { - // implementation + // implementation } ``` @@ -297,10 +302,11 @@ To assign a function to a domain, add a domain tag in its doc comment: // domain: equality // // Examples: -// success: 123, 123 -// failure: 123, 456 +// +// success: 123, 123 +// failure: 123, 456 func Equal(t T, expected, actual any, msgAndArgs ...any) bool { - // implementation + // implementation } ``` @@ -396,6 +402,12 @@ This layered approach ensures: **This repository uses a signature testing pattern** based on Go 1.23's `iter.Seq` for all table-driven tests: ```go +import ( + "iter" + "slices" + "testing" +) + // Define test case struct type parseTestExamplesCase struct { name string diff --git a/README.md b/README.md index ad3892d3b..ceb34c3a4 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ go get github.com/go-openapi/testify/v2 `testify` simplifies your test assertions like so. ```go - import ( +import ( "testing" ) ... @@ -101,7 +101,7 @@ go get github.com/go-openapi/testify/v2 Becomes: ```go - import ( +import ( "testing" "github.com/go-openapi/testify/v2/require" ) @@ -190,8 +190,8 @@ Maintainers can cut a new release by either: [doc-examples]: https://go-openapi.github.io/testify/usage/examples [doc-generics]: https://go-openapi.github.io/testify/usage/generics [example-with-generics-url]: https://go-openapi.github.io/testify#usage-with-generics -[godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify -[godoc-url]: http://pkg.go.dev/github.com/go-openapi/testify +[godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/testify/v2 +[godoc-url]: https://pkg.go.dev/github.com/go-openapi/testify/v2 [slack-logo]: https://a.slack-edge.com/e6a93c1/img/icons/favicon-32.png [slack-badge]: https://img.shields.io/badge/slack-blue?link=https%3A%2F%2Fgoswagger.slack.com%2Farchives%2FC04R30YM [slack-url]: https://goswagger.slack.com/archives/C04R30YMU diff --git a/codegen/internal/generator/funcmaps/markdown.go b/codegen/internal/generator/funcmaps/markdown.go index f1e563041..ce0ad46bd 100644 --- a/codegen/internal/generator/funcmaps/markdown.go +++ b/codegen/internal/generator/funcmaps/markdown.go @@ -25,7 +25,6 @@ const sensiblePrealloc = 20 // 1. Reference-style markdown links: [text]: url // 2. Godoc-style links: [errors.Is], [testing.T], etc. func FormatMarkdown(in string) string { - // Step 1: Extract reference-style link definitions // Pattern: [text]: url (at start of line or after whitespace) refLinks := make(map[string]string) diff --git a/codegen/internal/model/model.go b/codegen/internal/model/model.go index 2ba412d4f..965d4a8f0 100644 --- a/codegen/internal/model/model.go +++ b/codegen/internal/model/model.go @@ -133,7 +133,7 @@ func (f Function) GenericName(suffixes ...string) string { } // GenericCallName renders the function name with explicit type parameters. -// This is used when forwarding type parameters, as all type parameters may not be always infered from the arguments. +// This is used when forwarding type parameters, as all type parameters may not be always inferred from the arguments. func (f Function) GenericCallName(suffixes ...string) string { suffix := strings.Join(suffixes, "") if !f.IsGeneric { // means len(f.TypeParams) == 0 diff --git a/codegen/main.go b/codegen/main.go index ae1b20181..350a455f6 100644 --- a/codegen/main.go +++ b/codegen/main.go @@ -63,6 +63,7 @@ func registerFlags(cfg *config) { } func execute(cfg *config) error { + // 1. Scan internal/assertions scanner := scanner.New( scanner.WithWorkDir(cfg.dir), scanner.WithPackage(cfg.inputPkg), @@ -73,6 +74,7 @@ func execute(cfg *config) error { return err } + // 2. Generate assert and require packages builder := generator.New(scanned) var doc model.Documentation @@ -105,7 +107,9 @@ func execute(cfg *config) error { return nil } - // and now for something completely different: generating the documentation + // ... and now for something completely different: generating the documentation + // + // 3. Generate API documentation in docs/doc-site/api documentalist := generator.NewDocGenerator(doc) err = documentalist.Generate( // where to generate diff --git a/docs/doc-site/_index.md b/docs/doc-site/_index.md index 4e3c90ee5..62c9a7a92 100644 --- a/docs/doc-site/_index.md +++ b/docs/doc-site/_index.md @@ -20,7 +20,7 @@ This is the go-openapi fork of the great [testify](https://github.com/stretchr/t {{% button href="https://github.com/go-openapi/testify/fork" hint="fork me on github" style=primary icon=code-fork %}}Fork me{{% /button %}} Design and exploration phase. Feedback, contributions and proposals are welcome. -See our [ROADMAP](./maintainers/ROADMAP.md). +See our [ROADMAP](./project/maintainers/ROADMAP.md). ### Motivation @@ -31,7 +31,7 @@ See [why we wanted a v2](./MOTIVATION.md). Import this library in your project like so. ```cmd - go get github.com/go-openapi/testify/v2 +go get github.com/go-openapi/testify/v2 ``` ... and start writing tests. Look at our [examples][doc-examples]. @@ -43,7 +43,7 @@ Import this library in your project like so. {{< cards >}} {{% card title="Standard library" %}} ```go - import ( +import ( "testing" ) ... @@ -64,7 +64,7 @@ Import this library in your project like so. {{% card title="testify" %}} ```go - import ( +import ( "testing" "github.com/go-openapi/testify/v2/require" @@ -91,7 +91,7 @@ Obviously, the `Assertion` type cannot be extended with generic methods, as of ` {{< cards >}} {{% card title="EqualT" %}} ```go - import ( +import ( "testing" "github.com/go-openapi/testify/v2/require" @@ -107,7 +107,7 @@ Obviously, the `Assertion` type cannot be extended with generic methods, as of ` {{% /card %}} {{% card title="InDeltaT" %}} ```go - import ( +import ( "testing" "github.com/go-openapi/testify/v2/require" diff --git a/docs/doc-site/project/APPROACH.md b/docs/doc-site/project/APPROACH.md new file mode 100644 index 000000000..4330920bb --- /dev/null +++ b/docs/doc-site/project/APPROACH.md @@ -0,0 +1,451 @@ +--- +title: "An approach to testing" +description: "How testify builds on top Go testing without replacing it" +weight: 20 +--- + +{{% notice primary "TL;DR" "meteor" %}} +Software testing comes in two major styles: assertion-style (xUnit tradition: JUnit, pytest, NUnit, **testify**) +and BDD-style (RSpec, Cucumber, **Ginkgo**). + +Both are valid approaches. However, we think that **testify's assertion-style naturally aligns with Go's core values**: +simplicity, explicitness, standard library first, minimal abstraction. + +Testify brings powerful testing to Go developers who embrace these values: +zero-dependencies, reflection-based or generic assertions, and no framework (just works with `go test`). + +**If you chose Go for its philosophy, assertion-style testing is the natural extension of those values to your test suite.** +{{% /notice %}} + +--- + +## Make testing better. Keep it Go + +**go-openapi/testify** follows a simple philosophy: **make Go testing better without reinventing it**. + +Testify follows the assertion style: it is not a BDD framework. So you won't find chaining methods that produce +English-like sentences. + +Unlike frameworks that introduce new paradigms and require specialized tooling, +testify builds directly on top of Go's great standard `testing` package. + +It provides powerful assertions and utilities while preserving the familiar patterns that Go developers already know. +Testing patterns and constructs remain standard. + +### Core Principles + +**1. Zero Dependencies** + +Testify has no external dependencies. Everything you need is self-contained, with internalized implementations of +required functionality. This means: + +- No dependency conflicts in your project +- No supply chain security concerns +- No version compatibility issues +- Chrome is opt-in (all extra features that need additional dependencies are opt-in) + +**2. Standard Go Compatibility** + +Works seamlessly with `go test` and the standard library: + +- No special CLI tools required +- No framework-specific test runners +- Standard Go subtests with `t.Run()` +- Native IDE support out of the box +- Works with any Go test runner + +**3. Type Safety with Generics** + +Testify embraces Go's type system: + +- Most assertions come with a **generic variant** for compile-time type safety +- Catch type mismatches before tests even run +- On average **10x faster** than reflection-based assertions +- Full type inference: no manual type parameters needed +- Complex cases that require dynamic typing use go reflection + +**4. Simplicity and Clarity** + +Keep testing straightforward: + +- Function-based assertions with clear semantics +- No new DSL to learn +- Minimal cognitive overhead +- Immediate productivity for any Go developer + +--- + +## Testing Styles: Assertion vs. BDD + +Software testing has evolved into two primary styles, each with passionate advocates across programming communities. + +### Assertion-Style Testing (xUnit tradition) + +**Core idea**: Write tests as regular code with explicit assertions. + +Originating with Kent Beck's SUnit (Smalltalk) and popularized by JUnit (Java), this style emphasizes: + +- Tests are functions/methods in the language +- Direct assertion calls verify behavior +- Standard language constructs for organization +- Minimal framework abstraction + +**Examples across languages:** + +{{< tabs >}} +{{% tab title="JUnit (Java)" %}} +```java +// JUnit (Java) +@Test +public void testUserCreation() { + User user = createUser("alice@example.com"); + assertNotNull(user); + assertEquals("alice@example.com", user.getEmail()); +} +``` +{{% /tab %}} + +{{% tab title="pytest (python)" %}} +```python +# pytest (Python) +def test_user_creation(): + user = create_user("alice@example.com") + assert user is not None + assert user.email == "alice@example.com" +``` +{{% /tab %}} + +{{% tab title="NUnit (C#)" %}} +```csharp +// NUnit (C#) +[Test] +public void TestUserCreation() +{ + var user = CreateUser("alice@example.com"); + Assert.IsNotNull(user); + Assert.AreEqual("alice@example.com", user.Email); +} +``` +{{% /tab %}} + +{{% tab title="Testify (go)" %}} +```go +// Testify (go) +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" +) + +func TestUserCreation(t *testing.T) { + user := CreateUser("alice@example.com") + require.NotNil(t, user) + assert.Equal(t, "alice@example.com", user.Email) +} +``` +{{% /tab %}} +{{% /tabs %}} + +**Frameworks**: JUnit, NUnit, xUnit.net, pytest, PHPUnit, Go's `testing` package... and `testify`. + +### BDD-Style Testing (Behavior-Driven Development) + +**Core idea**: Write tests as executable specifications in narrative form. + +Originating with RSpec (Ruby) and influenced by Dan North's BDD methodology, this style emphasizes: + +- Tests describe behavior in natural language structure +- Hierarchical organization (describe/context/it) +- Focus on readability and documentation value +- Framework-specific DSL + +**Examples across languages:** + +{{< tabs >}} +{{% tab title="RSpec (Ruby)" %}} +```ruby +# RSpec (Ruby) +describe "User creation" do + it "creates a valid user" do + user = create_user("alice@example.com") + expect(user).not_to be_nil + expect(user.email).to eq("alice@example.com") + end +end +``` +{{% /tab %}} + +{{% tab title="Jasmine (JS)" %}} +```javascript +// Jasmine/Mocha (JavaScript) +describe("User creation", function() { + it("creates a valid user", function() { + const user = createUser("alice@example.com"); + expect(user).not.toBe(null); + expect(user.email).toEqual("alice@example.com"); + }); +}); +``` +{{% /tab %}} + +{{% tab title="behave (python)" %}} +```python +# behave (Python) +Scenario: User creation + Given a valid email address + When I create a user with "alice@example.com" + Then the user should exist + And the email should be "alice@example.com" +``` +{{% /tab %}} + +{{% tab title="Ginkgo (go)" %}} +```go +// Ginkgo +import ( + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" +) + +var _ = ginkgo.Describe("User creation", func() { + ginkgo.It("creates a valid user", func() { + user := CreateUser("alice@example.com") + gomega.Expect(user).NotTo(gomega.BeNil()) + gomega.Expect(user.Email).To(gomega.Equal("alice@example.com")) + }) + }) +``` +{{% /tab %}} +{{% /tabs %}} + +**Frameworks**: RSpec, Jasmine, Mocha, Cucumber, behave, **Ginkgo/Gomega** + +### Both Are Valid + +**Assertion-style strengths:** + +- Low cognitive overhead (just code) +- Minimal framework abstraction +- IDE tooling works naturally +- Easy to learn and adopt + +**BDD-style strengths:** + +- Readable test specifications +- Natural hierarchical organization +- Self-documenting intent +- Stakeholder-friendly output + +**The debate continues** across all programming communities. Neither style is objectively superior; they optimize for +different values and team preferences. + +--- + +## Assertion-Style and Go Values + +While both styles have merit in general, **assertion-style testing aligns naturally with Go's core philosophy**. + +### Go's Design Values + +Go emphasizes: + +- **Simplicity**: maximize clarity +- **Explicitness**: No magic, no hidden behavior +- **Standard library first**: Build on solid foundations +- **Readability**: Code is read more than written +- **Minimal abstraction**: Minimize concepts + +### How Assertion-Style Matches Go + +**1. Simplicity** + +Assertion-style keeps tests simple: they're just Go functions. + +```go +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" +) + +func TestAdd(t *testing.T) { + result := Add(2, 3) + assert.Equal(t, 5, result) // Straightforward, even though we haven't written "When().Two().Plus().Three().IsNot(5).Fail()"... +} +``` + +No new mental model. No framework semantics to learn. If you know Go, you know how to test with our lib. + +**2. Explicitness** + +Every assertion is an explicit function call with clear semantics: + +```go +assert.NotNil(t, user) // Explicit: check for nil +assert.ErrorIs(t, err, ErrNotFound) // Explicit: check error identity +assert.ElementsMatch(t, expected, actual) // Explicit: check collection equality +``` + +Compare to matcher-based approaches where behavior is composed through framework objects. +Assertion-style makes test intent immediately clear _to a programmer_. + +**3. Standard Library First** + +Testify builds on `testing.T`: no replacement, just enhancement. + +```go +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" +) + +func TestUserAPI(t *testing.T) { + t.Run("creation", func(t *testing.T) { // Standard Go subtest + t.Parallel() // Standard Go test parallelism + + user := CreateUser("alice@example.com") + assert.NotNil(t, user) // Enhanced with assertions + }) +} +``` + +Works with `go test`. Works with standard tooling. Works with the Go ecosystem. + +**4. Readability Through Directness** + +Go prioritizes code that's easy to read and understand: + +```go +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" +) + +// Clear, direct, readable +func TestEmailValidation(t *testing.T) { + tests := []struct { + email string + valid bool + }{ + {"alice@example.com", true}, + {"invalid-email", false}, + } + + for _, tt := range tests { + t.Run(tt.email, func(t *testing.T) { + err := ValidateEmail(tt.email) + if tt.valid { + assert.NoError(t, err) + + return + } + + assert.Error(t, err) + }) + } +} +``` + +Standard control flow. Standard Go idioms. No DSL to decode. +Better for most developers, perhaps less so for stakeholders not familiar with Go. + +**5. Minimal Abstraction** + +Go avoids abstraction for abstraction's sake. Testify provides assertions: nothing more. + +- No test lifecycle framework +- No dependency injection system +- No specialized runners +- No mandatory patterns + +Just functions that verify behavior and produce clear errors. +Solve the testing problem, don't create a testing ecosystem. + +### The Natural Fit + +If you appreciate Go's philosophy and if you choose Go because you value simplicity, explicitness, and building on +standards, then **assertion-style testing is the natural extension of those values to your test suite**. + +BDD frameworks serve teams with different priorities (narrative specifications, framework-managed workflows, stakeholder +communication). Those are valid priorities. But they optimize for values orthogonal to Go's design philosophy. +For such teams, **Ginkgo/Omega** provides a great BDD testing framework. + +**For Go developers who embrace Go values, assertion-style testing is the idiomatic approach.**. And **testify** is the tool. + +--- + +## Assertion-Style in Go: Testify vs. BDD Frameworks + +Go's testing ecosystem reflects the broader assertion-vs-BDD divide: + +### Different Philosophies + +| Aspect | Testify (Assertion-Style) | Ginkgo/Gomega (BDD-Style) | +|--------|---------------------------|---------------------------| +| **Testing style** | xUnit tradition | BDD tradition | +| **Approach** | Enhance standard testing | Replace with BDD framework | +| **Integration** | Works with `go test` directly | Requires `ginkgo` CLI tool | +| **Learning curve** | Immediate (standard Go) | Moderate (new DSL) | +| **Dependencies** | Zero external packages | Multiple framework packages | +| **Type safety** | 38 generic assertions | Reflection-based matchers | +| **Organization** | Standard Go subtests | Narrative hierarchy (Describe/Context/It) | +| **Go philosophy** | Aligns with Go values | Different priorities | + +### Example Comparison + +**Assertion-style (Testify):** + +```go +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" +) + +func TestUserCreation(t *testing.T) { + t.Parallel() + + user := CreateUser("alice@example.com") + + // Clear, type-safe assertions + require.NotNil(t, user) + assert.EqualT(t, "alice@example.com", user.Email) // Compile-time type check + assert.True(t, user.Active) +} +``` + +**BDD-style (Ginkgo/Gomega):** + +```go +var _ = Describe("User Creation", func() { + var user *User + + BeforeEach(func() { + user = CreateUser("alice@example.com") + }) + + It("creates a valid user", func() { + Expect(user).ToNot(BeNil()) + Expect(user.Email).To(Equal("alice@example.com")) + Expect(user.Active).To(BeTrue()) + }) +}) +``` + +Both approaches are valid. They reflect different testing philosophies that span the entire software industry. The +question for Go developers is: **which style aligns with the values that drew you to Go in the first place?** + +--- + +## See also + +- [API Reference](../api/) - Browse all 120 assertions by domain +- [Generics Guide](../usage/GENERICS.md) - Leverage type-safe assertions +- [Migration Guide](../usage/MIGRATION.md) - Switch from stretchr/testify +- [Examples](../usage/EXAMPLES.md) - See testify in action + +--- diff --git a/docs/doc-site/project/maintainers/BENCHMARKS.md b/docs/doc-site/project/maintainers/BENCHMARKS.md index 36b78046a..b8cacf395 100644 --- a/docs/doc-site/project/maintainers/BENCHMARKS.md +++ b/docs/doc-site/project/maintainers/BENCHMARKS.md @@ -8,7 +8,10 @@ weight: 10 ## Overview -While the primary motivation for adding **38 generic assertion functions** to testify v2 was **compile-time type safety** (see [Generics Guide](../../usage/GENERICS.md) for details), comprehensive benchmarking revealed an unexpected bonus: **dramatic performance improvements** ranging from 1.2x to 81x faster, with up to 99% reduction in memory allocations for collection operations. +While the primary motivation for adding **generic assertion functions** to testify v2 was **compile-time type safety** +(see [Generics Guide](../../usage/GENERICS.md) for details), our benchmarking revealed an unexpected bonus: +**dramatic performance improvements** ranging from 1.2x to 81x faster, +with up to 99% reduction in memory allocations for collection operations. This document focuses on the performance measurements and explains why these improvements occur. @@ -24,7 +27,8 @@ assert.ElementsMatch(t, []int{1, 2}, []string{"a", "b"}) assert.ElementsMatchT(t, []int{1, 2}, []string{"a", "b"}) // ❌ Compile error! ``` -See the [Generics Guide](../../usage/GENERICS.md) for comprehensive coverage of type safety benefits, refactoring safety, and when to use generic vs reflection variants. +See the [Generics Guide](../../usage/GENERICS.md) for comprehensive coverage of type safety benefits, +refactoring safety, and when to use generic vs reflection variants. ## Performance Results by Category diff --git a/docs/doc-site/project/maintainers/CODEGEN.md b/docs/doc-site/project/maintainers/CODEGEN.md index 45eb95a90..94411f640 100644 --- a/docs/doc-site/project/maintainers/CODEGEN.md +++ b/docs/doc-site/project/maintainers/CODEGEN.md @@ -90,20 +90,25 @@ This repository uses code generation extensively to maintain consistency across The following example would like go to `string.go`, next to the `Regexp` assertion. ```go + import ( + "fmt" + "strings" + ) + // StartsWith asserts that the string starts with the given prefix. // // Examples: // - // success: "hello world", "hello" - // failure: "hello world", "bye" + // success: "hello world", "hello" + // failure: "hello world", "bye" func StartsWith(t T, str, prefix string, msgAndArgs ...any) bool { - if h, ok := t.(H); ok { - h.Helper() - } - if !strings.HasPrefix(str, prefix) { - return Fail(t, fmt.Sprintf("Expected %q to start with %q", str, prefix), msgAndArgs...) - } - return true + if h, ok := t.(H); ok { + h.Helper() + } + if !strings.HasPrefix(str, prefix) { + return Fail(t, fmt.Sprintf("Expected %q to start with %q", str, prefix), msgAndArgs...) + } + return true } ``` @@ -250,17 +255,17 @@ from an external package. The relocation uses go parsing capabilities. The only ```bash go run ./codegen/main.go \ - -work-dir=.. \ - -input-package=github.com/go-openapi/testify/v2/internal/assertions \ - -output-packages=assert,require \ - -target-root=.. \ - -include-format-funcs=true \ - -include-forward-funcs=true \ - -include-tests=true \ - -include-examples=true \ - -runnable-examples=true \ - -include-helpers=true \ - -include-generics=false + -work-dir=.. \ + -input-package=github.com/go-openapi/testify/v2/internal/assertions \ + -output-packages=assert,require \ + -target-root=.. \ + -include-format-funcs=true \ + -include-forward-funcs=true \ + -include-tests=true \ + -include-examples=true \ + -runnable-examples=true \ + -include-helpers=true \ + -include-generics=false ``` Current usage with `go generate` (see `doc.go`): @@ -279,9 +284,9 @@ After generation, verify: go test ./... # Coverage remains high -go test -cover ./internal/assertions # Should be ~94% -go test -cover ./assert # Should be ~99.5% -go test -cover ./require # Should be ~99.5% +go test -cover ./internal/assertions # Should be ~94% +go test -cover ./assert # Should be ~99.5% +go test -cover ./require # Should be ~99.5% # Examples are valid go test -run Example ./assert diff --git a/docs/doc-site/project/maintainers/ORIGINAL.md b/docs/doc-site/project/maintainers/ORIGINAL.md index e85087da0..1e7494941 100644 --- a/docs/doc-site/project/maintainers/ORIGINAL.md +++ b/docs/doc-site/project/maintainers/ORIGINAL.md @@ -189,7 +189,6 @@ func TestSomethingWithPlaceholder(t *testing.T) { // assert that the expectations were met testObj.AssertExpectations(t) - } // TestSomethingElse2 is a third example that shows how you can use diff --git a/docs/doc-site/usage/EXAMPLES.md b/docs/doc-site/usage/EXAMPLES.md index b3900fe20..eb3f37969 100644 --- a/docs/doc-site/usage/EXAMPLES.md +++ b/docs/doc-site/usage/EXAMPLES.md @@ -16,13 +16,14 @@ The simplest way to get started with testify is using the `assert` package: ```go import ( - "testing" - "github.com/go-openapi/testify/v2/assert" + "testing" + + "github.com/go-openapi/testify/v2/assert" ) func TestCalculator(t *testing.T) { - result := Add(2, 3) - assert.Equal(t, 5, result) + result := Add(2, 3) + assert.Equal(t, 5, result) } ``` @@ -31,29 +32,41 @@ func TestCalculator(t *testing.T) { **Use `assert`** when you want tests to continue after a failure: ```go +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + func TestUser(t *testing.T) { - user := GetUser(123) + user := GetUser(123) - // All three checks run, even if the first fails - assert.NotNil(t, user) - assert.Equal(t, "Alice", user.Name) - assert.Equal(t, 25, user.Age) + // All three checks run, even if the first fails + assert.NotNil(t, user) + assert.Equal(t, "Alice", user.Name) + assert.Equal(t, 25, user.Age) } ``` **Use `require`** when subsequent checks depend on earlier ones: ```go -import "github.com/go-openapi/testify/v2/require" +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/go-openapi/testify/v2/require" +) func TestUser(t *testing.T) { - user := GetUser(123) + user := GetUser(123) - // Stop immediately if user is nil (prevents panic on next line) - require.NotNil(t, user) + // Stop immediately if user is nil (prevents panic on next line) + require.NotNil(t, user) - // Only runs if user is not nil - assert.Equal(t, "Alice", user.Name) + // Only runs if user is not nil + assert.Equal(t, "Alice", user.Name) } ``` @@ -66,92 +79,122 @@ func TestUser(t *testing.T) { ### Equality ```go +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" +) + func TestEquality(t *testing.T) { - // Basic equality - assert.Equal(t, 42, actualValue) + // Basic equality + assert.Equal(t, 42, actualValue) - // Deep equality for slices, maps, structs - assert.Equal(t, []int{1, 2, 3}, result) + // Deep equality for slices, maps, structs + assert.Equal(t, []int{1, 2, 3}, result) - // Check inequality - assert.NotEqual(t, 0, result) + // Check inequality + assert.NotEqual(t, 0, result) - // Type-converting equality (123 == int64(123)) - assert.EqualValues(t, 123, int64(123)) + // Type-converting equality (123 == int64(123)) + assert.EqualValues(t, 123, int64(123)) } ``` ### Collections ```go +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" +) + func TestCollections(t *testing.T) { - list := []string{"apple", "banana", "cherry"} + list := []string{"apple", "banana", "cherry"} - // Check if collection contains an element - assert.Contains(t, list, "banana") - assert.NotContains(t, list, "orange") + // Check if collection contains an element + assert.Contains(t, list, "banana") + assert.NotContains(t, list, "orange") - // Check length - assert.Len(t, list, 3) + // Check length + assert.Len(t, list, 3) - // Check if empty - assert.NotEmpty(t, list) - assert.Empty(t, []string{}) + // Check if empty + assert.NotEmpty(t, list) + assert.Empty(t, []string{}) - // Check if all elements match (order doesn't matter) - assert.ElementsMatch(t, []int{3, 1, 2}, []int{1, 2, 3}) + // Check if all elements match (order doesn't matter) + assert.ElementsMatch(t, []int{3, 1, 2}, []int{1, 2, 3}) } ``` ### Errors ```go +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" +) + func TestErrors(t *testing.T) { - // Check if function returns an error - err := DoSomething() - assert.Error(t, err) + // Check if function returns an error + err := DoSomething() + assert.Error(t, err) - // Check if function succeeds - err = DoSomethingElse() - assert.NoError(t, err) + // Check if function succeeds + err = DoSomethingElse() + assert.NoError(t, err) - // Check specific error message - err = Divide(10, 0) - assert.EqualError(t, err, "division by zero") + // Check specific error message + err = Divide(10, 0) + assert.EqualError(t, err, "division by zero") - // Check if error contains text - assert.ErrorContains(t, err, "division") + // Check if error contains text + assert.ErrorContains(t, err, "division") - // Check error type with errors.Is - assert.ErrorIs(t, err, ErrDivisionByZero) + // Check error type with errors.Is + assert.ErrorIs(t, err, ErrDivisionByZero) } ``` ### Nil Checks ```go +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" +) + func TestNil(t *testing.T) { - var ptr *User + var ptr *User - assert.Nil(t, ptr) + assert.Nil(t, ptr) - user := &User{Name: "Alice"} - assert.NotNil(t, user) + user := &User{Name: "Alice"} + assert.NotNil(t, user) } ``` ### Boolean and Comparisons ```go -func TestBooleans(t *testing.T) { - assert.True(t, isValid) - assert.False(t, hasErrors) +import ( + "testing" - // Numeric comparisons - assert.Greater(t, 10, 5) - assert.GreaterOrEqual(t, 10, 10) - assert.Less(t, 5, 10) - assert.LessOrEqual(t, 5, 5) + "github.com/go-openapi/testify/v2/assert" +) + +func TestBooleans(t *testing.T) { + assert.True(t, isValid) + assert.False(t, hasErrors) + + // Numeric comparisons + assert.Greater(t, 10, 5) + assert.GreaterOrEqual(t, 10, 10) + assert.Less(t, 5, 10) + assert.LessOrEqual(t, 5, 5) } ``` @@ -164,46 +207,74 @@ Testify provides multiple ways to call assertions: ### 1. Package-Level Functions ```go +import ( + "testing" + + "gotest.tools/assert" + + "github.com/go-openapi/testify/v2/require" +) + func TestPackageLevel(t *testing.T) { - assert.Equal(t, 42, result) - require.NotNil(t, user) + assert.Equal(t, 42, result) + require.NotNil(t, user) } ``` ### 2. Formatted Variants (Custom Messages) ```go +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" +) + func TestFormatted(t *testing.T) { - // Add custom failure message with formatting - assert.Equalf(t, 42, result, "expected answer to be %d", 42) - require.NotNilf(t, user, "user %d should exist", userID) + // Add custom failure message with formatting + assert.Equalf(t, 42, result, "expected answer to be %d", 42) + require.NotNilf(t, user, "user %d should exist", userID) } ``` ### 3. Forward Methods (Cleaner Syntax) ```go +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" +) + func TestForward(t *testing.T) { - a := assert.New(t) - r := require.New(t) + a := assert.New(t) + r := require.New(t) - // No need to pass 't' each time - a.Equal(42, result) - a.NotEmpty(list) + // No need to pass 't' each time + a.Equal(42, result) + a.NotEmpty(list) - r.NotNil(user) - r.NoError(err) + r.NotNil(user) + r.NoError(err) } ``` ### 4. Forward Methods with Formatting ```go +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" +) + func TestForwardFormatted(t *testing.T) { - a := assert.New(t) + a := assert.New(t) - a.Equalf(42, result, "expected answer to be %d", 42) - a.Lenf(list, 3, "expected %d items", 3) + a.Equalf(42, result, "expected answer to be %d", 42) + a.Lenf(list, 3, "expected %d items", 3) } ``` @@ -216,30 +287,37 @@ func TestForwardFormatted(t *testing.T) { The idiomatic Go pattern for testing multiple cases should be: ```go -func TestAdd(t *testing.T) { - tests := slices.Values([]struct { - name string - a, b int - expected int - }{ - {"positive numbers", 2, 3, 5}, - {"negative numbers", -2, -3, -5}, - {"mixed signs", -2, 3, 1}, - {"with zero", 0, 5, 5}, - }) +import ( + "slices" + "testing" - for tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := Add(tt.a, tt.b) - assert.Equal(t, tt.expected, result) - }) - } + "gotest.tools/assert" +) + +func TestAdd(t *testing.T) { + tests := slices.Values([]struct { + name string + a, b int + expected int + }{ + {"positive numbers", 2, 3, 5}, + {"negative numbers", -2, -3, -5}, + {"mixed signs", -2, 3, 1}, + {"with zero", 0, 5, 5}, + }) + + for tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Add(tt.a, tt.b) + assert.Equal(t, tt.expected, result) + }) + } } ``` With forward methods for cleaner syntax: -```gon +```go func TestAdd(t *testing.T) { tests := slices.Values([]struct { name string @@ -270,81 +348,101 @@ func TestAdd(t *testing.T) { ```go import ( - "net/http" - "net/http/httptest" - "github.com/go-openapi/testify/v2/assert" - "github.com/go-openapi/testify/v2/require" + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" ) func TestUserHandler(t *testing.T) { - req := httptest.NewRequest("GET", "/users/123", nil) - w := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/users/123", nil) + w := httptest.NewRecorder() - handler := NewUserHandler() - handler.ServeHTTP(w, req) + handler := NewUserHandler() + handler.ServeHTTP(w, req) - require.Equal(t, http.StatusOK, w.Code) + require.Equal(t, http.StatusOK, w.Code) - // Check response body contains expected data - body := w.Body.String() - assert.Contains(t, body, `"name":"Alice"`) - assert.Contains(t, body, `"id":123`) + // Check response body contains expected data + body := w.Body.String() + assert.Contains(t, body, `"name":"Alice"`) + assert.Contains(t, body, `"id":123`) } ``` ### Testing JSON ```go +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" +) + func TestJSONResponse(t *testing.T) { - expected := `{"name":"Alice","age":25}` - actual := `{"age":25,"name":"Alice"}` + expected := `{"name":"Alice","age":25}` + actual := `{"age":25,"name":"Alice"}` - // JSONEq compares JSON semantically (ignores key order, whitespace) - assert.JSONEq(t, expected, actual) + // JSONEq compares JSON semantically (ignores key order, whitespace) + assert.JSONEq(t, expected, actual) } ``` ### Testing with Subtests ```go -func TestUserOperations(t *testing.T) { - user := &User{ID: 123, Name: "Alice"} - - t.Run("creation", func(t *testing.T) { - assert.NotNil(t, user) - assert.Equal(t, 123, user.ID) - }) +import ( + "testing" - t.Run("update", func(t *testing.T) { - user.Name = "Bob" - assert.Equal(t, "Bob", user.Name) - }) + "github.com/go-openapi/testify/v2/assert" +) - t.Run("deletion", func(t *testing.T) { - err := DeleteUser(user.ID) - assert.NoError(t, err) - }) +func TestUserOperations(t *testing.T) { + user := &User{ID: 123, Name: "Alice"} + + t.Run("creation", func(t *testing.T) { + assert.NotNil(t, user) + assert.Equal(t, 123, user.ID) + }) + + t.Run("update", func(t *testing.T) { + user.Name = "Bob" + assert.Equal(t, "Bob", user.Name) + }) + + t.Run("deletion", func(t *testing.T) { + err := DeleteUser(user.ID) + assert.NoError(t, err) + }) } ``` ### Testing Panics ```go -func TestPanics(t *testing.T) { - // Function should panic - assert.Panics(t, func() { - Divide(10, 0) - }) +import ( + "testing" - // Function should NOT panic - assert.NotPanics(t, func() { - Divide(10, 2) - }) + "github.com/go-openapi/testify/v2/assert" +) - // Function should panic with specific value - assert.PanicsWithValue(t, "division by zero", func() { - Divide(10, 0) - }) +func TestPanics(t *testing.T) { + // Function should panic + assert.Panics(t, func() { + Divide(10, 0) + }) + + // Function should NOT panic + assert.NotPanics(t, func() { + Divide(10, 2) + }) + + // Function should panic with specific value + assert.PanicsWithValue(t, "division by zero", func() { + Divide(10, 0) + }) } ``` @@ -355,56 +453,304 @@ func TestPanics(t *testing.T) { ### Setup and Teardown ```go +import ( + "testing" + + "gotest.tools/assert" + + "github.com/go-openapi/testify/v2/require" +) + func TestWithSetup(t *testing.T) { - // Setup - db := setupTestDatabase(t) - defer db.Close() // Teardown + // Setup + db := setupTestDatabase(t) + t.Cleanup(func() { + db.Close() // Teardown + }) - // Test - user := &User{Name: "Alice"} - err := db.Save(user) - require.NoError(t, err) + // Test + user := &User{Name: "Alice"} + err := db.Save(user) + require.NoError(t, err) - // Verify - loaded, err := db.Find(user.ID) - require.NoError(t, err) - assert.Equal(t, "Alice", loaded.Name) + // Verify + loaded, err := db.Find(user.ID) + require.NoError(t, err) + assert.Equal(t, "Alice", loaded.Name) } ``` ### Helper Functions ```go -func assertUserValid(t *testing.T, user *User) { - t.Helper() // Mark as helper for better error messages +import ( + "testing" - assert.NotNil(t, user) - assert.NotEmpty(t, user.Name) - assert.Greater(t, user.Age, 0) -} + "github.com/go-openapi/testify/v2/assert" +) func TestUsers(t *testing.T) { - user := GetUser(123) - assertUserValid(t, user) // Failures point to this line, not inside helper + user := GetUser(123) + assertUserValid(t, user) // Failures point to this line, not inside helper +} + +func assertUserValid(t *testing.T, user *User) { + t.Helper() // Mark as helper for better error messages + + assert.NotNil(t, user) + assert.NotEmpty(t, user.Name) + assert.Greater(t, user.Age, 0) } ``` ### Combining Multiple Assertions ```go +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" +) + func TestUserCompleteness(t *testing.T) { - a := assert.New(t) - user := GetUser(123) + a := assert.New(t) + user := GetUser(123) + + // Chain multiple checks cleanly + a.NotNil(user) + a.NotEmpty(user.Name) + a.NotEmpty(user.Email) + a.Greater(user.Age, 0) + a.True(user.Active) +} +``` + +### Asynchronous Testing + +Testify provides three assertions for testing asynchronous code: `Eventually`, `Never`, and `EventuallyWithT`. + +#### Eventually: Wait for a Condition to Become True + +Use `Eventually` when testing code that updates state asynchronously (background goroutines, event loops, caches). + +```go +import ( + "sync" + "testing" + "time" + + "github.com/go-openapi/testify/v2/assert" +) + +func TestBackgroundProcessor(t *testing.T) { + // Simulate a background processor that updates state + var processed bool + var mu sync.Mutex + + go func() { + time.Sleep(50 * time.Millisecond) + mu.Lock() + processed = true + mu.Unlock() + }() + + // Wait up to 200ms for the background task to complete, + // checking every 10ms + assert.Eventually(t, func() bool { + mu.Lock() + defer mu.Unlock() + return processed + }, 200*time.Millisecond, 10*time.Millisecond, + "background processor should have completed") +} - // Chain multiple checks cleanly - a.NotNil(user) - a.NotEmpty(user.Name) - a.NotEmpty(user.Email) - a.Greater(user.Age, 0) - a.True(user.Active) +// Real-world example: Testing cache warming +func TestCacheWarming(t *testing.T) { + cache := NewCache() + cache.StartWarmup() // Populates cache in background + + // Verify cache becomes ready within 5 seconds + assert.Eventually(t, func() bool { + return cache.IsReady() && cache.Size() > 0 + }, 5*time.Second, 100*time.Millisecond, + "cache should warm up and contain entries") +} +``` + +#### Never: Ensure a Condition Remains False + +Use `Never` to verify that something undesirable never happens during a time window (no data corruption, no invalid state). + +```go +import ( + "sync/atomic" + "testing" + "time" + + "github.com/go-openapi/testify/v2/assert" +) + +func TestNoDataCorruption(t *testing.T) { + var counter atomic.Int32 + stopChan := make(chan struct{}) + defer close(stopChan) + + // Start multiple goroutines incrementing safely + for i := 0; i < 10; i++ { + go func() { + ticker := time.NewTicker(5 * time.Millisecond) + defer ticker.Stop() + for { + select { + case <-stopChan: + return + case <-ticker.C: + counter.Add(1) + } + } + }() + } + + // Verify counter never goes negative (indicating corruption) + assert.Never(t, func() bool { + return counter.Load() < 0 + }, 500*time.Millisecond, 20*time.Millisecond, + "counter should never go negative") +} + +// Real-world example: Testing rate limiter doesn't exceed threshold +func TestRateLimiter(t *testing.T) { + limiter := NewRateLimiter(100) // 100 requests per second max + stopChan := make(chan struct{}) + defer close(stopChan) + + // Hammer the limiter with requests + for i := 0; i < 50; i++ { + go func() { + ticker := time.NewTicker(1 * time.Millisecond) + defer ticker.Stop() + for { + select { + case <-stopChan: + return + case <-ticker.C: + limiter.Allow() + } + } + }() + } + + // Verify we never exceed the rate limit over 2 seconds + assert.Never(t, func() bool { + return limiter.CurrentRate() > 120 // 20% tolerance + }, 2*time.Second, 50*time.Millisecond, + "rate limiter should never exceed threshold") } ``` +#### EventuallyWithT: Complex Async Assertions + +Use `EventuallyWithT` when you need multiple assertions to pass together. +The `CollectT` parameter lets you make regular assertions. + +```go +import ( + "testing" + "time" + + "github.com/go-openapi/testify/v2/assert" +) + +func TestAPIEventualConsistency(t *testing.T) { + // Simulate an eventually-consistent API + api := NewEventuallyConsistentAPI() + api.CreateUser("alice", "alice@example.com") + + // Wait for the user to be fully replicated across all shards + // All conditions must pass in the same tick + assert.EventuallyWithT(t, func(c *assert.CollectT) { + user, err := api.GetUser("alice") + + // All these assertions must pass together + assert.NoError(c, err, "user should be retrievable") + assert.NotNil(c, user, "user should exist") + assert.EqualT(c, "alice@example.com", user.Email, "email should match") + assert.True(c, user.Replicated, "user should be replicated") + assert.GreaterOrEqual(c, user.ReplicaCount, 3, "should be on at least 3 replicas") + }, 10*time.Second, 500*time.Millisecond, + "user should be eventually consistent across all replicas") +} + +// Real-world example: Testing distributed cache sync +func TestDistributedCacheSync(t *testing.T) { + primary := NewCacheNode("primary") + replica1 := NewCacheNode("replica1") + replica2 := NewCacheNode("replica2") + + // Connect nodes for replication + primary.AddReplica(replica1) + primary.AddReplica(replica2) + + // Write to primary + primary.Set("key", "value", 5*time.Minute) + + // Verify value propagates to all replicas with correct TTL + assert.EventuallyWithT(t, func(c *assert.CollectT) { + val1, ttl1, ok1 := replica1.Get("key") + val2, ttl2, ok2 := replica2.Get("key") + + // All replicas must have the value + assert.True(c, ok1, "replica1 should have the key") + assert.True(c, ok2, "replica2 should have the key") + + // Values must match + assert.EqualT(c, "value", val1, "replica1 value should match") + assert.EqualT(c, "value", val2, "replica2 value should match") + + // TTL should be approximately the same (within 1 second) + assert.InDelta(c, 5*time.Minute, ttl1, float64(time.Second), + "replica1 TTL should be close to original") + assert.InDelta(c, 5*time.Minute, ttl2, float64(time.Second), + "replica2 TTL should be close to original") + }, 5*time.Second, 100*time.Millisecond, + "cache value should replicate to all nodes with correct TTL") +} + +// Advanced: Using require in EventuallyWithT to fail fast +func TestEventuallyWithRequire(t *testing.T) { + api := NewAPI() + + assert.EventuallyWithT(t, func(c *assert.CollectT) { + resp, err := api.HealthCheck() + + // Use require to stop checking this tick if request fails + // This prevents nil pointer panics on subsequent assertions + assert.NoError(c, err, "health check should not error") + if err != nil { + return // Skip remaining checks this tick + } + + // Now safe to check response fields + assert.EqualT(c, "healthy", resp.Status) + assert.Greater(c, resp.Uptime, 0) + assert.NotEmpty(c, resp.Version) + }, 30*time.Second, 1*time.Second, + "API should become healthy") +} +``` + +**Key differences:** +- **Eventually**: Simple boolean condition, use for single checks +- **Never**: Opposite of Eventually, verifies condition stays false +- **EventuallyWithT**: Complex checks with multiple assertions, use when you need detailed failure messages + +**Best practices:** +1. Choose appropriate timeouts: long enough for async operations, short enough for fast test feedback +2. Choose appropriate tick intervals: frequent enough to catch state changes, infrequent enough to avoid overhead +3. Use `EventuallyWithT` when you need to understand *which* assertion failed +4. Use `Eventually` for simple boolean conditions (faster, simpler) +5. Use `Never` to verify invariants over time (no race conditions, no invalid state) + --- ## YAML Support (Optional) @@ -413,23 +759,24 @@ YAML assertions require explicit opt-in: ```go import ( - "testing" - "github.com/go-openapi/testify/v2/assert" - _ "github.com/go-openapi/testify/enable/yaml/v2" // Enable YAML support + "testing" + + _ "github.com/go-openapi/testify/enable/yaml/v2" // Enable YAML support + "github.com/go-openapi/testify/v2/assert" ) func TestYAML(t *testing.T) { - expected := ` + expected := ` name: Alice age: 25 ` - actual := ` + actual := ` age: 25 name: Alice ` - // YAMLEq compares YAML semantically - assert.YAMLEq(t, expected, actual) + // YAMLEq compares YAML semantically + assert.YAMLEq(t, expected, actual) } ``` @@ -445,13 +792,14 @@ Testify can colorize test failure output for better readability. This is an opt- ```go import ( - "testing" - "github.com/go-openapi/testify/v2/assert" - _ "github.com/go-openapi/testify/enable/colors/v2" // Enable colorized output + "testing" + + _ "github.com/go-openapi/testify/enable/colors/v2" // Enable colorized output + "github.com/go-openapi/testify/v2/assert" ) func TestExample(t *testing.T) { - assert.Equal(t, "expected", "actual") // Failure will be colorized + assert.Equal(t, "expected", "actual") // Failure will be colorized } ``` @@ -501,6 +849,8 @@ TESTIFY_COLORIZED=true TESTIFY_COLORIZED_NOTTY=true go test -v ./... **Note:** Without the `enable/colors` import, output remains uncolored (no panic, just no colors). +See [screenshot](./MIGRATION.md#optional-enable-colorized-output). + --- ## Best Practices @@ -520,25 +870,33 @@ TESTIFY_COLORIZED=true TESTIFY_COLORIZED_NOTTY=true go test -v ./... **Before (stdlib):** ```go +import "testing" + func TestOld(t *testing.T) { - result := Calculate(5) - if result != 10 { - t.Errorf("Expected 10, got %d", result) - } - if len(items) == 0 { - t.Error("Expected non-empty items") - } + result := Calculate(5) + if result != 10 { + t.Errorf("Expected 10, got %d", result) + } + if len(items) == 0 { + t.Error("Expected non-empty items") + } } ``` **After (testify):** ```go +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" +) + func TestNew(t *testing.T) { - a := assert.New(t) + a := assert.New(t) - result := Calculate(5) - a.Equal(10, result) - a.NotEmpty(items) + result := Calculate(5) + a.Equal(10, result) + a.NotEmpty(items) } ``` diff --git a/docs/doc-site/usage/GENERICS.md b/docs/doc-site/usage/GENERICS.md index f65306eea..a58515848 100644 --- a/docs/doc-site/usage/GENERICS.md +++ b/docs/doc-site/usage/GENERICS.md @@ -17,28 +17,36 @@ Generic assertions work exactly like their reflection-based counterparts, but wi {{< cards >}} {{% card title="Reflection-based" %}} ```go -import "github.com/go-openapi/testify/v2/assert" +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" +) func TestUser(t *testing.T) { - expected := 42 - actual := getUserAge() + expected := 42 + actual := getUserAge() - // Compiles, but type errors appear at runtime - assert.Equal(t, expected, actual) + // Compiles, but type errors appear at runtime + assert.Equal(t, expected, actual) } ``` {{% /card %}} {{% card title="Generic (Type-safe)" %}} ```go -import "github.com/go-openapi/testify/v2/assert" +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" +) func TestUser(t *testing.T) { - expected := 42 - actual := getUserAge() + expected := 42 + actual := getUserAge() - // Compiler checks types immediately - assert.EqualT(t, expected, actual) + // Compiler checks types immediately + assert.EqualT(t, expected, actual) } ``` {{% /card %}} @@ -51,15 +59,15 @@ func TestUser(t *testing.T) { 1. **Testing with known concrete types** - The most common case ```go assert.EqualT(t, 42, result) // int comparison - assert.GreaterT(t, count, 0) // numeric comparison - assert.ElementsMatchT(t, expected, actual) // slice comparison + assert.GreaterT(t, count, 0) // numeric comparison + assert.ElementsMatchT(t, expected, actual) // slice comparison ``` 2. **You want refactoring safety** - Compiler catches broken tests immediately ```go // If getUserIDs() changes from []int to []string, - // the compiler flags this line immediately - assert.ElementsMatchT(t, expectedIDs, getUserIDs()) + // the compiler flags this line immediately + assert.ElementsMatchT(t, expectedIDs, getUserIDs()) ``` 3. **IDE assistance matters** - Autocomplete suggests only correctly-typed variables @@ -77,20 +85,20 @@ func TestUser(t *testing.T) { 1. **Intentionally comparing different types** - Especially with `EqualValues` ```go // Comparing int and int64 for semantic equality - assert.EqualValues(t, int64(42), int32(42)) // ✓ Reflection handles this - assert.EqualT(t, int64(42), int32(42)) // ❌ Compiler error + assert.EqualValues(t, int64(42), int32(42)) // ✓ Reflection handles this + assert.EqualT(t, int64(42), int32(42)) // ❌ Compiler error ``` 2. **Working with heterogeneous collections** - `[]any` or `interface{}` slices ```go mixed := []any{1, "string", true} - assert.Contains(t, mixed, "string") // ✓ Reflection works + assert.Contains(t, mixed, "string") // ✓ Reflection works ``` 3. **Dynamic type scenarios** - Where compile-time type is unknown ```go var result interface{} = getResult() - assert.Equal(t, expected, result) // ✓ Reflection handles dynamic types + assert.Equal(t, expected, result) // ✓ Reflection handles dynamic types ``` 4. **Backward compatibility** - Existing test code using reflection-based assertions @@ -238,30 +246,43 @@ For a detailed documentation of all generic functions, see the [API Reference](. {{< cards >}} {{% card title="Type-Safe Collection Assertions" %}} ```go +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" +) + func TestUserPermissions(t *testing.T) { - user := getUser(123) + user := getUser(123) - expectedPerms := []string{"read", "write"} - actualPerms := user.Permissions + expectedPerms := []string{"read", "write"} + actualPerms := user.Permissions - // Compiler ensures both slices are []string - assert.ElementsMatchT(t, expectedPerms, actualPerms) + // Compiler ensures both slices are []string + assert.ElementsMatchT(t, expectedPerms, actualPerms) - // Check subset relationship - assert.SliceSubsetT(t, []string{"read"}, actualPerms) + // Check subset relationship + assert.SliceSubsetT(t, []string{"read"}, actualPerms) } ``` {{% /card %}} {{% card title="Iterator Support (Go 1.23+)" %}} ```go +import ( + "slices" + "testing" + + "github.com/go-openapi/testify/v2/assert" +) + func TestSequenceContains(t *testing.T) { - // iter.Seq[int] from Go 1.23 - numbers := slices.Values([]int{1, 2, 3, 4, 5}) + // iter.Seq[int] from Go 1.23 + numbers := slices.Values([]int{1, 2, 3, 4, 5}) - // Type-safe iterator checking - assert.SeqContainsT(t, numbers, 3) - assert.SeqNotContainsT(t, numbers, 99) + // Type-safe iterator checking + assert.SeqContainsT(t, numbers, 3) + assert.SeqNotContainsT(t, numbers, 99) } ``` {{% /card %}} @@ -272,29 +293,41 @@ func TestSequenceContains(t *testing.T) { {{< cards >}} {{% card title="Ordered Types" %}} ```go +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" +) + func TestPricing(t *testing.T) { - price := calculatePrice(item) - discount := calculateDiscount(item) + price := calculatePrice(item) + discount := calculateDiscount(item) - // Type-safe numeric comparisons - assert.PositiveT(t, price) - assert.GreaterT(t, price, discount) - assert.LessOrEqualT(t, discount, price) + // Type-safe numeric comparisons + assert.PositiveT(t, price) + assert.GreaterT(t, price, discount) + assert.LessOrEqualT(t, discount, price) } ``` {{% /card %}} {{% card title="Float Comparisons" %}} ```go +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" +) + func TestPhysicsCalculation(t *testing.T) { - result := calculateVelocity(mass, force) - expected := 42.0 + result := calculateVelocity(mass, force) + expected := 42.0 - // Type-safe float comparison with delta - assert.InDeltaT(t, expected, result, 1e-6) + // Type-safe float comparison with delta + assert.InDeltaT(t, expected, result, 1e-6) - // Or with epsilon (relative error) - assert.InEpsilonT(t, expected, result, 0.001) + // Or with epsilon (relative error) + assert.InEpsilonT(t, expected, result, 0.001) } ``` {{% /card %}} @@ -307,28 +340,40 @@ The `IsOfTypeT` function eliminates the need for dummy values: {{< cards >}} {{% card title="Old Way (Reflection)" %}} ```go +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" +) + func TestGetUser(t *testing.T) { - result := getUser(123) + result := getUser(123) - // Need to create a dummy User instance - assert.IsType(t, User{}, result) + // Need to create a dummy User instance + assert.IsType(t, User{}, result) - // Or use a pointer dummy - assert.IsType(t, (*User)(nil), result) + // Or use a pointer dummy + assert.IsType(t, (*User)(nil), result) } ``` {{% /card %}} {{% card title="New Way (Generic)" %}} ```go +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" +) + func TestGetUser(t *testing.T) { - result := getUser(123) + result := getUser(123) - // No dummy value needed! - assert.IsOfTypeT[User](t, result) + // No dummy value needed! + assert.IsOfTypeT[User](t, result) - // For pointer types - assert.IsOfTypeT[*User](t, result) + // For pointer types + assert.IsOfTypeT[*User](t, result) } ``` {{% /card %}} @@ -339,35 +384,47 @@ func TestGetUser(t *testing.T) { {{< cards >}} {{% card title="Ordering Checks" %}} ```go +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" +) + func TestSortedData(t *testing.T) { - timestamps := []int64{ - 1640000000, - 1640000100, - 1640000200, - } - - // Type-safe ordering assertions - assert.IsIncreasingT(t, timestamps) - assert.SortedT(t, timestamps) // Generic-only function + timestamps := []int64{ + 1640000000, + 1640000100, + 1640000200, + } + + // Type-safe ordering assertions + assert.IsIncreasingT(t, timestamps) + assert.SortedT(t, timestamps) // Generic-only function } ``` {{% /card %}} {{% card title="Custom Ordered Types" %}} ```go +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" +) + type Priority int const ( - Low Priority = iota - Medium - High + Low Priority = iota + Medium + High ) func TestPriorities(t *testing.T) { - tasks := []Priority{Low, Medium, High} + tasks := []Priority{Low, Medium, High} - // Works with Ordered types (custom types supported) - assert.IsNonDecreasingT(t, tasks) + // Works with Ordered types (custom types supported) + assert.IsNonDecreasingT(t, tasks) } ``` {{% /card %}} @@ -439,16 +496,22 @@ assert.Equal(t, int64(result), count) You don't need to migrate everything at once: ```go +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" +) + func TestMixedAssertions(t *testing.T) { - // Use generic where types are known - assert.EqualT(t, 42, getAge()) - assert.GreaterT(t, count, 0) + // Use generic where types are known + assert.EqualT(t, 42, getAge()) + assert.GreaterT(t, count, 0) - // Keep reflection for dynamic types - var result interface{} = getResult() - assert.Equal(t, expected, result) + // Keep reflection for dynamic types + var result interface{} = getResult() + assert.Equal(t, expected, result) - // Both styles coexist peacefully + // Both styles coexist peacefully } ``` @@ -479,7 +542,7 @@ See the complete [Performance Benchmarks](../../project/maintainers/BENCHMARKS.m 2. **Let the compiler guide you** - Type errors reveal design issues ```go // Compiler error reveals you're comparing wrong types - assert.EqualT(t, userID, orderID) // ❌ Good - catches mistake! + assert.EqualT(t, userID, orderID) // ❌ Good - catches mistake! ``` 3. **Use explicit types for clarity** @@ -490,9 +553,9 @@ See the complete [Performance Benchmarks](../../project/maintainers/BENCHMARKS.m 4. **Leverage performance wins in hot paths** - Generic assertions are faster ```go // Table-driven tests with many iterations - for _, tc := range testCases { - assert.EqualT(t, tc.expected, tc.actual) // ✓ Fast - } + for _, tc := range testCases { + assert.EqualT(t, tc.expected, tc.actual) // ✓ Fast + } ``` ### ❌ Don't @@ -500,25 +563,25 @@ See the complete [Performance Benchmarks](../../project/maintainers/BENCHMARKS.m 1. **Don't force generics for dynamic types** ```go var result interface{} = getResult() - assert.Equal(t, expected, result) // ✓ Reflection is fine here + assert.Equal(t, expected, result) // ✓ Reflection is fine here ``` 2. **Don't use reflection to avoid fixing types** ```go // Bad: Using reflection to bypass type safety - assert.Equal(t, expected, actual) // ✗ Defeats the purpose - - // Good: Fix the types or use EqualValues if intentional - assert.EqualT(t, expected, actual) // ✓ Type safe + assert.Equal(t, expected, actual) // ✗ Defeats the purpose + + // Good: Fix the types or use EqualValues if intentional + assert.EqualT(t, expected, actual) // ✓ Type safe ``` 3. **Don't create unnecessary type conversions** ```go // Bad: Unnecessary conversion - assert.EqualT(t, int64(42), int64(result)) - - // Good: Work with natural types - assert.EqualT(t, 42, result) + assert.EqualT(t, int64(42), int64(result)) + + // Good: Work with natural types + assert.EqualT(t, 42, result) ``` ## Type Constraints Reference diff --git a/docs/doc-site/usage/MIGRATION.md b/docs/doc-site/usage/MIGRATION.md index 201353094..ad241a125 100644 --- a/docs/doc-site/usage/MIGRATION.md +++ b/docs/doc-site/usage/MIGRATION.md @@ -10,12 +10,8 @@ weight: 20 ```go // Old -import "github.com/stretchr/testify/assert" -import "github.com/stretchr/testify/require" // New -import "github.com/go-openapi/testify/v2/assert" -import "github.com/go-openapi/testify/v2/require" ``` ### 2. Optional: Enable YAML Support diff --git a/docs/doc-site/usage/TUTORIAL.md b/docs/doc-site/usage/TUTORIAL.md index ed16f2fcc..1b1fb339a 100644 --- a/docs/doc-site/usage/TUTORIAL.md +++ b/docs/doc-site/usage/TUTORIAL.md @@ -22,12 +22,19 @@ A good test is: **With testify, you write tests that read like documentation:** ```go +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" +) + func TestUserCreation(t *testing.T) { - user := CreateUser("alice@example.com") + user := CreateUser("alice@example.com") - require.NotNil(t, user) - assert.Equal(t, "alice@example.com", user.Email) // if user is nil, will fail and stop before - assert.True(t, user.Active) + require.NotNil(t, user) + assert.Equal(t, "alice@example.com", user.Email) // if user is nil, will fail and stop before + assert.True(t, user.Active) } ``` {{% notice style="tip" title="tip" icon="meteor" %}} @@ -60,25 +67,38 @@ The assertions are self-documenting - you can read the test and immediately unde Oftentimes, much of the test logic can be replaced by a proper use of `require`. ```go +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" +) + // ❌ Don't do this - repetitive and hard to maintain func TestUserCreation(t *testing.T) { - user := CreateUser("alice@example.com") + user := CreateUser("alice@example.com") - if assert.NotNil(t, user) { - assert.Equal(t, "alice@example.com", user.Email) // if user is nil, will skip this test - assert.True(t, user.Active) - } + if assert.NotNil(t, user) { + assert.Equal(t, "alice@example.com", user.Email) // if user is nil, will skip this test + assert.True(t, user.Active) + } } ``` ```go +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" +) + // ✅ Better - linear flow, no indented subcases func TestUserCreation(t *testing.T) { - user := CreateUser("alice@example.com") + user := CreateUser("alice@example.com") - require.NotNil(t, user) - assert.Equal(t, "alice@example.com", user.Email) // if user is nil, will fail and stop before - assert.True(t, user.Active) + require.NotNil(t, user) + assert.Equal(t, "alice@example.com", user.Email) // if user is nil, will fail and stop before + assert.True(t, user.Active) } ``` @@ -91,20 +111,26 @@ The **iterator pattern** is the idiomatic way to write table-driven tests in Go Instead of writing separate test functions for each case: ```go +import ( + "testing" + + "gotest.tools/assert" +) + // ❌ Don't do this - repetitive and hard to maintain func TestAdd_PositiveNumbers(t *testing.T) { - result := Add(2, 3) - assert.Equal(t, 5, result) + result := Add(2, 3) + assert.Equal(t, 5, result) } func TestAdd_NegativeNumbers(t *testing.T) { - result := Add(-2, -3) - assert.Equal(t, -5, result) + result := Add(-2, -3) + assert.Equal(t, -5, result) } func TestAdd_MixedSigns(t *testing.T) { - result := Add(-2, 3) - assert.Equal(t, 1, result) + result := Add(-2, 3) + assert.Equal(t, 1, result) } ``` @@ -137,61 +163,62 @@ func addTestCases() iter.Seq[addTestCase] { ```go import ( - "iter" - "slices" - "testing" - "github.com/go-openapi/testify/v2/assert" + "iter" + "slices" + "testing" + + "github.com/go-openapi/testify/v2/assert" ) // 1. Define a test case struct type addTestCase struct { - name string - a, b int - expected int + name string + a, b int + expected int } // 2. Create an iterator function returning iter.Seq[T] func addTestCases() iter.Seq[addTestCase] { - return slices.Values([]addTestCase{ - { - name: "positive numbers", - a: 2, - b: 3, - expected: 5, - }, - { - name: "negative numbers", - a: -2, - b: -3, - expected: -5, - }, - { - name: "mixed signs", - a: -2, - b: 3, - expected: 1, - }, - { - name: "with zero", - a: 0, - b: 5, - expected: 5, - }, - }) + return slices.Values([]addTestCase{ + { + name: "positive numbers", + a: 2, + b: 3, + expected: 5, + }, + { + name: "negative numbers", + a: -2, + b: -3, + expected: -5, + }, + { + name: "mixed signs", + a: -2, + b: 3, + expected: 1, + }, + { + name: "with zero", + a: 0, + b: 5, + expected: 5, + }, + }) } // 3. Test function iterates over cases using range func TestAdd(t *testing.T) { - t.Parallel() + t.Parallel() - for c := range addTestCases() { - t.Run(c.name, func(t *testing.T) { - t.Parallel() + for c := range addTestCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() - result := Add(c.a, c.b) - assert.Equal(t, c.expected, result) - }) - } + result := Add(c.a, c.b) + assert.Equal(t, c.expected, result) + }) + } } ``` @@ -225,56 +252,68 @@ func TestAdd(t *testing.T) { **Traditional inline pattern:** ```go -func TestAdd(t *testing.T) { - tests := []struct { - name string - a, b int - expected int - }{ - {"positive", 2, 3, 5}, - {"negative", -2, -3, -5}, - // Test data mixed with test function - // Hard to reuse - // No named fields - order matters - } +import ( + "testing" - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := Add(tt.a, tt.b) - assert.Equal(t, tt.expected, result) - }) - } + "gotest.tools/assert" +) + +func TestAdd(t *testing.T) { + tests := []struct { + name string + a, b int + expected int + }{ + {"positive", 2, 3, 5}, + {"negative", -2, -3, -5}, + // Test data mixed with test function + // Hard to reuse + // No named fields - order matters + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Add(tt.a, tt.b) + assert.Equal(t, tt.expected, result) + }) + } } ``` **Iterator pattern:** ```go +import ( + "iter" + "slices" + "testing" +) + // Test logic separate and clean func TestAdd(t *testing.T) { - t.Parallel() - for c := range addTestCases() { // Clean iteration - // ... - } + t.Parallel() + for c := range addTestCases() { // Clean iteration + // ... + } } type addTestCase struct { - name string - a, b int - expected int + name string + a, b int + expected int } // Test data in separate function - clean, reusable func addTestCases() iter.Seq[addTestCase] { - return slices.Values([]addTestCase{ - { - name: "positive numbers", // Named fields - a: 2, // Self-documenting - b: 3, - expected: 5, - }, - // More cases... - }) + return slices.Values([]addTestCase{ + { + name: "positive numbers", // Named fields + a: 2, // Self-documenting + b: 3, + expected: 5, + }, + // More cases... + }) } ``` @@ -289,63 +328,71 @@ func addTestCases() iter.Seq[addTestCase] { **Example - complex setup:** ```go -func TestUserValidation(t *testing.T) { - t.Parallel() - - for c := range userValidationCases() { - t.Run(c.name, func(t *testing.T) { - t.Parallel() +import ( + "iter" + "slices" + "testing" - err := ValidateUser(c.user) + "github.com/go-openapi/testify/v2/assert" +) - if c.shouldErr { - assert.Error(t, err) - assert.ErrorContains(t, err, c.errMsg) - } else { - assert.NoError(t, err) - } - }) - } +func TestUserValidation(t *testing.T) { + t.Parallel() + + for c := range userValidationCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + err := ValidateUser(c.user) + + if c.shouldErr { + assert.Error(t, err) + assert.ErrorContains(t, err, c.errMsg) + } else { + assert.NoError(t, err) + } + }) + } } type userValidationCase struct { - name string - user User - shouldErr bool - errMsg string + name string + user User + shouldErr bool + errMsg string } func userValidationCases() iter.Seq[userValidationCase] { - return slices.Values([]userValidationCase{ - { - name: "valid user", - user: User{ - Name: "Alice", - Email: "alice@example.com", - Age: 25, - }, - shouldErr: false, - }, - { - name: "missing email", - user: User{ - Name: "Bob", - Age: 30, - }, - shouldErr: true, - errMsg: "email is required", - }, - { - name: "invalid age", - user: User{ - Name: "Charlie", - Email: "charlie@example.com", - Age: -5, - }, - shouldErr: true, - errMsg: "age must be positive", - }, - }) + return slices.Values([]userValidationCase{ + { + name: "valid user", + user: User{ + Name: "Alice", + Email: "alice@example.com", + Age: 25, + }, + shouldErr: false, + }, + { + name: "missing email", + user: User{ + Name: "Bob", + Age: 30, + }, + shouldErr: true, + errMsg: "email is required", + }, + { + name: "invalid age", + user: User{ + Name: "Charlie", + Email: "charlie@example.com", + Age: -5, + }, + shouldErr: true, + errMsg: "age must be positive", + }, + }) } ``` @@ -356,22 +403,28 @@ func userValidationCases() iter.Seq[userValidationCase] { The iterator pattern works beautifully with testify's forward methods: ```go +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" +) + func TestUserOperations(t *testing.T) { - t.Parallel() + t.Parallel() - for c := range userOperationCases() { - t.Run(c.name, func(t *testing.T) { - t.Parallel() - a := assert.New(t) // Forward assertion object + for c := range userOperationCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + a := assert.New(t) // Forward assertion object - user := PerformOperation(c.input) + user := PerformOperation(c.input) - // Clean assertions without repeating 't' - a.NotNil(user) - a.Equal(c.expectedName, user.Name) - a.Greater(user.ID, 0) - }) - } + // Clean assertions without repeating 't' + a.NotNil(user) + a.Equal(c.expectedName, user.Name) + a.Greater(user.ID, 0) + }) + } } ``` @@ -382,20 +435,26 @@ func TestUserOperations(t *testing.T) { When extracting common assertions into helper functions, use `t.Helper()` to get better error messages: ```go +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" +) + func assertUserValid(t *testing.T, user *User) { - t.Helper() // Makes test failures point to the caller + t.Helper() // Makes test failures point to the caller - assert.NotNil(t, user) - assert.NotEmpty(t, user.Name) - assert.NotEmpty(t, user.Email) - assert.Greater(t, user.Age, 0) + assert.NotNil(t, user) + assert.NotEmpty(t, user.Name) + assert.NotEmpty(t, user.Email) + assert.Greater(t, user.Age, 0) } func TestUserCreation(t *testing.T) { - user := CreateUser("alice@example.com") + user := CreateUser("alice@example.com") - // If this fails, error points HERE, not inside assertUserValid - assertUserValid(t, user) + // If this fails, error points HERE, not inside assertUserValid + assertUserValid(t, user) } ``` @@ -408,17 +467,23 @@ Without `t.Helper()`, failures would show the line number inside `assertUserVali Always use `t.Parallel()` unless you have a specific reason not to: ```go +import ( + "testing" + + "gotest.tools/assert" +) + func TestAdd(t *testing.T) { - t.Parallel() // Outer test runs in parallel + t.Parallel() // Outer test runs in parallel - for c := range addTestCases() { - t.Run(c.name, func(t *testing.T) { - t.Parallel() // Each subtest runs in parallel + for c := range addTestCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() // Each subtest runs in parallel - result := Add(c.a, c.b) - assert.Equal(t, c.expected, result) - }) - } + result := Add(c.a, c.b) + assert.Equal(t, c.expected, result) + }) + } } ``` @@ -554,70 +619,71 @@ Here's a complete example showing all patterns together: package calculator_test import ( - "iter" - "slices" - "testing" - "github.com/go-openapi/testify/v2/assert" - "github.com/go-openapi/testify/v2/require" + "iter" + "slices" + "testing" + + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" ) type divideTestCase struct { - name string - a, b float64 - expected float64 - shouldErr bool + name string + a, b float64 + expected float64 + shouldErr bool } func divideTestCases() iter.Seq[divideTestCase] { - return slices.Values([]divideTestCase{ - { - name: "positive numbers", - a: 10, - b: 2, - expected: 5, - shouldErr: false, - }, - { - name: "negative dividend", - a: -10, - b: 2, - expected: -5, - shouldErr: false, - }, - { - name: "division by zero", - a: 10, - b: 0, - shouldErr: true, - }, - { - name: "zero dividend", - a: 0, - b: 5, - expected: 0, - shouldErr: false, - }, - }) + return slices.Values([]divideTestCase{ + { + name: "positive numbers", + a: 10, + b: 2, + expected: 5, + shouldErr: false, + }, + { + name: "negative dividend", + a: -10, + b: 2, + expected: -5, + shouldErr: false, + }, + { + name: "division by zero", + a: 10, + b: 0, + shouldErr: true, + }, + { + name: "zero dividend", + a: 0, + b: 5, + expected: 0, + shouldErr: false, + }, + }) } func TestDivide(t *testing.T) { - t.Parallel() - - for c := range divideTestCases() { - t.Run(c.name, func(t *testing.T) { - t.Parallel() - - result, err := Divide(c.a, c.b) - - if c.shouldErr { - assert.Error(t, err) - assert.ErrorIs(t, err, ErrDivisionByZero) - } else { - require.NoError(t, err) - assert.Equal(t, c.expected, result) - } - }) - } + t.Parallel() + + for c := range divideTestCases() { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + result, err := Divide(c.a, c.b) + + if c.shouldErr { + assert.Error(t, err) + assert.ErrorIs(t, err, ErrDivisionByZero) + } else { + require.NoError(t, err) + assert.Equal(t, c.expected, result) + } + }) + } } ``` diff --git a/docs/doc-site/usage/USAGE.md b/docs/doc-site/usage/USAGE.md index d852e980c..16a8e79b9 100644 --- a/docs/doc-site/usage/USAGE.md +++ b/docs/doc-site/usage/USAGE.md @@ -11,6 +11,47 @@ weight: 1 Testify v2 provides **over 40 core assertion types** (76+ functions including inverse variants and all naming styles) organized into clear domains. This guide explains how to navigate the API and use the naming conventions effectively. +## How the API is Organized + +Assertions are grouped by domain for easier discovery: + +| Domain | Examples | Count | +|--------|----------|-------| +| **Boolean** | `True`, `False` | 2 | +| **Equality** | `Equal`, `NotEqual`, `EqualValues`, `Same`, `Exactly` | 8 | +| **Comparison** | `Greater`, `Less`, `Positive` | 8 | +| **Collection** | `Contains`, `Len`, `Empty`, `ElementsMatch` | 18 | +| **Error** | `Error`, `NoError`, `ErrorIs`, `ErrorAs`, `ErrorContains` | 8 | +| **Type** | `IsType`, `Implements`, `Zero` | 7 | +| **String** | `Regexp`, `NotRegexp` | 4 | +| **Numeric** | `InDelta`, `InEpsilon` | 6 | +| **Ordering** | `IsIncreasing`, `Sorted` | 8 | +| **Panic** | `Panics`, `NotPanics` | 4 | +| **Others** | HTTP, JSON, YAML, Time, File assertions | 12 | + +{{% notice style="info" title="Browse by Domain" icon="book" %}} +See the complete [API Reference](../../api/_index.md) organized by domain for a detailed documentation of all assertions. +{{% /notice %}} + +## Navigating the Documentation + +### Quick Reference + +- **[Examples](../examples)** - Practical code examples for common testing scenarios +- **[API Reference](../../api/_index.md)** - Complete assertion catalog organized by domain +- **[Generics Guide](../GENERICS.md)** - Using type-safe assertions with the `T` suffix +- **[Changes](../CHANGES.md)** - All changes since fork from stretchr/testify +- **[pkg.go.dev](https://pkg.go.dev/github.com/go-openapi/testify/v2)** - godoc API reference with full signatures + +### Finding the Right Assertion + +1. Browse the [API Reference](../../api/_index.md) by domain (e.g., "Collection" for slice operations) +2. Search in the [API Reference](../../api/_index.md) (use search box) +3. Check (or search) the provided [Examples](../examples) for practical usage patterns +4. Check [pkg.go.dev](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert) for alphabetical listing +5. Use your editor's Go to Definition on any assertion +6. Use your IDE's autocomplete - type `assert.` and explore + ## API Conventions Understanding the naming patterns helps you find and use the right assertions quickly. @@ -22,14 +63,18 @@ Understanding the naming patterns helps you find and use the right assertions qu **Use when**: Tests should continue after failures to gather more context ```go -import "github.com/go-openapi/testify/v2/assert" +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" +) func TestUser(t *testing.T) { - user := getUser() + user := getUser() - assert.NotNil(t, user) // ✓ Returns false - assert.Equal(t, "Alice", user.Name) // Still runs - assert.True(t, user.Active) // Still runs + assert.NotNil(t, user) // ✓ Returns false + assert.Equal(t, "Alice", user.Name) // Still runs + assert.True(t, user.Active) // Still runs } ``` @@ -40,14 +85,18 @@ func TestUser(t *testing.T) { **Use when**: Test cannot continue meaningfully after failure ```go -import "github.com/go-openapi/testify/v2/require" +import ( + "testing" + + "github.com/go-openapi/testify/v2/require" +) func TestUser(t *testing.T) { - user := getUser() + user := getUser() - require.NotNil(t, user) // ✓ Calls t.FailNow() if fails - require.Equal(t, "Alice", user.Name) // Safe to proceed - require.True(t, user.Active) // user is guaranteed non-nil + require.NotNil(t, user) // ✓ Calls t.FailNow() if fails + require.Equal(t, "Alice", user.Name) // Safe to proceed + require.True(t, user.Active) // user is guaranteed non-nil } ``` @@ -218,33 +267,45 @@ When unsure about argument order: - Consult [pkg.go.dev](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert) for complete documentation {{% /notice %}} -### Forward Methods (Chaining Style) +### Forward Methods -Create an `Assertions` object to reduce repetition in tests with many assertions: +Create an `Assertion` object to reduce repetition in tests with many assertions: {{< cards >}} {{% card title="Package-Level Functions" %}} ```go +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" +) + func TestUser(t *testing.T) { - user := getUser() + user := getUser() - assert.NotNil(t, user) - assert.Equal(t, "Alice", user.Name) - assert.True(t, user.Active) - assert.Greater(t, user.Age, 0) + assert.NotNil(t, user) + assert.Equal(t, "Alice", user.Name) + assert.True(t, user.Active) + assert.Greater(t, user.Age, 0) } ``` {{% /card %}} {{% card title="Forward Methods" %}} ```go +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" +) + func TestUser(t *testing.T) { - a := assert.New(t) // Create once - user := getUser() + a := assert.New(t) // Create once + user := getUser() - a.NotNil(user) // No 't' needed - a.Equal("Alice", user.Name) - a.True(user.Active) + a.NotNil(user) // No 't' needed + a.Equal("Alice", user.Name) + a.True(user.Active) } ``` {{% /card %}} @@ -255,50 +316,6 @@ func TestUser(t *testing.T) { **⚠️ Generic assertions are not available as forward methods** (this is a limitation of go generics). -## How the API is Organized - -Assertions are grouped by domain for easier discovery: - -| Domain | Examples | Count | -|--------|----------|-------| -| **Boolean** | `True`, `False` | 2 | -| **Equality** | `Equal`, `NotEqual`, `EqualValues`, `Same`, `Exactly` | 8 | -| **Comparison** | `Greater`, `Less`, `Positive` | 8 | -| **Collection** | `Contains`, `Len`, `Empty`, `ElementsMatch` | 18 | -| **Error** | `Error`, `NoError`, `ErrorIs`, `ErrorAs`, `ErrorContains` | 8 | -| **Type** | `IsType`, `Implements`, `Zero` | 7 | -| **String** | `Regexp`, `NotRegexp` | 4 | -| **Numeric** | `InDelta`, `InEpsilon` | 6 | -| **Ordering** | `IsIncreasing`, `Sorted` | 8 | -| **Panic** | `Panics`, `NotPanics` | 4 | -| **Others** | HTTP, JSON, YAML, Time, File assertions | 12 | - -{{% notice style="info" title="Browse by Domain" icon="book" %}} -See the complete [API Reference](../../api/_index.md) organized by domain for detailed documentation of all assertions. -{{% /notice %}} - -## Navigating the Documentation - -### Quick Reference - -- **[Examples](../examples)** - Practical code examples for common testing scenarios -- **[API Reference](../../api/_index.md)** - Complete assertion catalog organized by domain -- **[Generics Guide](../GENERICS.md)** - Using type-safe assertions with the `T` suffix -- **[Changes](../CHANGES.md)** - All changes since fork from stretchr/testify -- **[pkg.go.dev](https://pkg.go.dev/github.com/go-openapi/testify/v2)** - Generated API reference with full signatures - -### Finding the Right Assertion - -**By task:** -1. Browse the [API Reference](../../api/_index.md) by domain (e.g., "Collection" for slice operations) -2. Check [Examples](../examples) for practical usage patterns -3. Use your IDE's autocomplete - type `assert.` and explore - -**By name:** -- Search in the [API Reference](../../api/_index.md) (use search box) -- Check [pkg.go.dev](https://pkg.go.dev/github.com/go-openapi/testify/v2/assert) for alphabetical listing -- Use your editor's Go to Definition on any assertion - ## Common Usage Patterns {{% tabs %}} @@ -307,23 +324,30 @@ See the complete [API Reference](../../api/_index.md) organized by domain for de **Pattern 1: Table-Driven Tests** ```go +import ( + "slices" + "testing" + + "gotest.tools/assert" +) + func TestCalculation(t *testing.T) { - tests := slices.Values([]struct { - name string - input int - expected int - }{ - {"positive", 5, 25}, - {"negative", -3, 9}, - {"zero", 0, 0}, - }) - - for tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := square(tt.input) - assert.Equal(t, tt.expected, result) - }) - } + tests := slices.Values([]struct { + name string + input int + expected int + }{ + {"positive", 5, 25}, + {"negative", -3, 9}, + {"zero", 0, 0}, + }) + + for tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := square(tt.input) + assert.Equal(t, tt.expected, result) + }) + } } ``` {{% /tab %}} @@ -333,14 +357,20 @@ func TestCalculation(t *testing.T) { **Pattern 2: Multiple Assertions (assert for context)** ```go +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" +) + func TestUserValidation(t *testing.T) { - user := createUser() + user := createUser() - // Use assert to see all failures - assert.NotEmpty(t, user.Name) // Check name - assert.NotEmpty(t, user.Email) // Check email - assert.Greater(t, user.Age, 0) // Check age - // All assertions run - see complete picture + // Use assert to see all failures + assert.NotEmpty(t, user.Name) // Check name + assert.NotEmpty(t, user.Email) // Check email + assert.Greater(t, user.Age, 0) // Check age + // All assertions run - see complete picture } ``` {{% /tab %}} @@ -349,15 +379,22 @@ func TestUserValidation(t *testing.T) { **Pattern 3: Early Exit (use require for prerequisites)** ```go +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" +) + func TestDatabaseQuery(t *testing.T) { - db := connectDB() - require.NotNil(t, db) // Stop if no connection + db := connectDB() + require.NotNil(t, db) // Stop if no connection - result := db.Query("SELECT * FROM users") - require.NoError(t, result.Error) // Stop if query fails + result := db.Query("SELECT * FROM users") + require.NoError(t, result.Error) // Stop if query fails - // Safe to proceed - db and result are valid - assert.NotEmpty(t, result.Rows) + // Safe to proceed - db and result are valid + assert.NotEmpty(t, result.Rows) } ``` {{% /tab %}} @@ -366,16 +403,22 @@ func TestDatabaseQuery(t *testing.T) { **Pattern 4: Type-Safe Generics** ```go +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" +) + func TestTypeSafety(t *testing.T) { - expected := []int{1, 2, 3} - actual := getNumbers() + expected := []int{1, 2, 3} + actual := getNumbers() - // Compiler checks types at compile time - assert.ElementsMatchT(t, expected, actual) - assert.GreaterT(t, len(actual), 0) + // Compiler checks types at compile time + assert.ElementsMatchT(t, expected, actual) + assert.GreaterT(t, len(actual), 0) - // If getNumbers() changes return type, - // compiler catches it immediately + // If getNumbers() changes return type, + // compiler catches it immediately } ``` {{% /tab %}} @@ -385,9 +428,7 @@ func TestTypeSafety(t *testing.T) { 1. **Import the package:** ```go - import "github.com/go-openapi/testify/v2/assert" // or - import "github.com/go-openapi/testify/v2/require" ``` 2. **Choose your style:** @@ -429,6 +470,134 @@ func TestTypeSafety(t *testing.T) { --- +## Customization + +### Using a Custom YAML Unmarshaler + +By default, testify uses `gopkg.in/yaml.v3` for YAML assertions (e.g. `YAMLEq`) when you import the standard +`enable/yaml/v2` package. + +However, you can register a custom YAML unmarshaler to use alternative libraries like +[goccy/go-yaml](https://github.com/goccy/go-yaml), either because you need additional features such as colored error +messages or better performance. + +#### How It Works + +The YAML support in testify works through a registration mechanism: + +1. `internal/assertions/yaml.go` calls `yaml.Unmarshal()` - an abstraction layer +2. The abstraction layer panics if no unmarshaler is registered +3. The `enable/yaml/v2` package registers `gopkg.in/yaml.v3` via `init()` when imported (e.g. on blank import) +4. You can register a custom unmarshaler using `enable/stubs/yaml.EnableYAMLWithUnmarshal()` + +#### Example: Using goccy/go-yaml + +Create a custom enable package in your test code: + +```go +package testutil + +import ( + goccyyaml "github.com/goccy/go-yaml" + yamlstub "github.com/go-openapi/testify/v2/enable/stubs/yaml" +) + +func init() { + // Register goccy/go-yaml as the YAML unmarshaler + yamlstub.EnableYAMLWithUnmarshal(goccyyaml.Unmarshal) +} +``` + +Then import your custom enable package in your tests: + +```go +// File: mypackage/user_test.go +package mypackage + +import ( + "testing" + + _ "yourmodule/internal/testutil" // Register goccy/go-yaml + + "github.com/go-openapi/testify/v2/assert" +) + +func TestUserYAML(t *testing.T) { + expected := ` +name: Alice +email: alice@example.com +age: 30 +` + actual := serializeUser(getUser()) + + // Now uses goccy/go-yaml under the hood + assert.YAMLEq(t, expected, actual) +} +``` + +#### Why Use a Custom YAML Library? + +Different YAML libraries offer different trade-offs: + +**`gopkg.in/yaml.v3` (default):** +- De factor standard library for Go YAML +- Widely used and well-tested +- Complete YAML 1.2 support + +**`github.com/goccy/go-yaml`:** +- Better performance (up to 2-3x faster) +- Colored error messages for debugging +- Better error reporting with line/column numbers +- JSON-like syntax support +- Comment preservation (useful for config testing) + +#### Important Notes + +1. **Register once:** Call `EnableYAMLWithUnmarshal()` only once, typically in an `init()` function +2. **Not concurrent-safe:** The registration is global and should happen during package or main program initialization +3. **Signature compatibility:** The custom unmarshaler must match the signature `func([]byte, any) error` +4. **No mixing:** Don't import both `github.com/go-openapi/testify/enable/yaml/v2` and your custom enable package - choose one + +#### Advanced: Wrapping Unmarshalers + +You can also wrap an unmarshaler to add custom behavior: + +```go +package testutil + +import ( + "fmt" + "log" + + goccyyaml "github.com/goccy/go-yaml" + + yamlstub "github.com/go-openapi/testify/v2/enable/stubs/yaml" +) + +func init() { + // Wrap the unmarshaler to add logging or validation + yamlstub.EnableYAMLWithUnmarshal(func(data []byte, v any) error { + // Custom pre-processing + if len(data) == 0 { + return fmt.Errorf("empty YAML document") + } + + // Call the actual unmarshaler + err := goccyyaml.Unmarshal(data, v) + // Custom post-processing + if err != nil { + log.Printf("YAML unmarshal error: %v", err) + } + + return err + }) +} +``` + +This pattern allows you to add logging, validation, or transformation logic around any YAML library. + +--- + ## See Also - [Examples](./EXAMPLES.md) - Practical code examples for common testing scenarios diff --git a/internal/testintegration/README.md b/internal/testintegration/README.md index 6f18821a8..13501fae6 100644 --- a/internal/testintegration/README.md +++ b/internal/testintegration/README.md @@ -169,9 +169,15 @@ Focused testing of known problematic patterns using the edge case generator. Go native fuzzing integrated with rapid: ```go +import ( + "testing" + + "pgregory.net/rapid" +) + func FuzzSdump(f *testing.F) { - prop := NoPanicProp(f.Context(), Generator(WithSkipCircularMap())) - f.Fuzz(rapid.MakeFuzz(prop)) + prop := NoPanicProp(f.Context(), Generator(WithSkipCircularMap())) + f.Fuzz(rapid.MakeFuzz(prop)) } ``` @@ -214,31 +220,33 @@ Example structure: package mypackage import ( - "context" - "testing" - "pgregory.net/rapid" + "context" + "testing" + "time" + + "pgregory.net/rapid" ) func TestMyFunction(t *testing.T) { - rapid.Check(t, func(rt *rapid.T) { - input := myGenerator().Draw(rt, "input") - - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - - done := make(chan struct{}) - go func() { - defer close(done) - _ = mypackage.MyFunction(input) - }() - - select { - case <-done: - // success - case <-ctx.Done(): - rt.Fatal("function timed out") - } - }) + rapid.Check(t, func(rt *rapid.T) { + input := myGenerator().Draw(rt, "input") + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + done := make(chan struct{}) + go func() { + defer close(done) + _ = mypackage.MyFunction(input) + }() + + select { + case <-done: + // success + case <-ctx.Done(): + rt.Fatal("function timed out") + } + }) } ```