// Copyright (c) 2023 - 2025 IBM Corp. // All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package assert provides functional assertion helpers for testing. // // This package wraps testify/assert functions in a Reader monad pattern, // allowing for composable and functional test assertions. Each assertion // returns a Reader that takes a *testing.T and performs the assertion. // // # Data Last Principle // // This package follows the "data last" functional programming principle, where // the data being operated on comes as the last parameter in a chain of function // applications. This design enables several powerful functional programming patterns: // // 1. **Partial Application**: You can create reusable assertion functions by providing // configuration parameters first, leaving the data and testing context for later. // // 2. **Function Composition**: Assertions can be composed and combined before being // applied to actual data. // // 3. **Point-Free Style**: You can pass assertion functions around without immediately // providing the data they operate on. // // The general pattern is: // // assert.Function(config)(data)(testingContext) // ↑ ↑ ↑ // expected actual *testing.T (always last) // // For single-parameter assertions: // // assert.Function(data)(testingContext) // ↑ ↑ // actual *testing.T (always last) // // Examples of "data last" in action: // // // Multi-parameter: expected value → actual value → testing context // assert.Equal(42)(result)(t) // assert.ArrayContains(3)(numbers)(t) // // // Single-parameter: data → testing context // assert.NoError(err)(t) // assert.ArrayNotEmpty(arr)(t) // // // Partial application - create reusable assertions // isPositive := assert.That(func(n int) bool { return n > 0 }) // // Later, apply to different values: // isPositive(42)(t) // Passes // isPositive(-5)(t) // Fails // // // Composition - combine assertions before applying data // validateUser := func(u User) assert.Reader { // return assert.AllOf([]assert.Reader{ // assert.Equal("Alice")(u.Name), // assert.That(func(age int) bool { return age >= 18 })(u.Age), // }) // } // validateUser(user)(t) // // The package supports: // - Equality and inequality assertions // - Collection assertions (arrays, maps, strings) // - Error handling assertions // - Result type assertions // - Custom predicate assertions // - Composable test suites // // Example: // // func TestExample(t *testing.T) { // value := 42 // assert.Equal(42)(value)(t) // Curried style // // // Composing multiple assertions // arr := []int{1, 2, 3} // assertions := assert.AllOf([]assert.Reader{ // assert.ArrayNotEmpty(arr), // assert.ArrayLength[int](3)(arr), // assert.ArrayContains(2)(arr), // }) // assertions(t) // } package assert import ( "fmt" "testing" "github.com/IBM/fp-go/v2/boolean" "github.com/IBM/fp-go/v2/eq" "github.com/IBM/fp-go/v2/option" "github.com/IBM/fp-go/v2/reader" "github.com/IBM/fp-go/v2/result" "github.com/stretchr/testify/assert" ) var ( // Eq is the equal predicate checking if objects are equal Eq = eq.FromEquals(assert.ObjectsAreEqual) ) // wrap1 is an internal helper function that wraps testify assertion functions // into the Reader monad pattern with curried parameters. // // It takes a testify assertion function and converts it into a curried function // that first takes an expected value, then an actual value, and finally returns // a Reader that performs the assertion when given a *testing.T. // // Parameters: // - wrapped: The testify assertion function to wrap // - expected: The expected value for comparison // - msgAndArgs: Optional message and arguments for assertion failure // // Returns: // - A Kleisli function that takes the actual value and returns a Reader func wrap1[T any](wrapped func(t assert.TestingT, expected, actual any, msgAndArgs ...any) bool, expected T, msgAndArgs ...any) Kleisli[T] { return func(actual T) Reader { return func(t *testing.T) bool { return wrapped(t, expected, actual, msgAndArgs...) } } } // NotEqual tests if the expected and the actual values are not equal. // // This function follows the "data last" principle - you provide the expected value first, // then the actual value, and finally the testing.T context. // // Example: // // func TestNotEqual(t *testing.T) { // value := 42 // assert.NotEqual(10)(value)(t) // Passes: 42 != 10 // assert.NotEqual(42)(value)(t) // Fails: 42 == 42 // } func NotEqual[T any](expected T) Kleisli[T] { return wrap1(assert.NotEqual, expected) } // Equal tests if the expected and the actual values are equal. // // This is one of the most commonly used assertions. It follows the "data last" principle - // you provide the expected value first, then the actual value, and finally the testing.T context. // // Example: // // func TestEqual(t *testing.T) { // result := 2 + 2 // assert.Equal(4)(result)(t) // Passes // // name := "Alice" // assert.Equal("Alice")(name)(t) // Passes // // // Can be composed with other assertions // user := User{Name: "Bob", Age: 30} // assertions := assert.AllOf([]assert.Reader{ // assert.Equal("Bob")(user.Name), // assert.Equal(30)(user.Age), // }) // assertions(t) // } func Equal[T any](expected T) Kleisli[T] { return wrap1(assert.Equal, expected) } // ArrayNotEmpty checks if an array is not empty. // // Example: // // func TestArrayNotEmpty(t *testing.T) { // numbers := []int{1, 2, 3} // assert.ArrayNotEmpty(numbers)(t) // Passes // // empty := []int{} // assert.ArrayNotEmpty(empty)(t) // Fails // } func ArrayNotEmpty[T any](arr []T) Reader { return func(t *testing.T) bool { return assert.NotEmpty(t, arr) } } // RecordNotEmpty checks if a map is not empty. // // Example: // // func TestRecordNotEmpty(t *testing.T) { // config := map[string]int{"timeout": 30, "retries": 3} // assert.RecordNotEmpty(config)(t) // Passes // // empty := map[string]int{} // assert.RecordNotEmpty(empty)(t) // Fails // } func RecordNotEmpty[K comparable, T any](mp map[K]T) Reader { return func(t *testing.T) bool { return assert.NotEmpty(t, mp) } } // StringNotEmpty checks if a string is not empty. // // Example: // // func TestStringNotEmpty(t *testing.T) { // message := "Hello, World!" // assert.StringNotEmpty(message)(t) // Passes // // empty := "" // assert.StringNotEmpty(empty)(t) // Fails // } func StringNotEmpty(s string) Reader { return func(t *testing.T) bool { return assert.NotEmpty(t, s) } } // ArrayLength tests if an array has the expected length. // // Example: // // func TestArrayLength(t *testing.T) { // numbers := []int{1, 2, 3, 4, 5} // assert.ArrayLength[int](5)(numbers)(t) // Passes // assert.ArrayLength[int](3)(numbers)(t) // Fails // } func ArrayLength[T any](expected int) Kleisli[[]T] { return func(actual []T) Reader { return func(t *testing.T) bool { return assert.Len(t, actual, expected) } } } // RecordLength tests if a map has the expected length. // // Example: // // func TestRecordLength(t *testing.T) { // config := map[string]string{"host": "localhost", "port": "8080"} // assert.RecordLength[string, string](2)(config)(t) // Passes // assert.RecordLength[string, string](3)(config)(t) // Fails // } func RecordLength[K comparable, T any](expected int) Kleisli[map[K]T] { return func(actual map[K]T) Reader { return func(t *testing.T) bool { return assert.Len(t, actual, expected) } } } // StringLength tests if a string has the expected length. // // Example: // // func TestStringLength(t *testing.T) { // message := "Hello" // assert.StringLength[any, any](5)(message)(t) // Passes // assert.StringLength[any, any](10)(message)(t) // Fails // } func StringLength[K comparable, T any](expected int) Kleisli[string] { return func(actual string) Reader { return func(t *testing.T) bool { return assert.Len(t, actual, expected) } } } // NoError validates that there is no error. // // This is commonly used to assert that operations complete successfully. // // Example: // // func TestNoError(t *testing.T) { // err := doSomething() // assert.NoError(err)(t) // Passes if err is nil // // // Can be used with result types // result := result.TryCatch(func() (int, error) { // return 42, nil // }) // assert.Success(result)(t) // Uses NoError internally // } func NoError(err error) Reader { return func(t *testing.T) bool { return assert.NoError(t, err) } } // Error validates that there is an error. // // This is used to assert that operations fail as expected. // // Example: // // func TestError(t *testing.T) { // err := validateInput("") // assert.Error(err)(t) // Passes if err is not nil // // err2 := validateInput("valid") // assert.Error(err2)(t) // Fails if err2 is nil // } func Error(err error) Reader { return func(t *testing.T) bool { return assert.Error(t, err) } } // Success checks if a [Result] represents success. // // This is a convenience function for testing Result types from the fp-go library. // // Example: // // func TestSuccess(t *testing.T) { // res := result.Of[int](42) // assert.Success(res)(t) // Passes // // failedRes := result.Error[int](errors.New("failed")) // assert.Success(failedRes)(t) // Fails // } func Success[T any](res Result[T]) Reader { return NoError(result.ToError(res)) } // Failure checks if a [Result] represents failure. // // This is a convenience function for testing Result types from the fp-go library. // // Example: // // func TestFailure(t *testing.T) { // res := result.Error[int](errors.New("something went wrong")) // assert.Failure(res)(t) // Passes // // successRes := result.Of[int](42) // assert.Failure(successRes)(t) // Fails // } func Failure[T any](res Result[T]) Reader { return Error(result.ToError(res)) } // ArrayContains tests if a value is contained in an array. // // Example: // // func TestArrayContains(t *testing.T) { // numbers := []int{1, 2, 3, 4, 5} // assert.ArrayContains(3)(numbers)(t) // Passes // assert.ArrayContains(10)(numbers)(t) // Fails // // names := []string{"Alice", "Bob", "Charlie"} // assert.ArrayContains("Bob")(names)(t) // Passes // } func ArrayContains[T any](expected T) Kleisli[[]T] { return func(actual []T) Reader { return func(t *testing.T) bool { return assert.Contains(t, actual, expected) } } } // ContainsKey tests if a key is contained in a map. // // Example: // // func TestContainsKey(t *testing.T) { // config := map[string]int{"timeout": 30, "retries": 3} // assert.ContainsKey[int]("timeout")(config)(t) // Passes // assert.ContainsKey[int]("maxSize")(config)(t) // Fails // } func ContainsKey[T any, K comparable](expected K) Kleisli[map[K]T] { return func(actual map[K]T) Reader { return func(t *testing.T) bool { return assert.Contains(t, actual, expected) } } } // NotContainsKey tests if a key is not contained in a map. // // Example: // // func TestNotContainsKey(t *testing.T) { // config := map[string]int{"timeout": 30, "retries": 3} // assert.NotContainsKey[int]("maxSize")(config)(t) // Passes // assert.NotContainsKey[int]("timeout")(config)(t) // Fails // } func NotContainsKey[T any, K comparable](expected K) Kleisli[map[K]T] { return func(actual map[K]T) Reader { return func(t *testing.T) bool { return assert.NotContains(t, actual, expected) } } } // That asserts that a particular predicate matches. // // This is a powerful function that allows you to create custom assertions using predicates. // // Example: // // func TestThat(t *testing.T) { // // Test if a number is positive // isPositive := func(n int) bool { return n > 0 } // assert.That(isPositive)(42)(t) // Passes // assert.That(isPositive)(-5)(t) // Fails // // // Test if a string is uppercase // isUppercase := func(s string) bool { return s == strings.ToUpper(s) } // assert.That(isUppercase)("HELLO")(t) // Passes // assert.That(isUppercase)("Hello")(t) // Fails // // // Can be combined with Local for property testing // type User struct { Age int } // ageIsAdult := assert.Local(func(u User) int { return u.Age })( // assert.That(func(age int) bool { return age >= 18 }), // ) // user := User{Age: 25} // ageIsAdult(user)(t) // Passes // } func That[T any](pred Predicate[T]) Kleisli[T] { return func(a T) Reader { return func(t *testing.T) bool { if pred(a) { return true } return assert.Fail(t, fmt.Sprintf("Preficate %v does not match value %v", pred, a)) } } } // AllOf combines multiple assertion Readers into a single Reader that passes // only if all assertions pass. // // This function uses boolean AND logic (MonoidAll) to combine the results of // all assertions. If any assertion fails, the combined assertion fails. // // This is useful for grouping related assertions together and ensuring all // conditions are met. // // Parameters: // - readers: Array of assertion Readers to combine // // Returns: // - A single Reader that performs all assertions and returns true only if all pass // // Example: // // func TestUser(t *testing.T) { // user := User{Name: "Alice", Age: 30, Active: true} // assertions := assert.AllOf([]assert.Reader{ // assert.Equal("Alice")(user.Name), // assert.Equal(30)(user.Age), // assert.Equal(true)(user.Active), // }) // assertions(t) // } // //go:inline func AllOf(readers []Reader) Reader { return reader.MonadReduceArrayM(readers, boolean.MonoidAll) } // RunAll executes a map of named test cases, running each as a subtest. // // This function creates a Reader that runs multiple named test cases using // Go's t.Run for proper test isolation and reporting. Each test case is // executed as a separate subtest with its own name. // // The function returns true only if all subtests pass. This allows for // better test organization and clearer test output. // // Parameters: // - testcases: Map of test names to assertion Readers // // Returns: // - A Reader that executes all named test cases and returns true if all pass // // Example: // // func TestMathOperations(t *testing.T) { // testcases := map[string]assert.Reader{ // "addition": assert.Equal(4)(2 + 2), // "multiplication": assert.Equal(6)(2 * 3), // "subtraction": assert.Equal(1)(3 - 2), // } // assert.RunAll(testcases)(t) // } // //go:inline func RunAll(testcases map[string]Reader) Reader { return func(t *testing.T) bool { current := true for k, r := range testcases { current = current && t.Run(k, func(t1 *testing.T) { r(t1) }) } return current } } // Local transforms a Reader that works on type R1 into a Reader that works on type R2, // by providing a function that converts R2 to R1. This allows you to focus a test on a // specific property or subset of a larger data structure. // // This is particularly useful when you have an assertion that operates on a specific field // or property, and you want to apply it to a complete object. Instead of extracting the // property and then asserting on it, you can transform the assertion to work directly // on the whole object. // // Parameters: // - f: A function that extracts or transforms R2 into R1 // // Returns: // - A function that transforms a Reader[R1, Reader] into a Reader[R2, Reader] // // Example: // // type User struct { // Name string // Age int // } // // // Create an assertion that checks if age is positive // ageIsPositive := assert.That(func(age int) bool { return age > 0 }) // // // Focus this assertion on the Age field of User // userAgeIsPositive := assert.Local(func(u User) int { return u.Age })(ageIsPositive) // // // Now we can test the whole User object // user := User{Name: "Alice", Age: 30} // userAgeIsPositive(user)(t) // //go:inline func Local[R1, R2 any](f func(R2) R1) func(Kleisli[R1]) Kleisli[R2] { return reader.Local[Reader](f) } // LocalL is similar to Local but uses a Lens to focus on a specific property. // A Lens is a functional programming construct that provides a composable way to // focus on a part of a data structure. // // This function is particularly useful when you want to focus a test on a specific // field of a struct using a lens, making the code more declarative and composable. // Lenses are often code-generated or predefined for common data structures. // // Parameters: // - l: A Lens that focuses from type S to type T // // Returns: // - A function that transforms a Reader[T, Reader] into a Reader[S, Reader] // // Example: // // type Person struct { // Name string // Email string // } // // // Assume we have a lens that focuses on the Email field // var emailLens = lens.Prop[Person, string]("Email") // // // Create an assertion for email format // validEmail := assert.That(func(email string) bool { // return strings.Contains(email, "@") // }) // // // Focus this assertion on the Email property using a lens // validPersonEmail := assert.LocalL(emailLens)(validEmail) // // // Test a Person object // person := Person{Name: "Bob", Email: "bob@example.com"} // validPersonEmail(person)(t) // //go:inline func LocalL[S, T any](l Lens[S, T]) func(Kleisli[T]) Kleisli[S] { return reader.Local[Reader](l.Get) } // fromOptionalGetter is an internal helper that creates an assertion Reader from // an optional getter function. It asserts that the optional value is present (Some). func fromOptionalGetter[S, T any](getter func(S) option.Option[T], msgAndArgs ...any) Kleisli[S] { return func(s S) Reader { return func(t *testing.T) bool { return assert.True(t, option.IsSome(getter(s)), msgAndArgs...) } } } // FromOptional creates an assertion that checks if an Optional can successfully extract a value. // An Optional is an optic that represents an optional reference to a subpart of a data structure. // // This function is useful when you have an Optional optic and want to assert that the optional // value is present (Some) rather than absent (None). The assertion passes if the Optional's // GetOption returns Some, and fails if it returns None. // // This enables property-focused testing where you verify that a particular optional field or // sub-structure exists and is accessible. // // Parameters: // - opt: An Optional optic that focuses from type S to type T // // Returns: // - A Reader that asserts the optional value is present when applied to a value of type S // // Example: // // type Config struct { // Database *DatabaseConfig // Optional field // } // // type DatabaseConfig struct { // Host string // Port int // } // // // Create an Optional that focuses on the Database field // dbOptional := optional.MakeOptional( // func(c Config) option.Option[*DatabaseConfig] { // if c.Database != nil { // return option.Some(c.Database) // } // return option.None[*DatabaseConfig]() // }, // func(c Config, db *DatabaseConfig) Config { // c.Database = db // return c // }, // ) // // // Assert that the database config is present // hasDatabaseConfig := assert.FromOptional(dbOptional) // // config := Config{Database: &DatabaseConfig{Host: "localhost", Port: 5432}} // hasDatabaseConfig(config)(t) // Passes // // emptyConfig := Config{Database: nil} // hasDatabaseConfig(emptyConfig)(t) // Fails // //go:inline func FromOptional[S, T any](opt Optional[S, T]) reader.Reader[S, Reader] { return fromOptionalGetter(opt.GetOption, "Optional: %s", opt) } // FromPrism creates an assertion that checks if a Prism can successfully extract a value. // A Prism is an optic used to select part of a sum type (tagged union or variant). // // This function is useful when you have a Prism optic and want to assert that a value // matches a specific variant of a sum type. The assertion passes if the Prism's GetOption // returns Some (meaning the value is of the expected variant), and fails if it returns None // (meaning the value is a different variant). // // This enables variant-focused testing where you verify that a value is of a particular // type or matches a specific condition within a sum type. // // Parameters: // - p: A Prism optic that focuses from type S to type T // // Returns: // - A Reader that asserts the prism successfully extracts when applied to a value of type S // // Example: // // type Result interface{ isResult() } // type Success struct{ Value int } // type Failure struct{ Error string } // // func (Success) isResult() {} // func (Failure) isResult() {} // // // Create a Prism that focuses on Success variant // successPrism := prism.MakePrism( // func(r Result) option.Option[int] { // if s, ok := r.(Success); ok { // return option.Some(s.Value) // } // return option.None[int]() // }, // func(v int) Result { return Success{Value: v} }, // ) // // // Assert that the result is a Success // isSuccess := assert.FromPrism(successPrism) // // result1 := Success{Value: 42} // isSuccess(result1)(t) // Passes // // result2 := Failure{Error: "something went wrong"} // isSuccess(result2)(t) // Fails // //go:inline func FromPrism[S, T any](p Prism[S, T]) reader.Reader[S, Reader] { return fromOptionalGetter(p.GetOption, "Prism: %s", p) }