mirror of
https://github.com/IBM/fp-go.git
synced 2026-03-02 13:19:12 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3fdb03df4 |
@@ -194,6 +194,25 @@ func ArrayNotEmpty[T any](arr []T) Reader {
|
||||
}
|
||||
}
|
||||
|
||||
// ArrayEmpty checks if an array is empty.
|
||||
//
|
||||
// This is the complement of ArrayNotEmpty, asserting that a slice has no elements.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestArrayEmpty(t *testing.T) {
|
||||
// empty := []int{}
|
||||
// assert.ArrayEmpty(empty)(t) // Passes
|
||||
//
|
||||
// numbers := []int{1, 2, 3}
|
||||
// assert.ArrayEmpty(numbers)(t) // Fails
|
||||
// }
|
||||
func ArrayEmpty[T any](arr []T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.Empty(t, arr)
|
||||
}
|
||||
}
|
||||
|
||||
// RecordNotEmpty checks if a map is not empty.
|
||||
//
|
||||
// Example:
|
||||
@@ -211,6 +230,25 @@ func RecordNotEmpty[K comparable, T any](mp map[K]T) Reader {
|
||||
}
|
||||
}
|
||||
|
||||
// RecordEmpty checks if a map is empty.
|
||||
//
|
||||
// This is the complement of RecordNotEmpty, asserting that a map has no key-value pairs.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestRecordEmpty(t *testing.T) {
|
||||
// empty := map[string]int{}
|
||||
// assert.RecordEmpty(empty)(t) // Passes
|
||||
//
|
||||
// config := map[string]int{"timeout": 30}
|
||||
// assert.RecordEmpty(config)(t) // Fails
|
||||
// }
|
||||
func RecordEmpty[K comparable, T any](mp map[K]T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return assert.Empty(t, mp)
|
||||
}
|
||||
}
|
||||
|
||||
// StringNotEmpty checks if a string is not empty.
|
||||
//
|
||||
// Example:
|
||||
@@ -504,15 +542,7 @@ func AllOf(readers []Reader) Reader {
|
||||
//
|
||||
//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
|
||||
}
|
||||
return SequenceRecord(testcases)
|
||||
}
|
||||
|
||||
// Local transforms a Reader that works on type R1 into a Reader that works on type R2,
|
||||
|
||||
@@ -85,6 +85,33 @@ func TestArrayNotEmpty(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestArrayEmpty(t *testing.T) {
|
||||
t.Run("should pass for empty array", func(t *testing.T) {
|
||||
arr := []int{}
|
||||
result := ArrayEmpty(arr)(t)
|
||||
if !result {
|
||||
t.Error("Expected ArrayEmpty to pass for empty array")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail for non-empty array", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
arr := []int{1, 2, 3}
|
||||
result := ArrayEmpty(arr)(mockT)
|
||||
if result {
|
||||
t.Error("Expected ArrayEmpty to fail for non-empty array")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with different types", func(t *testing.T) {
|
||||
strArr := []string{}
|
||||
result := ArrayEmpty(strArr)(t)
|
||||
if !result {
|
||||
t.Error("Expected ArrayEmpty to pass for empty string array")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRecordNotEmpty(t *testing.T) {
|
||||
t.Run("should pass for non-empty map", func(t *testing.T) {
|
||||
mp := map[string]int{"a": 1, "b": 2}
|
||||
@@ -131,6 +158,33 @@ func TestArrayLength(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestRecordEmpty(t *testing.T) {
|
||||
t.Run("should pass for empty map", func(t *testing.T) {
|
||||
mp := map[string]int{}
|
||||
result := RecordEmpty(mp)(t)
|
||||
if !result {
|
||||
t.Error("Expected RecordEmpty to pass for empty map")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail for non-empty map", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
mp := map[string]int{"a": 1, "b": 2}
|
||||
result := RecordEmpty(mp)(mockT)
|
||||
if result {
|
||||
t.Error("Expected RecordEmpty to fail for non-empty map")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with different key-value types", func(t *testing.T) {
|
||||
mp := map[int]string{}
|
||||
result := RecordEmpty(mp)(t)
|
||||
if !result {
|
||||
t.Error("Expected RecordEmpty to pass for empty map with int keys")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRecordLength(t *testing.T) {
|
||||
t.Run("should pass when map length matches", func(t *testing.T) {
|
||||
mp := map[string]int{"a": 1, "b": 2}
|
||||
@@ -150,6 +204,33 @@ func TestRecordLength(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestStringNotEmpty(t *testing.T) {
|
||||
t.Run("should pass for non-empty string", func(t *testing.T) {
|
||||
str := "Hello, World!"
|
||||
result := StringNotEmpty(str)(t)
|
||||
if !result {
|
||||
t.Error("Expected StringNotEmpty to pass for non-empty string")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail for empty string", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
str := ""
|
||||
result := StringNotEmpty(str)(mockT)
|
||||
if result {
|
||||
t.Error("Expected StringNotEmpty to fail for empty string")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should pass for string with whitespace", func(t *testing.T) {
|
||||
str := " "
|
||||
result := StringNotEmpty(str)(t)
|
||||
if !result {
|
||||
t.Error("Expected StringNotEmpty to pass for string with whitespace")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestStringLength(t *testing.T) {
|
||||
t.Run("should pass when string length matches", func(t *testing.T) {
|
||||
str := "hello"
|
||||
|
||||
122
v2/assert/from.go
Normal file
122
v2/assert/from.go
Normal file
@@ -0,0 +1,122 @@
|
||||
// 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
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
// FromReaderIOResult converts a ReaderIOResult[Reader] into a Reader.
|
||||
//
|
||||
// This function bridges the gap between context-aware, IO-based computations that may fail
|
||||
// (ReaderIOResult) and the simpler Reader type used for test assertions. It executes the
|
||||
// ReaderIOResult computation using the test's context, handles any potential errors by
|
||||
// converting them to test failures via NoError, and returns the resulting Reader.
|
||||
//
|
||||
// The conversion process:
|
||||
// 1. Executes the ReaderIOResult with the test context (t.Context())
|
||||
// 2. Runs the resulting IO operation ()
|
||||
// 3. Extracts the Result, converting errors to test failures using NoError
|
||||
// 4. Returns a Reader that can be applied to *testing.T
|
||||
//
|
||||
// This is particularly useful when you have test assertions that need to:
|
||||
// - Access context for cancellation or deadlines
|
||||
// - Perform IO operations (file access, network calls, etc.)
|
||||
// - Handle potential errors gracefully in tests
|
||||
//
|
||||
// Parameters:
|
||||
// - ri: A ReaderIOResult that produces a Reader when given a context and executed
|
||||
//
|
||||
// Returns:
|
||||
// - A Reader that can be directly applied to *testing.T for assertion
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestWithContext(t *testing.T) {
|
||||
// // Create a ReaderIOResult that performs an IO operation
|
||||
// checkDatabase := func(ctx context.Context) func() result.Result[assert.Reader] {
|
||||
// return func() result.Result[assert.Reader] {
|
||||
// // Simulate database check
|
||||
// if err := db.PingContext(ctx); err != nil {
|
||||
// return result.Error[assert.Reader](err)
|
||||
// }
|
||||
// return result.Of[assert.Reader](assert.NoError(nil))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Convert to Reader and execute
|
||||
// assertion := assert.FromReaderIOResult(checkDatabase)
|
||||
// assertion(t)
|
||||
// }
|
||||
func FromReaderIOResult(ri ReaderIOResult[Reader]) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return F.Pipe1(
|
||||
ri(t.Context())(),
|
||||
result.GetOrElse(NoError),
|
||||
)(t)
|
||||
}
|
||||
}
|
||||
|
||||
// FromReaderIO converts a ReaderIO[Reader] into a Reader.
|
||||
//
|
||||
// This function bridges the gap between context-aware, IO-based computations (ReaderIO)
|
||||
// and the simpler Reader type used for test assertions. It executes the ReaderIO
|
||||
// computation using the test's context and returns the resulting Reader.
|
||||
//
|
||||
// Unlike FromReaderIOResult, this function does not handle errors explicitly - it assumes
|
||||
// the IO operation will succeed or that any errors are handled within the ReaderIO itself.
|
||||
//
|
||||
// The conversion process:
|
||||
// 1. Executes the ReaderIO with the test context (t.Context())
|
||||
// 2. Runs the resulting IO operation ()
|
||||
// 3. Returns a Reader that can be applied to *testing.T
|
||||
//
|
||||
// This is particularly useful when you have test assertions that need to:
|
||||
// - Access context for cancellation or deadlines
|
||||
// - Perform IO operations that don't fail (or handle failures internally)
|
||||
// - Integrate with context-aware testing utilities
|
||||
//
|
||||
// Parameters:
|
||||
// - ri: A ReaderIO that produces a Reader when given a context and executed
|
||||
//
|
||||
// Returns:
|
||||
// - A Reader that can be directly applied to *testing.T for assertion
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestWithIO(t *testing.T) {
|
||||
// // Create a ReaderIO that performs an IO operation
|
||||
// logAndCheck := func(ctx context.Context) func() assert.Reader {
|
||||
// return func() assert.Reader {
|
||||
// // Log something using context
|
||||
// logger.InfoContext(ctx, "Running test")
|
||||
// // Return an assertion
|
||||
// return assert.Equal(42)(computeValue())
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Convert to Reader and execute
|
||||
// assertion := assert.FromReaderIO(logAndCheck)
|
||||
// assertion(t)
|
||||
// }
|
||||
func FromReaderIO(ri ReaderIO[Reader]) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return ri(t.Context())()(t)
|
||||
}
|
||||
}
|
||||
383
v2/assert/from_test.go
Normal file
383
v2/assert/from_test.go
Normal file
@@ -0,0 +1,383 @@
|
||||
// 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
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
func TestFromReaderIOResult(t *testing.T) {
|
||||
t.Run("should pass when ReaderIOResult returns success with passing assertion", func(t *testing.T) {
|
||||
// Create a ReaderIOResult that returns a successful Reader
|
||||
ri := func(ctx context.Context) func() result.Result[Reader] {
|
||||
return func() result.Result[Reader] {
|
||||
// Return a Reader that always passes
|
||||
return result.Of[Reader](func(t *testing.T) bool {
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIOResult(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIOResult to pass when ReaderIOResult returns success")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should pass when ReaderIOResult returns success with Equal assertion", func(t *testing.T) {
|
||||
// Create a ReaderIOResult that returns a successful Equal assertion
|
||||
ri := func(ctx context.Context) func() result.Result[Reader] {
|
||||
return func() result.Result[Reader] {
|
||||
return result.Of[Reader](Equal(42)(42))
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIOResult(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIOResult to pass with Equal assertion")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when ReaderIOResult returns error", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
|
||||
// Create a ReaderIOResult that returns an error
|
||||
ri := func(ctx context.Context) func() result.Result[Reader] {
|
||||
return func() result.Result[Reader] {
|
||||
return result.Left[Reader](errors.New("test error"))
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIOResult(ri)
|
||||
res := reader(mockT)
|
||||
if res {
|
||||
t.Error("Expected FromReaderIOResult to fail when ReaderIOResult returns error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when ReaderIOResult returns success but assertion fails", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
|
||||
// Create a ReaderIOResult that returns a failing assertion
|
||||
ri := func(ctx context.Context) func() result.Result[Reader] {
|
||||
return func() result.Result[Reader] {
|
||||
return result.Of[Reader](Equal(42)(43))
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIOResult(ri)
|
||||
res := reader(mockT)
|
||||
if res {
|
||||
t.Error("Expected FromReaderIOResult to fail when assertion fails")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should use test context", func(t *testing.T) {
|
||||
contextUsed := false
|
||||
|
||||
// Create a ReaderIOResult that checks if context is provided
|
||||
ri := func(ctx context.Context) func() result.Result[Reader] {
|
||||
if ctx != nil {
|
||||
contextUsed = true
|
||||
}
|
||||
return func() result.Result[Reader] {
|
||||
return result.Of[Reader](func(t *testing.T) bool {
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIOResult(ri)
|
||||
reader(t)
|
||||
|
||||
if !contextUsed {
|
||||
t.Error("Expected FromReaderIOResult to use test context")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with NoError assertion", func(t *testing.T) {
|
||||
// Create a ReaderIOResult that returns NoError assertion
|
||||
ri := func(ctx context.Context) func() result.Result[Reader] {
|
||||
return func() result.Result[Reader] {
|
||||
return result.Of[Reader](NoError(nil))
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIOResult(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIOResult to pass with NoError assertion")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with complex assertions", func(t *testing.T) {
|
||||
// Create a ReaderIOResult with multiple composed assertions
|
||||
ri := func(ctx context.Context) func() result.Result[Reader] {
|
||||
return func() result.Result[Reader] {
|
||||
arr := []int{1, 2, 3}
|
||||
assertions := AllOf([]Reader{
|
||||
ArrayNotEmpty(arr),
|
||||
ArrayLength[int](3)(arr),
|
||||
ArrayContains(2)(arr),
|
||||
})
|
||||
return result.Of[Reader](assertions)
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIOResult(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIOResult to pass with complex assertions")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromReaderIO(t *testing.T) {
|
||||
t.Run("should pass when ReaderIO returns passing assertion", func(t *testing.T) {
|
||||
// Create a ReaderIO that returns a Reader that always passes
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIO to pass when ReaderIO returns passing assertion")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should pass when ReaderIO returns Equal assertion", func(t *testing.T) {
|
||||
// Create a ReaderIO that returns an Equal assertion
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
return Equal(42)(42)
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIO to pass with Equal assertion")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail when ReaderIO returns failing assertion", func(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
|
||||
// Create a ReaderIO that returns a failing assertion
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
return Equal(42)(43)
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(mockT)
|
||||
if res {
|
||||
t.Error("Expected FromReaderIO to fail when assertion fails")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should use test context", func(t *testing.T) {
|
||||
contextUsed := false
|
||||
|
||||
// Create a ReaderIO that checks if context is provided
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
if ctx != nil {
|
||||
contextUsed = true
|
||||
}
|
||||
return func() Reader {
|
||||
return func(t *testing.T) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
reader(t)
|
||||
|
||||
if !contextUsed {
|
||||
t.Error("Expected FromReaderIO to use test context")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with NoError assertion", func(t *testing.T) {
|
||||
// Create a ReaderIO that returns NoError assertion
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
return NoError(nil)
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIO to pass with NoError assertion")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with Error assertion", func(t *testing.T) {
|
||||
// Create a ReaderIO that returns Error assertion
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
return Error(errors.New("expected error"))
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIO to pass with Error assertion")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with complex assertions", func(t *testing.T) {
|
||||
// Create a ReaderIO with multiple composed assertions
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
mp := map[string]int{"a": 1, "b": 2}
|
||||
return AllOf([]Reader{
|
||||
RecordNotEmpty(mp),
|
||||
RecordLength[string, int](2)(mp),
|
||||
ContainsKey[int]("a")(mp),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIO to pass with complex assertions")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with string assertions", func(t *testing.T) {
|
||||
// Create a ReaderIO with string assertions
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
str := "hello world"
|
||||
return AllOf([]Reader{
|
||||
StringNotEmpty(str),
|
||||
StringLength[any, any](11)(str),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIO to pass with string assertions")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with Result assertions", func(t *testing.T) {
|
||||
// Create a ReaderIO with Result assertions
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
successResult := result.Of[int](42)
|
||||
return Success(successResult)
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIO to pass with Success assertion")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should work with Failure assertion", func(t *testing.T) {
|
||||
// Create a ReaderIO with Failure assertion
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
failureResult := result.Left[int](errors.New("test error"))
|
||||
return Failure(failureResult)
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected FromReaderIO to pass with Failure assertion")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromReaderIOResultIntegration tests integration scenarios
|
||||
func TestFromReaderIOResultIntegration(t *testing.T) {
|
||||
t.Run("should work in a realistic scenario with context cancellation", func(t *testing.T) {
|
||||
// Create a ReaderIOResult that uses the context
|
||||
ri := func(testCtx context.Context) func() result.Result[Reader] {
|
||||
return func() result.Result[Reader] {
|
||||
// Check if context is valid
|
||||
if testCtx == nil {
|
||||
return result.Left[Reader](errors.New("context is nil"))
|
||||
}
|
||||
|
||||
// Return a successful assertion
|
||||
return result.Of[Reader](Equal("test")("test"))
|
||||
}
|
||||
}
|
||||
|
||||
// Use the actual testing.T from the subtest
|
||||
reader := FromReaderIOResult(ri)
|
||||
res := reader(t)
|
||||
if !res {
|
||||
t.Error("Expected integration test to pass")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromReaderIOIntegration tests integration scenarios
|
||||
func TestFromReaderIOIntegration(t *testing.T) {
|
||||
t.Run("should work in a realistic scenario with logging", func(t *testing.T) {
|
||||
logCalled := false
|
||||
|
||||
// Create a ReaderIO that simulates logging
|
||||
ri := func(ctx context.Context) func() Reader {
|
||||
return func() Reader {
|
||||
// Simulate logging with context
|
||||
if ctx != nil {
|
||||
logCalled = true
|
||||
}
|
||||
|
||||
// Return an assertion
|
||||
return Equal(100)(100)
|
||||
}
|
||||
}
|
||||
|
||||
reader := FromReaderIO(ri)
|
||||
res := reader(t)
|
||||
|
||||
if !res {
|
||||
t.Error("Expected integration test to pass")
|
||||
}
|
||||
|
||||
if !logCalled {
|
||||
t.Error("Expected logging to be called")
|
||||
}
|
||||
})
|
||||
}
|
||||
207
v2/assert/logger.go
Normal file
207
v2/assert/logger.go
Normal file
@@ -0,0 +1,207 @@
|
||||
// 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
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
)
|
||||
|
||||
// Logf creates a logging function that outputs formatted test messages using Go's testing.T.Logf.
|
||||
//
|
||||
// This function provides a functional programming approach to test logging, returning a
|
||||
// [ReaderIO] that can be composed with other test operations. It's particularly useful
|
||||
// for debugging tests, tracing execution flow, or documenting test behavior without
|
||||
// affecting test outcomes.
|
||||
//
|
||||
// The function uses a curried design pattern:
|
||||
// 1. First, you provide a format string (prefix) with format verbs (like %v, %d, %s)
|
||||
// 2. This returns a function that takes a value of type T
|
||||
// 3. That function returns a ReaderIO that performs the logging when executed
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - prefix: A format string compatible with fmt.Printf (e.g., "Value: %v", "Count: %d")
|
||||
// The format string should contain exactly one format verb that matches type T
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A function that takes a value of type T and returns a [ReaderIO][*testing.T, Void]
|
||||
// When executed, this ReaderIO logs the formatted message to the test output
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - T: The type of value to be logged. Can be any type that can be formatted by fmt
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Debugging test execution by logging intermediate values
|
||||
// - Tracing the flow of complex test scenarios
|
||||
// - Documenting test behavior in the test output
|
||||
// - Logging values in functional pipelines without breaking the chain
|
||||
// - Creating reusable logging operations for specific types
|
||||
//
|
||||
// # Example - Basic Logging
|
||||
//
|
||||
// func TestBasicLogging(t *testing.T) {
|
||||
// // Create a logger for integers
|
||||
// logInt := assert.Logf[int]("Processing value: %d")
|
||||
//
|
||||
// // Use it to log a value
|
||||
// value := 42
|
||||
// logInt(value)(t)() // Outputs: "Processing value: 42"
|
||||
// }
|
||||
//
|
||||
// # Example - Logging in Test Pipeline
|
||||
//
|
||||
// func TestPipelineWithLogging(t *testing.T) {
|
||||
// type User struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// user := User{Name: "Alice", Age: 30}
|
||||
//
|
||||
// // Create a logger for User
|
||||
// logUser := assert.Logf[User]("Testing user: %+v")
|
||||
//
|
||||
// // Log the user being tested
|
||||
// logUser(user)(t)()
|
||||
//
|
||||
// // Continue with assertions
|
||||
// assert.StringNotEmpty(user.Name)(t)
|
||||
// assert.That(func(age int) bool { return age > 0 })(user.Age)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - Multiple Loggers for Different Types
|
||||
//
|
||||
// func TestMultipleLoggers(t *testing.T) {
|
||||
// // Create type-specific loggers
|
||||
// logString := assert.Logf[string]("String value: %s")
|
||||
// logInt := assert.Logf[int]("Integer value: %d")
|
||||
// logFloat := assert.Logf[float64]("Float value: %.2f")
|
||||
//
|
||||
// // Use them throughout the test
|
||||
// logString("hello")(t)() // Outputs: "String value: hello"
|
||||
// logInt(42)(t)() // Outputs: "Integer value: 42"
|
||||
// logFloat(3.14159)(t)() // Outputs: "Float value: 3.14"
|
||||
// }
|
||||
//
|
||||
// # Example - Logging Complex Structures
|
||||
//
|
||||
// func TestComplexStructureLogging(t *testing.T) {
|
||||
// type Config struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// Timeout int
|
||||
// }
|
||||
//
|
||||
// config := Config{Host: "localhost", Port: 8080, Timeout: 30}
|
||||
//
|
||||
// // Use %+v to include field names
|
||||
// logConfig := assert.Logf[Config]("Configuration: %+v")
|
||||
// logConfig(config)(t)()
|
||||
// // Outputs: "Configuration: {Host:localhost Port:8080 Timeout:30}"
|
||||
//
|
||||
// // Or use %#v for Go-syntax representation
|
||||
// logConfigGo := assert.Logf[Config]("Config (Go syntax): %#v")
|
||||
// logConfigGo(config)(t)()
|
||||
// // Outputs: "Config (Go syntax): assert.Config{Host:"localhost", Port:8080, Timeout:30}"
|
||||
// }
|
||||
//
|
||||
// # Example - Debugging Test Failures
|
||||
//
|
||||
// func TestWithDebugLogging(t *testing.T) {
|
||||
// numbers := []int{1, 2, 3, 4, 5}
|
||||
// logSlice := assert.Logf[[]int]("Testing slice: %v")
|
||||
//
|
||||
// // Log the input data
|
||||
// logSlice(numbers)(t)()
|
||||
//
|
||||
// // Perform assertions
|
||||
// assert.ArrayNotEmpty(numbers)(t)
|
||||
// assert.ArrayLength[int](5)(numbers)(t)
|
||||
//
|
||||
// // Log intermediate results
|
||||
// sum := 0
|
||||
// for _, n := range numbers {
|
||||
// sum += n
|
||||
// }
|
||||
// logInt := assert.Logf[int]("Sum: %d")
|
||||
// logInt(sum)(t)()
|
||||
//
|
||||
// assert.Equal(15)(sum)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - Conditional Logging
|
||||
//
|
||||
// func TestConditionalLogging(t *testing.T) {
|
||||
// logDebug := assert.Logf[string]("DEBUG: %s")
|
||||
//
|
||||
// values := []int{1, 2, 3, 4, 5}
|
||||
// for _, v := range values {
|
||||
// if v%2 == 0 {
|
||||
// logDebug(fmt.Sprintf("Found even number: %d", v))(t)()
|
||||
// }
|
||||
// }
|
||||
// // Outputs:
|
||||
// // DEBUG: Found even number: 2
|
||||
// // DEBUG: Found even number: 4
|
||||
// }
|
||||
//
|
||||
// # Format Verbs
|
||||
//
|
||||
// Common format verbs you can use in the prefix string:
|
||||
// - %v: Default format
|
||||
// - %+v: Default format with field names for structs
|
||||
// - %#v: Go-syntax representation
|
||||
// - %T: Type of the value
|
||||
// - %d: Integer in base 10
|
||||
// - %s: String
|
||||
// - %f: Floating point number
|
||||
// - %t: Boolean (true/false)
|
||||
// - %p: Pointer address
|
||||
//
|
||||
// See the fmt package documentation for a complete list of format verbs.
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - Logging does not affect test pass/fail status
|
||||
// - Log output appears in test results when running with -v flag or when tests fail
|
||||
// - The function returns Void, indicating it's used for side effects only
|
||||
// - The ReaderIO pattern allows logging to be composed with other operations
|
||||
//
|
||||
// # Related Functions
|
||||
//
|
||||
// - [FromReaderIO]: Converts ReaderIO operations into test assertions
|
||||
// - testing.T.Logf: The underlying Go testing log function
|
||||
//
|
||||
// # References
|
||||
//
|
||||
// - Go testing package: https://pkg.go.dev/testing
|
||||
// - fmt package format verbs: https://pkg.go.dev/fmt
|
||||
// - ReaderIO pattern: Combines Reader (context dependency) with IO (side effects)
|
||||
func Logf[T any](prefix string) func(T) readerio.ReaderIO[*testing.T, Void] {
|
||||
return func(a T) readerio.ReaderIO[*testing.T, Void] {
|
||||
return func(t *testing.T) IO[Void] {
|
||||
return io.FromImpure(func() {
|
||||
t.Logf(prefix, a)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
406
v2/assert/logger_test.go
Normal file
406
v2/assert/logger_test.go
Normal file
@@ -0,0 +1,406 @@
|
||||
// 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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestLogf_BasicInteger tests basic integer logging
|
||||
func TestLogf_BasicInteger(t *testing.T) {
|
||||
logInt := Logf[int]("Processing value: %d")
|
||||
|
||||
// This should not panic and should log the value
|
||||
logInt(42)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_BasicString tests basic string logging
|
||||
func TestLogf_BasicString(t *testing.T) {
|
||||
logString := Logf[string]("String value: %s")
|
||||
|
||||
logString("hello world")(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_BasicFloat tests basic float logging
|
||||
func TestLogf_BasicFloat(t *testing.T) {
|
||||
logFloat := Logf[float64]("Float value: %.2f")
|
||||
|
||||
logFloat(3.14159)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_BasicBoolean tests basic boolean logging
|
||||
func TestLogf_BasicBoolean(t *testing.T) {
|
||||
logBool := Logf[bool]("Boolean value: %t")
|
||||
|
||||
logBool(true)(t)()
|
||||
logBool(false)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_ComplexStruct tests logging of complex structures
|
||||
func TestLogf_ComplexStruct(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
logUser := Logf[User]("User: %+v")
|
||||
|
||||
user := User{Name: "Alice", Age: 30}
|
||||
logUser(user)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_Slice tests logging of slices
|
||||
func TestLogf_Slice(t *testing.T) {
|
||||
logSlice := Logf[[]int]("Slice: %v")
|
||||
|
||||
numbers := []int{1, 2, 3, 4, 5}
|
||||
logSlice(numbers)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_Map tests logging of maps
|
||||
func TestLogf_Map(t *testing.T) {
|
||||
logMap := Logf[map[string]int]("Map: %v")
|
||||
|
||||
data := map[string]int{"a": 1, "b": 2, "c": 3}
|
||||
logMap(data)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_Pointer tests logging of pointers
|
||||
func TestLogf_Pointer(t *testing.T) {
|
||||
logPtr := Logf[*int]("Pointer: %p")
|
||||
|
||||
value := 42
|
||||
logPtr(&value)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_NilPointer tests logging of nil pointers
|
||||
func TestLogf_NilPointer(t *testing.T) {
|
||||
logPtr := Logf[*int]("Pointer: %v")
|
||||
|
||||
var nilPtr *int
|
||||
logPtr(nilPtr)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_EmptyString tests logging of empty strings
|
||||
func TestLogf_EmptyString(t *testing.T) {
|
||||
logString := Logf[string]("String: '%s'")
|
||||
|
||||
logString("")(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_EmptySlice tests logging of empty slices
|
||||
func TestLogf_EmptySlice(t *testing.T) {
|
||||
logSlice := Logf[[]int]("Slice: %v")
|
||||
|
||||
logSlice([]int{})(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_EmptyMap tests logging of empty maps
|
||||
func TestLogf_EmptyMap(t *testing.T) {
|
||||
logMap := Logf[map[string]int]("Map: %v")
|
||||
|
||||
logMap(map[string]int{})(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_MultipleTypes tests using multiple loggers for different types
|
||||
func TestLogf_MultipleTypes(t *testing.T) {
|
||||
logString := Logf[string]("String: %s")
|
||||
logInt := Logf[int]("Integer: %d")
|
||||
logFloat := Logf[float64]("Float: %.2f")
|
||||
|
||||
logString("test")(t)()
|
||||
logInt(42)(t)()
|
||||
logFloat(3.14)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_WithinTestPipeline tests logging within a test pipeline
|
||||
func TestLogf_WithinTestPipeline(t *testing.T) {
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
config := Config{Host: "localhost", Port: 8080}
|
||||
|
||||
logConfig := Logf[Config]("Testing config: %+v")
|
||||
logConfig(config)(t)()
|
||||
|
||||
// Continue with assertions
|
||||
StringNotEmpty(config.Host)(t)
|
||||
That(func(port int) bool { return port > 0 })(config.Port)(t)
|
||||
|
||||
// Test passes if no panic occurs and assertions pass
|
||||
}
|
||||
|
||||
// TestLogf_NestedStructures tests logging of nested structures
|
||||
func TestLogf_NestedStructures(t *testing.T) {
|
||||
type Address struct {
|
||||
Street string
|
||||
City string
|
||||
}
|
||||
|
||||
type Person struct {
|
||||
Name string
|
||||
Address Address
|
||||
}
|
||||
|
||||
logPerson := Logf[Person]("Person: %+v")
|
||||
|
||||
person := Person{
|
||||
Name: "Bob",
|
||||
Address: Address{
|
||||
Street: "123 Main St",
|
||||
City: "Springfield",
|
||||
},
|
||||
}
|
||||
|
||||
logPerson(person)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_Interface tests logging of interface values
|
||||
func TestLogf_Interface(t *testing.T) {
|
||||
logAny := Logf[any]("Value: %v")
|
||||
|
||||
logAny(42)(t)()
|
||||
logAny("string")(t)()
|
||||
logAny([]int{1, 2, 3})(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_GoSyntaxFormat tests logging with Go-syntax format
|
||||
func TestLogf_GoSyntaxFormat(t *testing.T) {
|
||||
type Point struct {
|
||||
X int
|
||||
Y int
|
||||
}
|
||||
|
||||
logPoint := Logf[Point]("Point: %#v")
|
||||
|
||||
point := Point{X: 10, Y: 20}
|
||||
logPoint(point)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_TypeFormat tests logging with type format
|
||||
func TestLogf_TypeFormat(t *testing.T) {
|
||||
logType := Logf[any]("Type: %T, Value: %v")
|
||||
|
||||
logType(42)(t)()
|
||||
logType("string")(t)()
|
||||
logType(3.14)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_LargeNumbers tests logging of large numbers
|
||||
func TestLogf_LargeNumbers(t *testing.T) {
|
||||
logInt := Logf[int64]("Large number: %d")
|
||||
|
||||
logInt(9223372036854775807)(t)() // Max int64
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_NegativeNumbers tests logging of negative numbers
|
||||
func TestLogf_NegativeNumbers(t *testing.T) {
|
||||
logInt := Logf[int]("Number: %d")
|
||||
|
||||
logInt(-42)(t)()
|
||||
logInt(-100)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_SpecialFloats tests logging of special float values
|
||||
func TestLogf_SpecialFloats(t *testing.T) {
|
||||
logFloat := Logf[float64]("Float: %v")
|
||||
|
||||
logFloat(0.0)(t)()
|
||||
logFloat(-0.0)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_UnicodeStrings tests logging of unicode strings
|
||||
func TestLogf_UnicodeStrings(t *testing.T) {
|
||||
logString := Logf[string]("Unicode: %s")
|
||||
|
||||
logString("Hello, 世界")(t)()
|
||||
logString("Emoji: 🎉🎊")(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_MultilineStrings tests logging of multiline strings
|
||||
func TestLogf_MultilineStrings(t *testing.T) {
|
||||
logString := Logf[string]("Multiline:\n%s")
|
||||
|
||||
multiline := `Line 1
|
||||
Line 2
|
||||
Line 3`
|
||||
|
||||
logString(multiline)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_ReuseLogger tests reusing the same logger multiple times
|
||||
func TestLogf_ReuseLogger(t *testing.T) {
|
||||
logInt := Logf[int]("Value: %d")
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
logInt(i)(t)()
|
||||
}
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_ConditionalLogging tests conditional logging based on values
|
||||
func TestLogf_ConditionalLogging(t *testing.T) {
|
||||
logDebug := Logf[string]("DEBUG: %s")
|
||||
|
||||
values := []int{1, 2, 3, 4, 5}
|
||||
for _, v := range values {
|
||||
if v%2 == 0 {
|
||||
logDebug(fmt.Sprintf("Found even number: %d", v))(t)()
|
||||
}
|
||||
}
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_WithAssertions tests combining logging with assertions
|
||||
func TestLogf_WithAssertions(t *testing.T) {
|
||||
logInt := Logf[int]("Testing value: %d")
|
||||
|
||||
value := 42
|
||||
logInt(value)(t)()
|
||||
|
||||
// Perform assertion after logging
|
||||
Equal(42)(value)(t)
|
||||
|
||||
// Test passes if assertion passes
|
||||
}
|
||||
|
||||
// TestLogf_DebuggingFailures demonstrates using logging to debug test failures
|
||||
func TestLogf_DebuggingFailures(t *testing.T) {
|
||||
logSlice := Logf[[]int]("Input slice: %v")
|
||||
logInt := Logf[int]("Computed sum: %d")
|
||||
|
||||
numbers := []int{1, 2, 3, 4, 5}
|
||||
logSlice(numbers)(t)()
|
||||
|
||||
sum := 0
|
||||
for _, n := range numbers {
|
||||
sum += n
|
||||
}
|
||||
logInt(sum)(t)()
|
||||
|
||||
Equal(15)(sum)(t)
|
||||
|
||||
// Test passes if assertion passes
|
||||
}
|
||||
|
||||
// TestLogf_ComplexDataStructures tests logging of complex nested data
|
||||
func TestLogf_ComplexDataStructures(t *testing.T) {
|
||||
type Metadata struct {
|
||||
Version string
|
||||
Tags []string
|
||||
}
|
||||
|
||||
type Document struct {
|
||||
ID int
|
||||
Title string
|
||||
Metadata Metadata
|
||||
}
|
||||
|
||||
logDoc := Logf[Document]("Document: %+v")
|
||||
|
||||
doc := Document{
|
||||
ID: 1,
|
||||
Title: "Test Document",
|
||||
Metadata: Metadata{
|
||||
Version: "1.0",
|
||||
Tags: []string{"test", "example"},
|
||||
},
|
||||
}
|
||||
|
||||
logDoc(doc)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_ArrayTypes tests logging of array types
|
||||
func TestLogf_ArrayTypes(t *testing.T) {
|
||||
logArray := Logf[[5]int]("Array: %v")
|
||||
|
||||
arr := [5]int{1, 2, 3, 4, 5}
|
||||
logArray(arr)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_ChannelTypes tests logging of channel types
|
||||
func TestLogf_ChannelTypes(t *testing.T) {
|
||||
logChan := Logf[chan int]("Channel: %v")
|
||||
|
||||
ch := make(chan int, 1)
|
||||
logChan(ch)(t)()
|
||||
close(ch)
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
|
||||
// TestLogf_FunctionTypes tests logging of function types
|
||||
func TestLogf_FunctionTypes(t *testing.T) {
|
||||
logFunc := Logf[func() int]("Function: %v")
|
||||
|
||||
fn := func() int { return 42 }
|
||||
logFunc(fn)(t)()
|
||||
|
||||
// Test passes if no panic occurs
|
||||
}
|
||||
152
v2/assert/monoid.go
Normal file
152
v2/assert/monoid.go
Normal file
@@ -0,0 +1,152 @@
|
||||
// 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
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/boolean"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
// ApplicativeMonoid returns a [monoid.Monoid] for combining test assertion [Reader]s.
|
||||
//
|
||||
// This monoid combines multiple test assertions using logical AND (conjunction) semantics,
|
||||
// meaning all assertions must pass for the combined assertion to pass. It leverages the
|
||||
// applicative structure of Reader to execute multiple assertions with the same testing.T
|
||||
// context and combines their boolean results using boolean.MonoidAll (logical AND).
|
||||
//
|
||||
// The monoid provides:
|
||||
// - Concat: Combines two assertions such that both must pass (logical AND)
|
||||
// - Empty: Returns an assertion that always passes (identity element)
|
||||
//
|
||||
// This is particularly useful for:
|
||||
// - Composing multiple test assertions into a single assertion
|
||||
// - Building complex test conditions from simpler ones
|
||||
// - Creating reusable assertion combinators
|
||||
// - Implementing test assertion DSLs
|
||||
//
|
||||
// # Monoid Laws
|
||||
//
|
||||
// The returned monoid satisfies the standard monoid laws:
|
||||
//
|
||||
// 1. Associativity:
|
||||
// Concat(Concat(a1, a2), a3) ≡ Concat(a1, Concat(a2, a3))
|
||||
//
|
||||
// 2. Left Identity:
|
||||
// Concat(Empty(), a) ≡ a
|
||||
//
|
||||
// 3. Right Identity:
|
||||
// Concat(a, Empty()) ≡ a
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A [monoid.Monoid][Reader] that combines assertions using logical AND
|
||||
//
|
||||
// # Example - Basic Usage
|
||||
//
|
||||
// func TestUserValidation(t *testing.T) {
|
||||
// user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
|
||||
// m := assert.ApplicativeMonoid()
|
||||
//
|
||||
// // Combine multiple assertions
|
||||
// assertion := m.Concat(
|
||||
// assert.Equal("Alice")(user.Name),
|
||||
// m.Concat(
|
||||
// assert.Equal(30)(user.Age),
|
||||
// assert.StringNotEmpty(user.Email),
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
// // Execute combined assertion
|
||||
// assertion(t) // All three assertions must pass
|
||||
// }
|
||||
//
|
||||
// # Example - Building Reusable Validators
|
||||
//
|
||||
// func TestWithReusableValidators(t *testing.T) {
|
||||
// m := assert.ApplicativeMonoid()
|
||||
//
|
||||
// // Create a reusable validator
|
||||
// validateUser := func(u User) assert.Reader {
|
||||
// return m.Concat(
|
||||
// assert.StringNotEmpty(u.Name),
|
||||
// m.Concat(
|
||||
// assert.True(u.Age > 0),
|
||||
// assert.StringContains("@")(u.Email),
|
||||
// ),
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// user := User{Name: "Bob", Age: 25, Email: "bob@test.com"}
|
||||
// validateUser(user)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - Using Empty for Identity
|
||||
//
|
||||
// func TestEmptyIdentity(t *testing.T) {
|
||||
// m := assert.ApplicativeMonoid()
|
||||
// assertion := assert.Equal(42)(42)
|
||||
//
|
||||
// // Empty is the identity - these are equivalent
|
||||
// result1 := m.Concat(m.Empty(), assertion)(t)
|
||||
// result2 := m.Concat(assertion, m.Empty())(t)
|
||||
// result3 := assertion(t)
|
||||
// // All three produce the same result
|
||||
// }
|
||||
//
|
||||
// # Example - Combining with AllOf
|
||||
//
|
||||
// func TestCombiningWithAllOf(t *testing.T) {
|
||||
// // ApplicativeMonoid provides the underlying mechanism for AllOf
|
||||
// arr := []int{1, 2, 3, 4, 5}
|
||||
//
|
||||
// // These are conceptually equivalent:
|
||||
// m := assert.ApplicativeMonoid()
|
||||
// manual := m.Concat(
|
||||
// assert.ArrayNotEmpty(arr),
|
||||
// m.Concat(
|
||||
// assert.ArrayLength[int](5)(arr),
|
||||
// assert.ArrayContains(3)(arr),
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
// // AllOf uses ApplicativeMonoid internally
|
||||
// convenient := assert.AllOf([]assert.Reader{
|
||||
// assert.ArrayNotEmpty(arr),
|
||||
// assert.ArrayLength[int](5)(arr),
|
||||
// assert.ArrayContains(3)(arr),
|
||||
// })
|
||||
//
|
||||
// manual(t)
|
||||
// convenient(t)
|
||||
// }
|
||||
//
|
||||
// # Related Functions
|
||||
//
|
||||
// - [AllOf]: Convenient wrapper for combining multiple assertions using this monoid
|
||||
// - [boolean.MonoidAll]: The underlying boolean monoid (logical AND with true as identity)
|
||||
// - [reader.ApplicativeMonoid]: Generic applicative monoid for Reader types
|
||||
//
|
||||
// # References
|
||||
//
|
||||
// - Haskell Monoid: https://hackage.haskell.org/package/base/docs/Data-Monoid.html
|
||||
// - Applicative Functors: https://hackage.haskell.org/package/base/docs/Control-Applicative.html
|
||||
// - Boolean Monoid (All): https://hackage.haskell.org/package/base/docs/Data-Monoid.html#t:All
|
||||
func ApplicativeMonoid() monoid.Monoid[Reader] {
|
||||
return reader.ApplicativeMonoid[*testing.T](boolean.MonoidAll)
|
||||
}
|
||||
454
v2/assert/monoid_test.go
Normal file
454
v2/assert/monoid_test.go
Normal file
@@ -0,0 +1,454 @@
|
||||
// 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
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestApplicativeMonoid_Empty tests that Empty returns an assertion that always passes
|
||||
func TestApplicativeMonoid_Empty(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
empty := m.Empty()
|
||||
|
||||
result := empty(t)
|
||||
if !result {
|
||||
t.Error("Expected Empty() to return an assertion that always passes")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_Concat_BothPass tests that Concat returns true when both assertions pass
|
||||
func TestApplicativeMonoid_Concat_BothPass(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
assertion1 := Equal(42)(42)
|
||||
assertion2 := Equal("hello")("hello")
|
||||
|
||||
combined := m.Concat(assertion1, assertion2)
|
||||
result := combined(t)
|
||||
|
||||
if !result {
|
||||
t.Error("Expected Concat to pass when both assertions pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_Concat_FirstFails tests that Concat returns false when first assertion fails
|
||||
func TestApplicativeMonoid_Concat_FirstFails(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
assertion1 := Equal(42)(43) // This will fail
|
||||
assertion2 := Equal("hello")("hello")
|
||||
|
||||
combined := m.Concat(assertion1, assertion2)
|
||||
result := combined(mockT)
|
||||
|
||||
if result {
|
||||
t.Error("Expected Concat to fail when first assertion fails")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_Concat_SecondFails tests that Concat returns false when second assertion fails
|
||||
func TestApplicativeMonoid_Concat_SecondFails(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
assertion1 := Equal(42)(42)
|
||||
assertion2 := Equal("hello")("world") // This will fail
|
||||
|
||||
combined := m.Concat(assertion1, assertion2)
|
||||
result := combined(mockT)
|
||||
|
||||
if result {
|
||||
t.Error("Expected Concat to fail when second assertion fails")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_Concat_BothFail tests that Concat returns false when both assertions fail
|
||||
func TestApplicativeMonoid_Concat_BothFail(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
assertion1 := Equal(42)(43) // This will fail
|
||||
assertion2 := Equal("hello")("world") // This will fail
|
||||
|
||||
combined := m.Concat(assertion1, assertion2)
|
||||
result := combined(mockT)
|
||||
|
||||
if result {
|
||||
t.Error("Expected Concat to fail when both assertions fail")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_LeftIdentity tests the left identity law: Concat(Empty(), a) = a
|
||||
func TestApplicativeMonoid_LeftIdentity(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
assertion := Equal(42)(42)
|
||||
|
||||
// Concat(Empty(), assertion) should behave the same as assertion
|
||||
combined := m.Concat(m.Empty(), assertion)
|
||||
|
||||
result1 := assertion(t)
|
||||
result2 := combined(t)
|
||||
|
||||
if result1 != result2 {
|
||||
t.Error("Left identity law violated: Concat(Empty(), a) should equal a")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_RightIdentity tests the right identity law: Concat(a, Empty()) = a
|
||||
func TestApplicativeMonoid_RightIdentity(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
assertion := Equal(42)(42)
|
||||
|
||||
// Concat(assertion, Empty()) should behave the same as assertion
|
||||
combined := m.Concat(assertion, m.Empty())
|
||||
|
||||
result1 := assertion(t)
|
||||
result2 := combined(t)
|
||||
|
||||
if result1 != result2 {
|
||||
t.Error("Right identity law violated: Concat(a, Empty()) should equal a")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_Associativity tests the associativity law: Concat(Concat(a, b), c) = Concat(a, Concat(b, c))
|
||||
func TestApplicativeMonoid_Associativity(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
a1 := Equal(1)(1)
|
||||
a2 := Equal(2)(2)
|
||||
a3 := Equal(3)(3)
|
||||
|
||||
// Concat(Concat(a1, a2), a3)
|
||||
left := m.Concat(m.Concat(a1, a2), a3)
|
||||
|
||||
// Concat(a1, Concat(a2, a3))
|
||||
right := m.Concat(a1, m.Concat(a2, a3))
|
||||
|
||||
result1 := left(t)
|
||||
result2 := right(t)
|
||||
|
||||
if result1 != result2 {
|
||||
t.Error("Associativity law violated: Concat(Concat(a, b), c) should equal Concat(a, Concat(b, c))")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_AssociativityWithFailure tests associativity when assertions fail
|
||||
func TestApplicativeMonoid_AssociativityWithFailure(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
a1 := Equal(1)(1)
|
||||
a2 := Equal(2)(3) // This will fail
|
||||
a3 := Equal(3)(3)
|
||||
|
||||
// Concat(Concat(a1, a2), a3)
|
||||
left := m.Concat(m.Concat(a1, a2), a3)
|
||||
|
||||
// Concat(a1, Concat(a2, a3))
|
||||
right := m.Concat(a1, m.Concat(a2, a3))
|
||||
|
||||
result1 := left(mockT)
|
||||
result2 := right(mockT)
|
||||
|
||||
if result1 != result2 {
|
||||
t.Error("Associativity law violated even with failures")
|
||||
}
|
||||
|
||||
if result1 || result2 {
|
||||
t.Error("Expected both to fail when one assertion fails")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_ComplexAssertions tests combining complex assertions
|
||||
func TestApplicativeMonoid_ComplexAssertions(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
arr := []int{1, 2, 3, 4, 5}
|
||||
mp := map[string]int{"a": 1, "b": 2}
|
||||
|
||||
arrayAssertions := m.Concat(
|
||||
ArrayNotEmpty(arr),
|
||||
m.Concat(
|
||||
ArrayLength[int](5)(arr),
|
||||
ArrayContains(3)(arr),
|
||||
),
|
||||
)
|
||||
|
||||
mapAssertions := m.Concat(
|
||||
RecordNotEmpty(mp),
|
||||
RecordLength[string, int](2)(mp),
|
||||
)
|
||||
|
||||
combined := m.Concat(arrayAssertions, mapAssertions)
|
||||
|
||||
result := combined(t)
|
||||
if !result {
|
||||
t.Error("Expected complex combined assertions to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_ComplexAssertionsWithFailure tests complex assertions when one fails
|
||||
func TestApplicativeMonoid_ComplexAssertionsWithFailure(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
arr := []int{1, 2, 3}
|
||||
mp := map[string]int{"a": 1, "b": 2}
|
||||
|
||||
arrayAssertions := m.Concat(
|
||||
ArrayNotEmpty(arr),
|
||||
m.Concat(
|
||||
ArrayLength[int](5)(arr), // This will fail - array has 3 elements, not 5
|
||||
ArrayContains(3)(arr),
|
||||
),
|
||||
)
|
||||
|
||||
mapAssertions := m.Concat(
|
||||
RecordNotEmpty(mp),
|
||||
RecordLength[string, int](2)(mp),
|
||||
)
|
||||
|
||||
combined := m.Concat(arrayAssertions, mapAssertions)
|
||||
|
||||
result := combined(mockT)
|
||||
if result {
|
||||
t.Error("Expected complex combined assertions to fail when one assertion fails")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_MultipleConcat tests chaining multiple Concat operations
|
||||
func TestApplicativeMonoid_MultipleConcat(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
a1 := Equal(1)(1)
|
||||
a2 := Equal(2)(2)
|
||||
a3 := Equal(3)(3)
|
||||
a4 := Equal(4)(4)
|
||||
|
||||
combined := m.Concat(
|
||||
m.Concat(a1, a2),
|
||||
m.Concat(a3, a4),
|
||||
)
|
||||
|
||||
result := combined(t)
|
||||
if !result {
|
||||
t.Error("Expected multiple Concat operations to pass when all assertions pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_WithStringAssertions tests combining string assertions
|
||||
func TestApplicativeMonoid_WithStringAssertions(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
str := "hello world"
|
||||
|
||||
combined := m.Concat(
|
||||
StringNotEmpty(str),
|
||||
StringLength[any, any](11)(str),
|
||||
)
|
||||
|
||||
result := combined(t)
|
||||
if !result {
|
||||
t.Error("Expected string assertions to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_WithBooleanAssertions tests combining boolean assertions
|
||||
func TestApplicativeMonoid_WithBooleanAssertions(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
combined := m.Concat(
|
||||
Equal(true)(true),
|
||||
m.Concat(
|
||||
Equal(false)(false),
|
||||
Equal(true)(true),
|
||||
),
|
||||
)
|
||||
|
||||
result := combined(t)
|
||||
if !result {
|
||||
t.Error("Expected boolean assertions to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_WithErrorAssertions tests combining error assertions
|
||||
func TestApplicativeMonoid_WithErrorAssertions(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
combined := m.Concat(
|
||||
NoError(nil),
|
||||
Equal("test")("test"),
|
||||
)
|
||||
|
||||
result := combined(t)
|
||||
if !result {
|
||||
t.Error("Expected error assertions to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_EmptyWithMultipleConcat tests Empty with multiple Concat operations
|
||||
func TestApplicativeMonoid_EmptyWithMultipleConcat(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
assertion := Equal(42)(42)
|
||||
|
||||
// Multiple Empty values should still act as identity
|
||||
combined := m.Concat(
|
||||
m.Empty(),
|
||||
m.Concat(
|
||||
assertion,
|
||||
m.Empty(),
|
||||
),
|
||||
)
|
||||
|
||||
result1 := assertion(t)
|
||||
result2 := combined(t)
|
||||
|
||||
if result1 != result2 {
|
||||
t.Error("Multiple Empty values should still act as identity")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_OnlyEmpty tests using only Empty values
|
||||
func TestApplicativeMonoid_OnlyEmpty(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
// Concat of Empty values should still be Empty (identity)
|
||||
combined := m.Concat(m.Empty(), m.Empty())
|
||||
|
||||
result := combined(t)
|
||||
if !result {
|
||||
t.Error("Expected Concat of Empty values to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_RealWorldExample tests a realistic use case
|
||||
func TestApplicativeMonoid_RealWorldExample(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
Email string
|
||||
}
|
||||
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
validateUser := func(u User) Reader {
|
||||
return m.Concat(
|
||||
StringNotEmpty(u.Name),
|
||||
m.Concat(
|
||||
That(func(age int) bool { return age > 0 })(u.Age),
|
||||
m.Concat(
|
||||
That(func(age int) bool { return age < 150 })(u.Age),
|
||||
That(func(email string) bool {
|
||||
for _, ch := range email {
|
||||
if ch == '@' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})(u.Email),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
validUser := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
|
||||
result := validateUser(validUser)(t)
|
||||
|
||||
if !result {
|
||||
t.Error("Expected valid user to pass all validations")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_RealWorldExampleWithFailure tests a realistic use case with failure
|
||||
func TestApplicativeMonoid_RealWorldExampleWithFailure(t *testing.T) {
|
||||
mockT := &testing.T{}
|
||||
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
Email string
|
||||
}
|
||||
|
||||
m := ApplicativeMonoid()
|
||||
|
||||
validateUser := func(u User) Reader {
|
||||
return m.Concat(
|
||||
StringNotEmpty(u.Name),
|
||||
m.Concat(
|
||||
That(func(age int) bool { return age > 0 })(u.Age),
|
||||
m.Concat(
|
||||
That(func(age int) bool { return age < 150 })(u.Age),
|
||||
That(func(email string) bool {
|
||||
for _, ch := range email {
|
||||
if ch == '@' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})(u.Email),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
invalidUser := User{Name: "Bob", Age: 200, Email: "bob@test.com"} // Age > 150
|
||||
result := validateUser(invalidUser)(mockT)
|
||||
|
||||
if result {
|
||||
t.Error("Expected invalid user to fail validation")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicativeMonoid_IntegrationWithAllOf demonstrates relationship with AllOf
|
||||
func TestApplicativeMonoid_IntegrationWithAllOf(t *testing.T) {
|
||||
m := ApplicativeMonoid()
|
||||
arr := []int{1, 2, 3, 4, 5}
|
||||
|
||||
// Using ApplicativeMonoid directly
|
||||
manualCombination := m.Concat(
|
||||
ArrayNotEmpty(arr),
|
||||
m.Concat(
|
||||
ArrayLength[int](5)(arr),
|
||||
ArrayContains(3)(arr),
|
||||
),
|
||||
)
|
||||
|
||||
// Using AllOf (which uses ApplicativeMonoid internally)
|
||||
allOfCombination := AllOf([]Reader{
|
||||
ArrayNotEmpty(arr),
|
||||
ArrayLength[int](5)(arr),
|
||||
ArrayContains(3)(arr),
|
||||
})
|
||||
|
||||
result1 := manualCombination(t)
|
||||
result2 := allOfCombination(t)
|
||||
|
||||
if result1 != result2 {
|
||||
t.Error("Expected manual combination and AllOf to produce same result")
|
||||
}
|
||||
|
||||
if !result1 || !result2 {
|
||||
t.Error("Expected both combinations to pass")
|
||||
}
|
||||
}
|
||||
650
v2/assert/traverse.go
Normal file
650
v2/assert/traverse.go
Normal file
@@ -0,0 +1,650 @@
|
||||
// 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
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
// TraverseArray transforms an array of values into a test suite by applying a function
|
||||
// that generates named test cases for each element.
|
||||
//
|
||||
// This function enables data-driven testing where you have a collection of test inputs
|
||||
// and want to run a named subtest for each one. It follows the functional programming
|
||||
// pattern of "traverse" - transforming a collection while preserving structure and
|
||||
// accumulating effects (in this case, test execution).
|
||||
//
|
||||
// The function takes each element of the array, applies the provided function to generate
|
||||
// a [Pair] of (test name, test assertion), and runs each as a separate subtest using
|
||||
// Go's t.Run. All subtests must pass for the overall test to pass.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A function that takes a value of type T and returns a [Pair] containing:
|
||||
// - Head: The test name (string) for the subtest
|
||||
// - Tail: The test assertion ([Reader]) to execute
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A [Kleisli] function that takes an array of T and returns a [Reader] that:
|
||||
// - Executes each element as a named subtest
|
||||
// - Returns true only if all subtests pass
|
||||
// - Provides proper test isolation and reporting via t.Run
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Data-driven testing with multiple test cases
|
||||
// - Parameterized tests where each parameter gets its own subtest
|
||||
// - Testing collections where each element needs validation
|
||||
// - Property-based testing with generated test data
|
||||
//
|
||||
// # Example - Basic Data-Driven Testing
|
||||
//
|
||||
// func TestMathOperations(t *testing.T) {
|
||||
// type TestCase struct {
|
||||
// Input int
|
||||
// Expected int
|
||||
// }
|
||||
//
|
||||
// testCases := []TestCase{
|
||||
// {Input: 2, Expected: 4},
|
||||
// {Input: 3, Expected: 9},
|
||||
// {Input: 4, Expected: 16},
|
||||
// }
|
||||
//
|
||||
// square := func(n int) int { return n * n }
|
||||
//
|
||||
// traverse := assert.TraverseArray(func(tc TestCase) assert.Pair[string, assert.Reader] {
|
||||
// name := fmt.Sprintf("square(%d)=%d", tc.Input, tc.Expected)
|
||||
// assertion := assert.Equal(tc.Expected)(square(tc.Input))
|
||||
// return pair.MakePair(name, assertion)
|
||||
// })
|
||||
//
|
||||
// traverse(testCases)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - String Validation
|
||||
//
|
||||
// func TestStringValidation(t *testing.T) {
|
||||
// inputs := []string{"hello", "world", "test"}
|
||||
//
|
||||
// traverse := assert.TraverseArray(func(s string) assert.Pair[string, assert.Reader] {
|
||||
// return pair.MakePair(
|
||||
// fmt.Sprintf("validate_%s", s),
|
||||
// assert.AllOf([]assert.Reader{
|
||||
// assert.StringNotEmpty(s),
|
||||
// assert.That(func(str string) bool { return len(str) > 0 })(s),
|
||||
// }),
|
||||
// )
|
||||
// })
|
||||
//
|
||||
// traverse(inputs)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - Complex Object Testing
|
||||
//
|
||||
// func TestUsers(t *testing.T) {
|
||||
// type User struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// Email string
|
||||
// }
|
||||
//
|
||||
// users := []User{
|
||||
// {Name: "Alice", Age: 30, Email: "alice@example.com"},
|
||||
// {Name: "Bob", Age: 25, Email: "bob@example.com"},
|
||||
// }
|
||||
//
|
||||
// traverse := assert.TraverseArray(func(u User) assert.Pair[string, assert.Reader] {
|
||||
// return pair.MakePair(
|
||||
// fmt.Sprintf("user_%s", u.Name),
|
||||
// assert.AllOf([]assert.Reader{
|
||||
// assert.StringNotEmpty(u.Name),
|
||||
// assert.That(func(age int) bool { return age > 0 })(u.Age),
|
||||
// assert.That(func(email string) bool {
|
||||
// return len(email) > 0 && strings.Contains(email, "@")
|
||||
// })(u.Email),
|
||||
// }),
|
||||
// )
|
||||
// })
|
||||
//
|
||||
// traverse(users)(t)
|
||||
// }
|
||||
//
|
||||
// # Comparison with RunAll
|
||||
//
|
||||
// TraverseArray and [RunAll] serve similar purposes but differ in their approach:
|
||||
//
|
||||
// - TraverseArray: Generates test cases from an array of data
|
||||
//
|
||||
// - Input: Array of values + function to generate test cases
|
||||
//
|
||||
// - Use when: You have test data and need to generate test cases from it
|
||||
//
|
||||
// - RunAll: Executes pre-defined named test cases
|
||||
//
|
||||
// - Input: Map of test names to assertions
|
||||
//
|
||||
// - Use when: You have already defined test cases with names
|
||||
//
|
||||
// # Related Functions
|
||||
//
|
||||
// - [SequenceSeq2]: Similar but works with Go iterators (Seq2) instead of arrays
|
||||
// - [RunAll]: Executes a map of named test cases
|
||||
// - [AllOf]: Combines multiple assertions without subtests
|
||||
//
|
||||
// # References
|
||||
//
|
||||
// - Haskell traverse: https://hackage.haskell.org/package/base/docs/Data-Traversable.html#v:traverse
|
||||
// - Go subtests: https://go.dev/blog/subtests
|
||||
func TraverseArray[T any](f func(T) Pair[string, Reader]) Kleisli[[]T] {
|
||||
return func(ts []T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
ok := true
|
||||
for _, src := range ts {
|
||||
test := f(src)
|
||||
res := t.Run(pair.Head(test), func(t *testing.T) {
|
||||
pair.Tail(test)(t)
|
||||
})
|
||||
ok = ok && res
|
||||
}
|
||||
return ok
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SequenceSeq2 executes a sequence of named test cases provided as a Go iterator.
|
||||
//
|
||||
// This function takes a [Seq2] iterator that yields (name, assertion) pairs and
|
||||
// executes each as a separate subtest using Go's t.Run. It's similar to [TraverseArray]
|
||||
// but works directly with Go's iterator protocol (introduced in Go 1.23) rather than
|
||||
// requiring an array.
|
||||
//
|
||||
// The function iterates through all test cases, running each as a named subtest.
|
||||
// All subtests must pass for the overall test to pass. This provides proper test
|
||||
// isolation and clear reporting of which specific test cases fail.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - s: A [Seq2] iterator that yields pairs of:
|
||||
// - Key: Test name (string) for the subtest
|
||||
// - Value: Test assertion ([Reader]) to execute
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A [Reader] that:
|
||||
// - Executes each test case as a named subtest
|
||||
// - Returns true only if all subtests pass
|
||||
// - Provides proper test isolation via t.Run
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Working with iterator-based test data
|
||||
// - Lazy evaluation of test cases
|
||||
// - Integration with Go 1.23+ iterator patterns
|
||||
// - Memory-efficient testing of large test suites
|
||||
//
|
||||
// # Example - Basic Usage with Iterator
|
||||
//
|
||||
// func TestWithIterator(t *testing.T) {
|
||||
// // Create an iterator of test cases
|
||||
// testCases := func(yield func(string, assert.Reader) bool) {
|
||||
// if !yield("test_addition", assert.Equal(4)(2+2)) {
|
||||
// return
|
||||
// }
|
||||
// if !yield("test_subtraction", assert.Equal(1)(3-2)) {
|
||||
// return
|
||||
// }
|
||||
// if !yield("test_multiplication", assert.Equal(6)(2*3)) {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// assert.SequenceSeq2(testCases)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - Generated Test Cases
|
||||
//
|
||||
// func TestGeneratedCases(t *testing.T) {
|
||||
// // Generate test cases on the fly
|
||||
// generateTests := func(yield func(string, assert.Reader) bool) {
|
||||
// for i := 1; i <= 5; i++ {
|
||||
// name := fmt.Sprintf("test_%d", i)
|
||||
// assertion := assert.Equal(i*i)(i * i)
|
||||
// if !yield(name, assertion) {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// assert.SequenceSeq2(generateTests)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - Filtering Test Cases
|
||||
//
|
||||
// func TestFilteredCases(t *testing.T) {
|
||||
// type TestCase struct {
|
||||
// Name string
|
||||
// Input int
|
||||
// Expected int
|
||||
// Skip bool
|
||||
// }
|
||||
//
|
||||
// allCases := []TestCase{
|
||||
// {Name: "test1", Input: 2, Expected: 4, Skip: false},
|
||||
// {Name: "test2", Input: 3, Expected: 9, Skip: true},
|
||||
// {Name: "test3", Input: 4, Expected: 16, Skip: false},
|
||||
// }
|
||||
//
|
||||
// // Create iterator that filters out skipped tests
|
||||
// activeTests := func(yield func(string, assert.Reader) bool) {
|
||||
// for _, tc := range allCases {
|
||||
// if !tc.Skip {
|
||||
// assertion := assert.Equal(tc.Expected)(tc.Input * tc.Input)
|
||||
// if !yield(tc.Name, assertion) {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// assert.SequenceSeq2(activeTests)(t)
|
||||
// }
|
||||
//
|
||||
// # Comparison with TraverseArray
|
||||
//
|
||||
// SequenceSeq2 and [TraverseArray] serve similar purposes but differ in their input:
|
||||
//
|
||||
// - SequenceSeq2: Works with iterators (Seq2)
|
||||
//
|
||||
// - Input: Iterator yielding (name, assertion) pairs
|
||||
//
|
||||
// - Use when: Working with Go 1.23+ iterators or lazy evaluation
|
||||
//
|
||||
// - Memory: More efficient for large test suites (lazy evaluation)
|
||||
//
|
||||
// - TraverseArray: Works with arrays
|
||||
//
|
||||
// - Input: Array of values + transformation function
|
||||
//
|
||||
// - Use when: You have an array of test data
|
||||
//
|
||||
// - Memory: All test data must be in memory
|
||||
//
|
||||
// # Comparison with RunAll
|
||||
//
|
||||
// SequenceSeq2 and [RunAll] are very similar:
|
||||
//
|
||||
// - SequenceSeq2: Takes an iterator (Seq2)
|
||||
// - RunAll: Takes a map[string]Reader
|
||||
//
|
||||
// Both execute named test cases as subtests. Choose based on your data structure:
|
||||
// use SequenceSeq2 for iterators, RunAll for maps.
|
||||
//
|
||||
// # Related Functions
|
||||
//
|
||||
// - [TraverseArray]: Similar but works with arrays instead of iterators
|
||||
// - [RunAll]: Executes a map of named test cases
|
||||
// - [AllOf]: Combines multiple assertions without subtests
|
||||
//
|
||||
// # References
|
||||
//
|
||||
// - Go iterators: https://go.dev/blog/range-functions
|
||||
// - Go subtests: https://go.dev/blog/subtests
|
||||
// - Haskell sequence: https://hackage.haskell.org/package/base/docs/Data-Traversable.html#v:sequence
|
||||
func SequenceSeq2[T any](s Seq2[string, Reader]) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
ok := true
|
||||
for name, test := range s {
|
||||
res := t.Run(name, func(t *testing.T) {
|
||||
test(t)
|
||||
})
|
||||
ok = ok && res
|
||||
}
|
||||
return ok
|
||||
}
|
||||
}
|
||||
|
||||
// TraverseRecord transforms a map of values into a test suite by applying a function
|
||||
// that generates test assertions for each map entry.
|
||||
//
|
||||
// This function enables data-driven testing where you have a map of test data and want
|
||||
// to run a named subtest for each entry. The map keys become test names, and the function
|
||||
// transforms each value into a test assertion. It follows the functional programming
|
||||
// pattern of "traverse" - transforming a collection while preserving structure and
|
||||
// accumulating effects (in this case, test execution).
|
||||
//
|
||||
// The function takes each key-value pair from the map, applies the provided function to
|
||||
// generate a [Reader] assertion, and runs each as a separate subtest using Go's t.Run.
|
||||
// All subtests must pass for the overall test to pass.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A [Kleisli] function that takes a value of type T and returns a [Reader] assertion
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A [Kleisli] function that takes a map[string]T and returns a [Reader] that:
|
||||
// - Executes each map entry as a named subtest (using the key as the test name)
|
||||
// - Returns true only if all subtests pass
|
||||
// - Provides proper test isolation and reporting via t.Run
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Data-driven testing with named test cases in a map
|
||||
// - Testing configuration maps where keys are meaningful names
|
||||
// - Validating collections where natural keys exist
|
||||
// - Property-based testing with named scenarios
|
||||
//
|
||||
// # Example - Basic Configuration Testing
|
||||
//
|
||||
// func TestConfigurations(t *testing.T) {
|
||||
// configs := map[string]int{
|
||||
// "timeout": 30,
|
||||
// "maxRetries": 3,
|
||||
// "bufferSize": 1024,
|
||||
// }
|
||||
//
|
||||
// validatePositive := assert.That(func(n int) bool { return n > 0 })
|
||||
//
|
||||
// traverse := assert.TraverseRecord(validatePositive)
|
||||
// traverse(configs)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - User Validation
|
||||
//
|
||||
// func TestUserMap(t *testing.T) {
|
||||
// type User struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// users := map[string]User{
|
||||
// "alice": {Name: "Alice", Age: 30},
|
||||
// "bob": {Name: "Bob", Age: 25},
|
||||
// "carol": {Name: "Carol", Age: 35},
|
||||
// }
|
||||
//
|
||||
// validateUser := func(u User) assert.Reader {
|
||||
// return assert.AllOf([]assert.Reader{
|
||||
// assert.StringNotEmpty(u.Name),
|
||||
// assert.That(func(age int) bool { return age > 0 && age < 150 })(u.Age),
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// traverse := assert.TraverseRecord(validateUser)
|
||||
// traverse(users)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - API Endpoint Testing
|
||||
//
|
||||
// func TestEndpoints(t *testing.T) {
|
||||
// type Endpoint struct {
|
||||
// Path string
|
||||
// Method string
|
||||
// }
|
||||
//
|
||||
// endpoints := map[string]Endpoint{
|
||||
// "get_users": {Path: "/api/users", Method: "GET"},
|
||||
// "create_user": {Path: "/api/users", Method: "POST"},
|
||||
// "delete_user": {Path: "/api/users/:id", Method: "DELETE"},
|
||||
// }
|
||||
//
|
||||
// validateEndpoint := func(e Endpoint) assert.Reader {
|
||||
// return assert.AllOf([]assert.Reader{
|
||||
// assert.StringNotEmpty(e.Path),
|
||||
// assert.That(func(path string) bool {
|
||||
// return strings.HasPrefix(path, "/api/")
|
||||
// })(e.Path),
|
||||
// assert.That(func(method string) bool {
|
||||
// return method == "GET" || method == "POST" ||
|
||||
// method == "PUT" || method == "DELETE"
|
||||
// })(e.Method),
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// traverse := assert.TraverseRecord(validateEndpoint)
|
||||
// traverse(endpoints)(t)
|
||||
// }
|
||||
//
|
||||
// # Comparison with TraverseArray
|
||||
//
|
||||
// TraverseRecord and [TraverseArray] serve similar purposes but differ in their input:
|
||||
//
|
||||
// - TraverseRecord: Works with maps (records)
|
||||
//
|
||||
// - Input: Map with string keys + transformation function
|
||||
//
|
||||
// - Use when: You have named test data in a map
|
||||
//
|
||||
// - Test names: Derived from map keys
|
||||
//
|
||||
// - TraverseArray: Works with arrays
|
||||
//
|
||||
// - Input: Array of values + function that generates names and assertions
|
||||
//
|
||||
// - Use when: You have sequential test data
|
||||
//
|
||||
// - Test names: Generated by the transformation function
|
||||
//
|
||||
// # Comparison with SequenceRecord
|
||||
//
|
||||
// TraverseRecord and [SequenceRecord] are closely related:
|
||||
//
|
||||
// - TraverseRecord: Transforms values into assertions
|
||||
//
|
||||
// - Input: map[string]T + function T -> Reader
|
||||
//
|
||||
// - Use when: You need to transform data before asserting
|
||||
//
|
||||
// - SequenceRecord: Executes pre-defined assertions
|
||||
//
|
||||
// - Input: map[string]Reader
|
||||
//
|
||||
// - Use when: Assertions are already defined
|
||||
//
|
||||
// # Related Functions
|
||||
//
|
||||
// - [SequenceRecord]: Similar but takes pre-defined assertions
|
||||
// - [TraverseArray]: Similar but works with arrays
|
||||
// - [RunAll]: Alias for SequenceRecord
|
||||
//
|
||||
// # References
|
||||
//
|
||||
// - Haskell traverse: https://hackage.haskell.org/package/base/docs/Data-Traversable.html#v:traverse
|
||||
// - Go subtests: https://go.dev/blog/subtests
|
||||
func TraverseRecord[T any](f Kleisli[T]) Kleisli[map[string]T] {
|
||||
return func(m map[string]T) Reader {
|
||||
return func(t *testing.T) bool {
|
||||
ok := true
|
||||
for name, src := range m {
|
||||
res := t.Run(name, func(t *testing.T) {
|
||||
f(src)(t)
|
||||
})
|
||||
ok = ok && res
|
||||
}
|
||||
return ok
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SequenceRecord executes a map of named test cases as subtests.
|
||||
//
|
||||
// This function takes a map where keys are test names and values are test assertions
|
||||
// ([Reader]), and executes each as a separate subtest using Go's t.Run. It's the
|
||||
// record (map) equivalent of [SequenceSeq2] and is actually aliased as [RunAll] for
|
||||
// convenience.
|
||||
//
|
||||
// The function iterates through all map entries, running each as a named subtest.
|
||||
// All subtests must pass for the overall test to pass. This provides proper test
|
||||
// isolation and clear reporting of which specific test cases fail.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - m: A map[string]Reader where:
|
||||
// - Keys: Test names (strings) for the subtests
|
||||
// - Values: Test assertions ([Reader]) to execute
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A [Reader] that:
|
||||
// - Executes each map entry as a named subtest
|
||||
// - Returns true only if all subtests pass
|
||||
// - Provides proper test isolation via t.Run
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Executing a collection of pre-defined named test cases
|
||||
// - Organizing related tests in a map structure
|
||||
// - Running multiple assertions with descriptive names
|
||||
// - Building test suites programmatically
|
||||
//
|
||||
// # Example - Basic Named Tests
|
||||
//
|
||||
// func TestMathOperations(t *testing.T) {
|
||||
// tests := map[string]assert.Reader{
|
||||
// "addition": assert.Equal(4)(2 + 2),
|
||||
// "subtraction": assert.Equal(1)(3 - 2),
|
||||
// "multiplication": assert.Equal(6)(2 * 3),
|
||||
// "division": assert.Equal(2)(6 / 3),
|
||||
// }
|
||||
//
|
||||
// assert.SequenceRecord(tests)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - String Validation Suite
|
||||
//
|
||||
// func TestStringValidations(t *testing.T) {
|
||||
// testString := "hello world"
|
||||
//
|
||||
// tests := map[string]assert.Reader{
|
||||
// "not_empty": assert.StringNotEmpty(testString),
|
||||
// "correct_length": assert.StringLength[any, any](11)(testString),
|
||||
// "has_space": assert.That(func(s string) bool {
|
||||
// return strings.Contains(s, " ")
|
||||
// })(testString),
|
||||
// "lowercase": assert.That(func(s string) bool {
|
||||
// return s == strings.ToLower(s)
|
||||
// })(testString),
|
||||
// }
|
||||
//
|
||||
// assert.SequenceRecord(tests)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - Complex Object Validation
|
||||
//
|
||||
// func TestUserValidation(t *testing.T) {
|
||||
// type User struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// Email string
|
||||
// }
|
||||
//
|
||||
// user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
|
||||
//
|
||||
// tests := map[string]assert.Reader{
|
||||
// "name_not_empty": assert.StringNotEmpty(user.Name),
|
||||
// "age_positive": assert.That(func(age int) bool { return age > 0 })(user.Age),
|
||||
// "age_reasonable": assert.That(func(age int) bool { return age < 150 })(user.Age),
|
||||
// "email_valid": assert.That(func(email string) bool {
|
||||
// return strings.Contains(email, "@") && strings.Contains(email, ".")
|
||||
// })(user.Email),
|
||||
// }
|
||||
//
|
||||
// assert.SequenceRecord(tests)(t)
|
||||
// }
|
||||
//
|
||||
// # Example - Array Validation Suite
|
||||
//
|
||||
// func TestArrayValidations(t *testing.T) {
|
||||
// numbers := []int{1, 2, 3, 4, 5}
|
||||
//
|
||||
// tests := map[string]assert.Reader{
|
||||
// "not_empty": assert.ArrayNotEmpty(numbers),
|
||||
// "correct_length": assert.ArrayLength[int](5)(numbers),
|
||||
// "contains_three": assert.ArrayContains(3)(numbers),
|
||||
// "all_positive": assert.That(func(arr []int) bool {
|
||||
// for _, n := range arr {
|
||||
// if n <= 0 {
|
||||
// return false
|
||||
// }
|
||||
// }
|
||||
// return true
|
||||
// })(numbers),
|
||||
// }
|
||||
//
|
||||
// assert.SequenceRecord(tests)(t)
|
||||
// }
|
||||
//
|
||||
// # Comparison with TraverseRecord
|
||||
//
|
||||
// SequenceRecord and [TraverseRecord] are closely related:
|
||||
//
|
||||
// - SequenceRecord: Executes pre-defined assertions
|
||||
//
|
||||
// - Input: map[string]Reader (assertions already created)
|
||||
//
|
||||
// - Use when: You have already defined test cases with assertions
|
||||
//
|
||||
// - TraverseRecord: Transforms values into assertions
|
||||
//
|
||||
// - Input: map[string]T + function T -> Reader
|
||||
//
|
||||
// - Use when: You need to transform data before asserting
|
||||
//
|
||||
// # Comparison with SequenceSeq2
|
||||
//
|
||||
// SequenceRecord and [SequenceSeq2] serve similar purposes but differ in their input:
|
||||
//
|
||||
// - SequenceRecord: Works with maps
|
||||
//
|
||||
// - Input: map[string]Reader
|
||||
//
|
||||
// - Use when: You have named test cases in a map
|
||||
//
|
||||
// - Iteration order: Non-deterministic (map iteration)
|
||||
//
|
||||
// - SequenceSeq2: Works with iterators
|
||||
//
|
||||
// - Input: Seq2[string, Reader]
|
||||
//
|
||||
// - Use when: You have test cases in an iterator
|
||||
//
|
||||
// - Iteration order: Deterministic (iterator order)
|
||||
//
|
||||
// # Note on Map Iteration Order
|
||||
//
|
||||
// Go maps have non-deterministic iteration order. If test execution order matters,
|
||||
// consider using [SequenceSeq2] with an iterator that provides deterministic ordering,
|
||||
// or use [TraverseArray] with a slice of test cases.
|
||||
//
|
||||
// # Related Functions
|
||||
//
|
||||
// - [RunAll]: Alias for SequenceRecord
|
||||
// - [TraverseRecord]: Similar but transforms values into assertions
|
||||
// - [SequenceSeq2]: Similar but works with iterators
|
||||
// - [TraverseArray]: Similar but works with arrays
|
||||
//
|
||||
// # References
|
||||
//
|
||||
// - Go subtests: https://go.dev/blog/subtests
|
||||
// - Haskell sequence: https://hackage.haskell.org/package/base/docs/Data-Traversable.html#v:sequence
|
||||
func SequenceRecord(m map[string]Reader) Reader {
|
||||
return TraverseRecord(reader.Ask[Reader]())(m)
|
||||
}
|
||||
960
v2/assert/traverse_test.go
Normal file
960
v2/assert/traverse_test.go
Normal file
@@ -0,0 +1,960 @@
|
||||
// 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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
// TestTraverseArray_EmptyArray tests that TraverseArray handles empty arrays correctly
|
||||
func TestTraverseArray_EmptyArray(t *testing.T) {
|
||||
traverse := TraverseArray(func(n int) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("test_%d", n),
|
||||
Equal(n)(n),
|
||||
)
|
||||
})
|
||||
|
||||
result := traverse([]int{})(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseArray to pass with empty array")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_SingleElement tests TraverseArray with a single element
|
||||
func TestTraverseArray_SingleElement(t *testing.T) {
|
||||
traverse := TraverseArray(func(n int) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("test_%d", n),
|
||||
Equal(n*2)(n*2),
|
||||
)
|
||||
})
|
||||
|
||||
result := traverse([]int{5})(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseArray to pass with single element")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_MultipleElements tests TraverseArray with multiple passing elements
|
||||
func TestTraverseArray_MultipleElements(t *testing.T) {
|
||||
traverse := TraverseArray(func(n int) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("square_%d", n),
|
||||
Equal(n*n)(n*n),
|
||||
)
|
||||
})
|
||||
|
||||
result := traverse([]int{1, 2, 3, 4, 5})(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseArray to pass with all passing elements")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_WithFailure tests that TraverseArray fails when one element fails
|
||||
func TestTraverseArray_WithFailure(t *testing.T) {
|
||||
t.Skip("Skipping test that intentionally creates failing subtests")
|
||||
traverse := TraverseArray(func(n int) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("test_%d", n),
|
||||
Equal(10)(n), // Will fail for all except 10
|
||||
)
|
||||
})
|
||||
|
||||
// Run in a subtest - we expect the subtests to fail, so t.Run returns false
|
||||
result := traverse([]int{1, 2, 3})(t)
|
||||
|
||||
// The traverse should return false because assertions fail
|
||||
if result {
|
||||
t.Error("Expected traverse to return false when elements don't match")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_MixedResults tests TraverseArray with some passing and some failing
|
||||
func TestTraverseArray_MixedResults(t *testing.T) {
|
||||
t.Skip("Skipping test that intentionally creates failing subtests")
|
||||
traverse := TraverseArray(func(n int) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("is_even_%d", n),
|
||||
Equal(0)(n%2), // Only passes for even numbers
|
||||
)
|
||||
})
|
||||
|
||||
result := traverse([]int{2, 3, 4})(t) // 3 is odd, should fail
|
||||
|
||||
// The traverse should return false because one assertion fails
|
||||
if result {
|
||||
t.Error("Expected traverse to return false when some elements fail")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_StringData tests TraverseArray with string data
|
||||
func TestTraverseArray_StringData(t *testing.T) {
|
||||
words := []string{"hello", "world", "test"}
|
||||
|
||||
traverse := TraverseArray(func(s string) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("validate_%s", s),
|
||||
AllOf([]Reader{
|
||||
StringNotEmpty(s),
|
||||
That(func(str string) bool { return len(str) > 0 })(s),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
result := traverse(words)(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseArray to pass with valid strings")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_ComplexObjects tests TraverseArray with complex objects
|
||||
func TestTraverseArray_ComplexObjects(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
users := []User{
|
||||
{Name: "Alice", Age: 30},
|
||||
{Name: "Bob", Age: 25},
|
||||
{Name: "Charlie", Age: 35},
|
||||
}
|
||||
|
||||
traverse := TraverseArray(func(u User) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("user_%s", u.Name),
|
||||
AllOf([]Reader{
|
||||
StringNotEmpty(u.Name),
|
||||
That(func(age int) bool { return age > 0 && age < 150 })(u.Age),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
result := traverse(users)(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseArray to pass with valid users")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_ComplexObjectsWithFailure tests TraverseArray with invalid complex objects
|
||||
func TestTraverseArray_ComplexObjectsWithFailure(t *testing.T) {
|
||||
t.Skip("Skipping test that intentionally creates failing subtests")
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
users := []User{
|
||||
{Name: "Alice", Age: 30},
|
||||
{Name: "", Age: 25}, // Invalid: empty name
|
||||
{Name: "Charlie", Age: 35},
|
||||
}
|
||||
|
||||
traverse := TraverseArray(func(u User) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("user_%s", u.Name),
|
||||
AllOf([]Reader{
|
||||
StringNotEmpty(u.Name),
|
||||
That(func(age int) bool { return age > 0 })(u.Age),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
result := traverse(users)(t)
|
||||
|
||||
// The traverse should return false because one user is invalid
|
||||
if result {
|
||||
t.Error("Expected traverse to return false with invalid user")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_DataDrivenTesting demonstrates data-driven testing pattern
|
||||
func TestTraverseArray_DataDrivenTesting(t *testing.T) {
|
||||
type TestCase struct {
|
||||
Input int
|
||||
Expected int
|
||||
}
|
||||
|
||||
testCases := []TestCase{
|
||||
{Input: 2, Expected: 4},
|
||||
{Input: 3, Expected: 9},
|
||||
{Input: 4, Expected: 16},
|
||||
{Input: 5, Expected: 25},
|
||||
}
|
||||
|
||||
square := func(n int) int { return n * n }
|
||||
|
||||
traverse := TraverseArray(func(tc TestCase) Pair[string, Reader] {
|
||||
return pair.MakePair(
|
||||
fmt.Sprintf("square(%d)=%d", tc.Input, tc.Expected),
|
||||
Equal(tc.Expected)(square(tc.Input)),
|
||||
)
|
||||
})
|
||||
|
||||
result := traverse(testCases)(t)
|
||||
if !result {
|
||||
t.Error("Expected all test cases to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_EmptySequence tests that SequenceSeq2 handles empty sequences correctly
|
||||
func TestSequenceSeq2_EmptySequence(t *testing.T) {
|
||||
emptySeq := func(yield func(string, Reader) bool) {
|
||||
// Empty - yields nothing
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](emptySeq)(t)
|
||||
if !result {
|
||||
t.Error("Expected SequenceSeq2 to pass with empty sequence")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_SingleTest tests SequenceSeq2 with a single test
|
||||
func TestSequenceSeq2_SingleTest(t *testing.T) {
|
||||
singleSeq := func(yield func(string, Reader) bool) {
|
||||
yield("test_one", Equal(42)(42))
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](singleSeq)(t)
|
||||
if !result {
|
||||
t.Error("Expected SequenceSeq2 to pass with single test")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_MultipleTests tests SequenceSeq2 with multiple passing tests
|
||||
func TestSequenceSeq2_MultipleTests(t *testing.T) {
|
||||
multiSeq := func(yield func(string, Reader) bool) {
|
||||
if !yield("test_addition", Equal(4)(2+2)) {
|
||||
return
|
||||
}
|
||||
if !yield("test_subtraction", Equal(1)(3-2)) {
|
||||
return
|
||||
}
|
||||
if !yield("test_multiplication", Equal(6)(2*3)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](multiSeq)(t)
|
||||
if !result {
|
||||
t.Error("Expected SequenceSeq2 to pass with all passing tests")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_WithFailure tests that SequenceSeq2 fails when one test fails
|
||||
func TestSequenceSeq2_WithFailure(t *testing.T) {
|
||||
t.Skip("Skipping test that intentionally creates failing subtests")
|
||||
failSeq := func(yield func(string, Reader) bool) {
|
||||
if !yield("test_pass", Equal(4)(2+2)) {
|
||||
return
|
||||
}
|
||||
if !yield("test_fail", Equal(5)(2+2)) { // This will fail
|
||||
return
|
||||
}
|
||||
if !yield("test_pass2", Equal(6)(2*3)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](failSeq)(t)
|
||||
|
||||
// The sequence should return false because one test fails
|
||||
if result {
|
||||
t.Error("Expected sequence to return false when one test fails")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_GeneratedTests tests SequenceSeq2 with generated test cases
|
||||
func TestSequenceSeq2_GeneratedTests(t *testing.T) {
|
||||
generateTests := func(yield func(string, Reader) bool) {
|
||||
for i := 1; i <= 5; i++ {
|
||||
name := fmt.Sprintf("test_%d", i)
|
||||
assertion := Equal(i * i)(i * i)
|
||||
if !yield(name, assertion) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](generateTests)(t)
|
||||
if !result {
|
||||
t.Error("Expected all generated tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_StringTests tests SequenceSeq2 with string assertions
|
||||
func TestSequenceSeq2_StringTests(t *testing.T) {
|
||||
stringSeq := func(yield func(string, Reader) bool) {
|
||||
if !yield("test_hello", StringNotEmpty("hello")) {
|
||||
return
|
||||
}
|
||||
if !yield("test_world", StringNotEmpty("world")) {
|
||||
return
|
||||
}
|
||||
if !yield("test_length", StringLength[any, any](5)("hello")) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](stringSeq)(t)
|
||||
if !result {
|
||||
t.Error("Expected all string tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_ArrayTests tests SequenceSeq2 with array assertions
|
||||
func TestSequenceSeq2_ArrayTests(t *testing.T) {
|
||||
arr := []int{1, 2, 3, 4, 5}
|
||||
|
||||
arraySeq := func(yield func(string, Reader) bool) {
|
||||
if !yield("test_not_empty", ArrayNotEmpty(arr)) {
|
||||
return
|
||||
}
|
||||
if !yield("test_length", ArrayLength[int](5)(arr)) {
|
||||
return
|
||||
}
|
||||
if !yield("test_contains", ArrayContains(3)(arr)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](arraySeq)(t)
|
||||
if !result {
|
||||
t.Error("Expected all array tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_ComplexAssertions tests SequenceSeq2 with complex combined assertions
|
||||
func TestSequenceSeq2_ComplexAssertions(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
Email string
|
||||
}
|
||||
|
||||
user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
|
||||
|
||||
userSeq := func(yield func(string, Reader) bool) {
|
||||
if !yield("test_name", StringNotEmpty(user.Name)) {
|
||||
return
|
||||
}
|
||||
if !yield("test_age", That(func(age int) bool { return age > 0 && age < 150 })(user.Age)) {
|
||||
return
|
||||
}
|
||||
if !yield("test_email", That(func(email string) bool {
|
||||
for _, ch := range email {
|
||||
if ch == '@' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})(user.Email)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](userSeq)(t)
|
||||
if !result {
|
||||
t.Error("Expected all user validation tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_EarlyTermination tests that SequenceSeq2 respects early termination
|
||||
func TestSequenceSeq2_EarlyTermination(t *testing.T) {
|
||||
executionCount := 0
|
||||
|
||||
earlyTermSeq := func(yield func(string, Reader) bool) {
|
||||
executionCount++
|
||||
if !yield("test_1", Equal(1)(1)) {
|
||||
return
|
||||
}
|
||||
executionCount++
|
||||
if !yield("test_2", Equal(2)(2)) {
|
||||
return
|
||||
}
|
||||
executionCount++
|
||||
// This should execute even though we don't check the return
|
||||
yield("test_3", Equal(3)(3))
|
||||
executionCount++
|
||||
}
|
||||
|
||||
SequenceSeq2[Reader](earlyTermSeq)(t)
|
||||
|
||||
// All iterations should execute since we're not terminating early
|
||||
if executionCount != 4 {
|
||||
t.Errorf("Expected 4 executions, got %d", executionCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceSeq2_WithMapConversion demonstrates converting a map to Seq2
|
||||
func TestSequenceSeq2_WithMapConversion(t *testing.T) {
|
||||
testMap := map[string]Reader{
|
||||
"test_addition": Equal(4)(2 + 2),
|
||||
"test_multiplication": Equal(6)(2 * 3),
|
||||
"test_subtraction": Equal(1)(3 - 2),
|
||||
}
|
||||
|
||||
// Convert map to Seq2
|
||||
mapSeq := func(yield func(string, Reader) bool) {
|
||||
for name, assertion := range testMap {
|
||||
if !yield(name, assertion) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := SequenceSeq2[Reader](mapSeq)(t)
|
||||
if !result {
|
||||
t.Error("Expected all map-based tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseArray_vs_SequenceSeq2 demonstrates the relationship between the two functions
|
||||
func TestTraverseArray_vs_SequenceSeq2(t *testing.T) {
|
||||
type TestCase struct {
|
||||
Name string
|
||||
Input int
|
||||
Expected int
|
||||
}
|
||||
|
||||
testCases := []TestCase{
|
||||
{Name: "test_1", Input: 2, Expected: 4},
|
||||
{Name: "test_2", Input: 3, Expected: 9},
|
||||
{Name: "test_3", Input: 4, Expected: 16},
|
||||
}
|
||||
|
||||
// Using TraverseArray
|
||||
traverseResult := TraverseArray(func(tc TestCase) Pair[string, Reader] {
|
||||
return pair.MakePair(tc.Name, Equal(tc.Expected)(tc.Input*tc.Input))
|
||||
})(testCases)(t)
|
||||
|
||||
// Using SequenceSeq2
|
||||
seqResult := SequenceSeq2[Reader](func(yield func(string, Reader) bool) {
|
||||
for _, tc := range testCases {
|
||||
if !yield(tc.Name, Equal(tc.Expected)(tc.Input*tc.Input)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
})(t)
|
||||
|
||||
if traverseResult != seqResult {
|
||||
t.Error("Expected TraverseArray and SequenceSeq2 to produce same result")
|
||||
}
|
||||
|
||||
if !traverseResult || !seqResult {
|
||||
t.Error("Expected both approaches to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_EmptyMap tests that TraverseRecord handles empty maps correctly
|
||||
func TestTraverseRecord_EmptyMap(t *testing.T) {
|
||||
traverse := TraverseRecord(func(n int) Reader {
|
||||
return Equal(n)(n)
|
||||
})
|
||||
|
||||
result := traverse(map[string]int{})(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseRecord to pass with empty map")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_SingleEntry tests TraverseRecord with a single map entry
|
||||
func TestTraverseRecord_SingleEntry(t *testing.T) {
|
||||
traverse := TraverseRecord(func(n int) Reader {
|
||||
return Equal(n * 2)(n * 2)
|
||||
})
|
||||
|
||||
result := traverse(map[string]int{"test_5": 5})(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseRecord to pass with single entry")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_MultipleEntries tests TraverseRecord with multiple passing entries
|
||||
func TestTraverseRecord_MultipleEntries(t *testing.T) {
|
||||
traverse := TraverseRecord(func(n int) Reader {
|
||||
return Equal(n * n)(n * n)
|
||||
})
|
||||
|
||||
result := traverse(map[string]int{
|
||||
"square_1": 1,
|
||||
"square_2": 2,
|
||||
"square_3": 3,
|
||||
"square_4": 4,
|
||||
"square_5": 5,
|
||||
})(t)
|
||||
|
||||
if !result {
|
||||
t.Error("Expected TraverseRecord to pass with all passing entries")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_WithFailure tests that TraverseRecord fails when one entry fails
|
||||
func TestTraverseRecord_WithFailure(t *testing.T) {
|
||||
t.Skip("Skipping test that intentionally creates failing subtests")
|
||||
traverse := TraverseRecord(func(n int) Reader {
|
||||
return Equal(10)(n) // Will fail for all except 10
|
||||
})
|
||||
|
||||
result := traverse(map[string]int{
|
||||
"test_1": 1,
|
||||
"test_2": 2,
|
||||
"test_3": 3,
|
||||
})(t)
|
||||
|
||||
// The traverse should return false because entries don't match
|
||||
if result {
|
||||
t.Error("Expected traverse to return false when entries don't match")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_MixedResults tests TraverseRecord with some passing and some failing
|
||||
func TestTraverseRecord_MixedResults(t *testing.T) {
|
||||
t.Skip("Skipping test that intentionally creates failing subtests")
|
||||
traverse := TraverseRecord(func(n int) Reader {
|
||||
return Equal(0)(n % 2) // Only passes for even numbers
|
||||
})
|
||||
|
||||
result := traverse(map[string]int{
|
||||
"even_2": 2,
|
||||
"odd_3": 3,
|
||||
"even_4": 4,
|
||||
})(t)
|
||||
|
||||
// The traverse should return false because some entries fail
|
||||
if result {
|
||||
t.Error("Expected traverse to return false when some entries fail")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_StringData tests TraverseRecord with string data
|
||||
func TestTraverseRecord_StringData(t *testing.T) {
|
||||
words := map[string]string{
|
||||
"greeting": "hello",
|
||||
"world": "world",
|
||||
"test": "test",
|
||||
}
|
||||
|
||||
traverse := TraverseRecord(func(s string) Reader {
|
||||
return AllOf([]Reader{
|
||||
StringNotEmpty(s),
|
||||
That(func(str string) bool { return len(str) > 0 })(s),
|
||||
})
|
||||
})
|
||||
|
||||
result := traverse(words)(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseRecord to pass with valid strings")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_ComplexObjects tests TraverseRecord with complex objects
|
||||
func TestTraverseRecord_ComplexObjects(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
users := map[string]User{
|
||||
"alice": {Name: "Alice", Age: 30},
|
||||
"bob": {Name: "Bob", Age: 25},
|
||||
"charlie": {Name: "Charlie", Age: 35},
|
||||
}
|
||||
|
||||
traverse := TraverseRecord(func(u User) Reader {
|
||||
return AllOf([]Reader{
|
||||
StringNotEmpty(u.Name),
|
||||
That(func(age int) bool { return age > 0 && age < 150 })(u.Age),
|
||||
})
|
||||
})
|
||||
|
||||
result := traverse(users)(t)
|
||||
if !result {
|
||||
t.Error("Expected TraverseRecord to pass with valid users")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_ComplexObjectsWithFailure tests TraverseRecord with invalid complex objects
|
||||
func TestTraverseRecord_ComplexObjectsWithFailure(t *testing.T) {
|
||||
t.Skip("Skipping test that intentionally creates failing subtests")
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
users := map[string]User{
|
||||
"alice": {Name: "Alice", Age: 30},
|
||||
"invalid": {Name: "", Age: 25}, // Invalid: empty name
|
||||
"charlie": {Name: "Charlie", Age: 35},
|
||||
}
|
||||
|
||||
traverse := TraverseRecord(func(u User) Reader {
|
||||
return AllOf([]Reader{
|
||||
StringNotEmpty(u.Name),
|
||||
That(func(age int) bool { return age > 0 })(u.Age),
|
||||
})
|
||||
})
|
||||
|
||||
result := traverse(users)(t)
|
||||
|
||||
// The traverse should return false because one user is invalid
|
||||
if result {
|
||||
t.Error("Expected traverse to return false with invalid user")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_ConfigurationTesting demonstrates configuration testing pattern
|
||||
func TestTraverseRecord_ConfigurationTesting(t *testing.T) {
|
||||
configs := map[string]int{
|
||||
"timeout": 30,
|
||||
"maxRetries": 3,
|
||||
"bufferSize": 1024,
|
||||
}
|
||||
|
||||
validatePositive := That(func(n int) bool { return n > 0 })
|
||||
|
||||
traverse := TraverseRecord(validatePositive)
|
||||
result := traverse(configs)(t)
|
||||
|
||||
if !result {
|
||||
t.Error("Expected all configuration values to be positive")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_APIEndpointTesting demonstrates API endpoint testing pattern
|
||||
func TestTraverseRecord_APIEndpointTesting(t *testing.T) {
|
||||
type Endpoint struct {
|
||||
Path string
|
||||
Method string
|
||||
}
|
||||
|
||||
endpoints := map[string]Endpoint{
|
||||
"get_users": {Path: "/api/users", Method: "GET"},
|
||||
"create_user": {Path: "/api/users", Method: "POST"},
|
||||
"delete_user": {Path: "/api/users/:id", Method: "DELETE"},
|
||||
}
|
||||
|
||||
validateEndpoint := func(e Endpoint) Reader {
|
||||
return AllOf([]Reader{
|
||||
StringNotEmpty(e.Path),
|
||||
That(func(path string) bool {
|
||||
return len(path) > 0 && path[0] == '/'
|
||||
})(e.Path),
|
||||
That(func(method string) bool {
|
||||
return method == "GET" || method == "POST" ||
|
||||
method == "PUT" || method == "DELETE"
|
||||
})(e.Method),
|
||||
})
|
||||
}
|
||||
|
||||
traverse := TraverseRecord(validateEndpoint)
|
||||
result := traverse(endpoints)(t)
|
||||
|
||||
if !result {
|
||||
t.Error("Expected all endpoints to be valid")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_EmptyMap tests that SequenceRecord handles empty maps correctly
|
||||
func TestSequenceRecord_EmptyMap(t *testing.T) {
|
||||
result := SequenceRecord(map[string]Reader{})(t)
|
||||
if !result {
|
||||
t.Error("Expected SequenceRecord to pass with empty map")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_SingleTest tests SequenceRecord with a single test
|
||||
func TestSequenceRecord_SingleTest(t *testing.T) {
|
||||
tests := map[string]Reader{
|
||||
"test_one": Equal(42)(42),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected SequenceRecord to pass with single test")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_MultipleTests tests SequenceRecord with multiple passing tests
|
||||
func TestSequenceRecord_MultipleTests(t *testing.T) {
|
||||
tests := map[string]Reader{
|
||||
"test_addition": Equal(4)(2 + 2),
|
||||
"test_subtraction": Equal(1)(3 - 2),
|
||||
"test_multiplication": Equal(6)(2 * 3),
|
||||
"test_division": Equal(2)(6 / 3),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected SequenceRecord to pass with all passing tests")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_WithFailure tests that SequenceRecord fails when one test fails
|
||||
func TestSequenceRecord_WithFailure(t *testing.T) {
|
||||
t.Skip("Skipping test that intentionally creates failing subtests")
|
||||
tests := map[string]Reader{
|
||||
"test_pass": Equal(4)(2 + 2),
|
||||
"test_fail": Equal(5)(2 + 2), // This will fail
|
||||
"test_pass2": Equal(6)(2 * 3),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
|
||||
// The sequence should return false because one test fails
|
||||
if result {
|
||||
t.Error("Expected sequence to return false when one test fails")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_StringTests tests SequenceRecord with string assertions
|
||||
func TestSequenceRecord_StringTests(t *testing.T) {
|
||||
testString := "hello world"
|
||||
|
||||
tests := map[string]Reader{
|
||||
"not_empty": StringNotEmpty(testString),
|
||||
"correct_length": StringLength[any, any](11)(testString),
|
||||
"has_space": That(func(s string) bool {
|
||||
for _, ch := range s {
|
||||
if ch == ' ' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})(testString),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected all string tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_ArrayTests tests SequenceRecord with array assertions
|
||||
func TestSequenceRecord_ArrayTests(t *testing.T) {
|
||||
arr := []int{1, 2, 3, 4, 5}
|
||||
|
||||
tests := map[string]Reader{
|
||||
"not_empty": ArrayNotEmpty(arr),
|
||||
"correct_length": ArrayLength[int](5)(arr),
|
||||
"contains_three": ArrayContains(3)(arr),
|
||||
"all_positive": That(func(arr []int) bool {
|
||||
for _, n := range arr {
|
||||
if n <= 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})(arr),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected all array tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_ComplexAssertions tests SequenceRecord with complex combined assertions
|
||||
func TestSequenceRecord_ComplexAssertions(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
Email string
|
||||
}
|
||||
|
||||
user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
|
||||
|
||||
tests := map[string]Reader{
|
||||
"name_not_empty": StringNotEmpty(user.Name),
|
||||
"age_positive": That(func(age int) bool { return age > 0 })(user.Age),
|
||||
"age_reasonable": That(func(age int) bool { return age < 150 })(user.Age),
|
||||
"email_valid": That(func(email string) bool {
|
||||
hasAt := false
|
||||
hasDot := false
|
||||
for _, ch := range email {
|
||||
if ch == '@' {
|
||||
hasAt = true
|
||||
}
|
||||
if ch == '.' {
|
||||
hasDot = true
|
||||
}
|
||||
}
|
||||
return hasAt && hasDot
|
||||
})(user.Email),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected all user validation tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_MathOperations demonstrates basic math operations testing
|
||||
func TestSequenceRecord_MathOperations(t *testing.T) {
|
||||
tests := map[string]Reader{
|
||||
"addition": Equal(4)(2 + 2),
|
||||
"subtraction": Equal(1)(3 - 2),
|
||||
"multiplication": Equal(6)(2 * 3),
|
||||
"division": Equal(2)(6 / 3),
|
||||
"modulo": Equal(1)(7 % 3),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected all math operations to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_BooleanTests tests SequenceRecord with boolean assertions
|
||||
func TestSequenceRecord_BooleanTests(t *testing.T) {
|
||||
tests := map[string]Reader{
|
||||
"true_is_true": Equal(true)(true),
|
||||
"false_is_false": Equal(false)(false),
|
||||
"not_true": Equal(false)(!true),
|
||||
"not_false": Equal(true)(!false),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected all boolean tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_ErrorTests tests SequenceRecord with error assertions
|
||||
func TestSequenceRecord_ErrorTests(t *testing.T) {
|
||||
tests := map[string]Reader{
|
||||
"no_error": NoError(nil),
|
||||
"equal_value": Equal("test")("test"),
|
||||
"not_empty": StringNotEmpty("hello"),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected all error tests to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_vs_SequenceRecord demonstrates the relationship between the two functions
|
||||
func TestTraverseRecord_vs_SequenceRecord(t *testing.T) {
|
||||
type TestCase struct {
|
||||
Input int
|
||||
Expected int
|
||||
}
|
||||
|
||||
testData := map[string]TestCase{
|
||||
"test_1": {Input: 2, Expected: 4},
|
||||
"test_2": {Input: 3, Expected: 9},
|
||||
"test_3": {Input: 4, Expected: 16},
|
||||
}
|
||||
|
||||
// Using TraverseRecord
|
||||
traverseResult := TraverseRecord(func(tc TestCase) Reader {
|
||||
return Equal(tc.Expected)(tc.Input * tc.Input)
|
||||
})(testData)(t)
|
||||
|
||||
// Using SequenceRecord (manually creating the map)
|
||||
tests := make(map[string]Reader)
|
||||
for name, tc := range testData {
|
||||
tests[name] = Equal(tc.Expected)(tc.Input * tc.Input)
|
||||
}
|
||||
seqResult := SequenceRecord(tests)(t)
|
||||
|
||||
if traverseResult != seqResult {
|
||||
t.Error("Expected TraverseRecord and SequenceRecord to produce same result")
|
||||
}
|
||||
|
||||
if !traverseResult || !seqResult {
|
||||
t.Error("Expected both approaches to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_WithAllOf demonstrates combining SequenceRecord with AllOf
|
||||
func TestSequenceRecord_WithAllOf(t *testing.T) {
|
||||
arr := []int{1, 2, 3, 4, 5}
|
||||
|
||||
tests := map[string]Reader{
|
||||
"array_validations": AllOf([]Reader{
|
||||
ArrayNotEmpty(arr),
|
||||
ArrayLength[int](5)(arr),
|
||||
ArrayContains(3)(arr),
|
||||
}),
|
||||
"element_checks": AllOf([]Reader{
|
||||
That(func(a []int) bool { return a[0] == 1 })(arr),
|
||||
That(func(a []int) bool { return a[4] == 5 })(arr),
|
||||
}),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected combined assertions to pass")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTraverseRecord_ConfigValidation demonstrates real-world configuration validation
|
||||
func TestTraverseRecord_ConfigValidation(t *testing.T) {
|
||||
type Config struct {
|
||||
Value int
|
||||
Min int
|
||||
Max int
|
||||
}
|
||||
|
||||
configs := map[string]Config{
|
||||
"timeout": {Value: 30, Min: 1, Max: 60},
|
||||
"maxRetries": {Value: 3, Min: 1, Max: 10},
|
||||
"bufferSize": {Value: 1024, Min: 512, Max: 4096},
|
||||
}
|
||||
|
||||
validateConfig := func(c Config) Reader {
|
||||
return AllOf([]Reader{
|
||||
That(func(val int) bool { return val >= c.Min })(c.Value),
|
||||
That(func(val int) bool { return val <= c.Max })(c.Value),
|
||||
})
|
||||
}
|
||||
|
||||
traverse := TraverseRecord(validateConfig)
|
||||
result := traverse(configs)(t)
|
||||
|
||||
if !result {
|
||||
t.Error("Expected all configurations to be within valid ranges")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSequenceRecord_RealWorldExample demonstrates a realistic use case
|
||||
func TestSequenceRecord_RealWorldExample(t *testing.T) {
|
||||
type Response struct {
|
||||
StatusCode int
|
||||
Body string
|
||||
}
|
||||
|
||||
response := Response{StatusCode: 200, Body: `{"status":"ok"}`}
|
||||
|
||||
tests := map[string]Reader{
|
||||
"status_ok": Equal(200)(response.StatusCode),
|
||||
"body_not_empty": StringNotEmpty(response.Body),
|
||||
"body_is_json": That(func(s string) bool {
|
||||
return len(s) > 0 && s[0] == '{' && s[len(s)-1] == '}'
|
||||
})(response.Body),
|
||||
}
|
||||
|
||||
result := SequenceRecord(tests)(t)
|
||||
if !result {
|
||||
t.Error("Expected response validation to pass")
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,32 @@
|
||||
// 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
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/context/readerio"
|
||||
"github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/optics/optional"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
@@ -13,23 +34,506 @@ import (
|
||||
|
||||
type (
|
||||
// Result represents a computation that may fail with an error.
|
||||
//
|
||||
// This is an alias for [result.Result][T], which encapsulates either a successful
|
||||
// value of type T or an error. It's commonly used in test assertions to represent
|
||||
// operations that might fail, allowing for functional error handling without exceptions.
|
||||
//
|
||||
// A Result can be in one of two states:
|
||||
// - Success: Contains a value of type T
|
||||
// - Failure: Contains an error
|
||||
//
|
||||
// This type is particularly useful in testing scenarios where you need to:
|
||||
// - Test functions that return results
|
||||
// - Chain operations that might fail
|
||||
// - Handle errors functionally
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestResultHandling(t *testing.T) {
|
||||
// successResult := result.Of[int](42)
|
||||
// assert.Success(successResult)(t) // Passes
|
||||
//
|
||||
// failureResult := result.Error[int](errors.New("failed"))
|
||||
// assert.Failure(failureResult)(t) // Passes
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [Success]: Asserts a Result is successful
|
||||
// - [Failure]: Asserts a Result contains an error
|
||||
// - [result.Result]: The underlying Result type
|
||||
Result[T any] = result.Result[T]
|
||||
|
||||
// Reader represents a test assertion that depends on a testing.T context and returns a boolean.
|
||||
// Reader represents a test assertion that depends on a [testing.T] context and returns a boolean.
|
||||
//
|
||||
// This is the core type for all assertions in this package. It's an alias for
|
||||
// [reader.Reader][*testing.T, bool], which is a function that takes a testing context
|
||||
// and produces a boolean result indicating whether the assertion passed.
|
||||
//
|
||||
// The Reader pattern enables:
|
||||
// - Composable assertions that can be combined using functional operators
|
||||
// - Deferred execution - assertions are defined but not executed until applied to a test
|
||||
// - Reusable assertion logic that can be applied to multiple tests
|
||||
// - Functional composition of complex test conditions
|
||||
//
|
||||
// All assertion functions in this package return a Reader, which must be applied
|
||||
// to a *testing.T to execute the assertion:
|
||||
//
|
||||
// assertion := assert.Equal(42)(result) // Creates a Reader
|
||||
// assertion(t) // Executes the assertion
|
||||
//
|
||||
// Readers can be composed using functions like [AllOf], [ApplicativeMonoid], or
|
||||
// functional operators from the reader package.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestReaderComposition(t *testing.T) {
|
||||
// // Create individual assertions
|
||||
// assertion1 := assert.Equal(42)(42)
|
||||
// assertion2 := assert.StringNotEmpty("hello")
|
||||
//
|
||||
// // Combine them
|
||||
// combined := assert.AllOf([]assert.Reader{assertion1, assertion2})
|
||||
//
|
||||
// // Execute the combined assertion
|
||||
// combined(t)
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [Kleisli]: Function that produces a Reader from a value
|
||||
// - [AllOf]: Combines multiple Readers
|
||||
// - [ApplicativeMonoid]: Monoid for combining Readers
|
||||
Reader = reader.Reader[*testing.T, bool]
|
||||
|
||||
// Kleisli represents a function that produces a test assertion Reader from a value of type T.
|
||||
// Kleisli represents a function that produces a test assertion [Reader] from a value of type T.
|
||||
//
|
||||
// This is an alias for [reader.Reader][T, Reader], which is a function that takes a value
|
||||
// of type T and returns a Reader (test assertion). This pattern is fundamental to the
|
||||
// "data last" principle used throughout this package.
|
||||
//
|
||||
// Kleisli functions enable:
|
||||
// - Partial application of assertions - configure the expected value first, apply actual value later
|
||||
// - Reusable assertion builders that can be applied to different values
|
||||
// - Functional composition of assertion pipelines
|
||||
// - Point-free style programming with assertions
|
||||
//
|
||||
// Most assertion functions in this package return a Kleisli, which must be applied
|
||||
// to the actual value being tested, and then to a *testing.T:
|
||||
//
|
||||
// kleisli := assert.Equal(42) // Kleisli[int] - expects an int
|
||||
// reader := kleisli(result) // Reader - assertion ready to execute
|
||||
// reader(t) // Execute the assertion
|
||||
//
|
||||
// Or more concisely:
|
||||
//
|
||||
// assert.Equal(42)(result)(t)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestKleisliPattern(t *testing.T) {
|
||||
// // Create a reusable assertion for positive numbers
|
||||
// isPositive := assert.That(func(n int) bool { return n > 0 })
|
||||
//
|
||||
// // Apply it to different values
|
||||
// isPositive(42)(t) // Passes
|
||||
// isPositive(100)(t) // Passes
|
||||
// // isPositive(-5)(t) would fail
|
||||
//
|
||||
// // Can be used with Local for property testing
|
||||
// type User struct { Age int }
|
||||
// checkAge := assert.Local(func(u User) int { return u.Age })(isPositive)
|
||||
// checkAge(User{Age: 25})(t) // Passes
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [Reader]: The assertion type produced by Kleisli
|
||||
// - [Local]: Focuses a Kleisli on a property of a larger structure
|
||||
Kleisli[T any] = reader.Reader[T, Reader]
|
||||
|
||||
// Predicate represents a function that tests a value of type T and returns a boolean.
|
||||
//
|
||||
// This is an alias for [predicate.Predicate][T], which is a simple function that
|
||||
// takes a value and returns true or false based on some condition. Predicates are
|
||||
// used with the [That] function to create custom assertions.
|
||||
//
|
||||
// Predicates enable:
|
||||
// - Custom validation logic for any type
|
||||
// - Reusable test conditions
|
||||
// - Composition of complex validation rules
|
||||
// - Integration with functional programming patterns
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestPredicates(t *testing.T) {
|
||||
// // Simple predicate
|
||||
// isEven := func(n int) bool { return n%2 == 0 }
|
||||
// assert.That(isEven)(42)(t) // Passes
|
||||
//
|
||||
// // String predicate
|
||||
// hasPrefix := func(s string) bool { return strings.HasPrefix(s, "test") }
|
||||
// assert.That(hasPrefix)("test_file.go")(t) // Passes
|
||||
//
|
||||
// // Complex predicate
|
||||
// isValidEmail := func(s string) bool {
|
||||
// return strings.Contains(s, "@") && strings.Contains(s, ".")
|
||||
// }
|
||||
// assert.That(isValidEmail)("user@example.com")(t) // Passes
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [That]: Creates an assertion from a Predicate
|
||||
// - [predicate.Predicate]: The underlying predicate type
|
||||
Predicate[T any] = predicate.Predicate[T]
|
||||
|
||||
// Lens is a functional reference to a subpart of a data structure.
|
||||
//
|
||||
// This is an alias for [lens.Lens][S, T], which provides a composable way to focus
|
||||
// on a specific field within a larger structure. Lenses enable getting and setting
|
||||
// values in nested data structures in a functional, immutable way.
|
||||
//
|
||||
// In the context of testing, lenses are used with [LocalL] to focus assertions
|
||||
// on specific properties of complex objects without manually extracting those properties.
|
||||
//
|
||||
// A Lens[S, T] focuses on a value of type T within a structure of type S.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestLensUsage(t *testing.T) {
|
||||
// type Address struct { City string }
|
||||
// type User struct { Name string; Address Address }
|
||||
//
|
||||
// // Define lenses (typically generated)
|
||||
// addressLens := lens.Lens[User, Address]{...}
|
||||
// cityLens := lens.Lens[Address, string]{...}
|
||||
//
|
||||
// // Compose lenses to focus on nested field
|
||||
// userCityLens := lens.Compose(addressLens, cityLens)
|
||||
//
|
||||
// // Use with LocalL to assert on nested property
|
||||
// user := User{Name: "Alice", Address: Address{City: "NYC"}}
|
||||
// assert.LocalL(userCityLens)(assert.Equal("NYC"))(user)(t)
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [LocalL]: Uses a Lens to focus assertions on a property
|
||||
// - [lens.Lens]: The underlying lens type
|
||||
// - [Optional]: Similar but for values that may not exist
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
|
||||
// Optional is an optic that focuses on a value that may or may not be present.
|
||||
//
|
||||
// This is an alias for [optional.Optional][S, T], which is similar to a [Lens] but
|
||||
// handles cases where the focused value might not exist. Optionals are useful for
|
||||
// working with nullable fields, optional properties, or values that might be absent.
|
||||
//
|
||||
// In testing, Optionals are used with [FromOptional] to create assertions that
|
||||
// verify whether an optional value is present and, if so, whether it satisfies
|
||||
// certain conditions.
|
||||
//
|
||||
// An Optional[S, T] focuses on an optional value of type T within a structure of type S.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestOptionalUsage(t *testing.T) {
|
||||
// type Config struct { Timeout *int }
|
||||
//
|
||||
// // Define optional (typically generated)
|
||||
// timeoutOptional := optional.Optional[Config, int]{...}
|
||||
//
|
||||
// // Test when value is present
|
||||
// config1 := Config{Timeout: ptr(30)}
|
||||
// assert.FromOptional(timeoutOptional)(
|
||||
// assert.Equal(30),
|
||||
// )(config1)(t) // Passes
|
||||
//
|
||||
// // Test when value is absent
|
||||
// config2 := Config{Timeout: nil}
|
||||
// // FromOptional would fail because value is not present
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [FromOptional]: Creates assertions for optional values
|
||||
// - [optional.Optional]: The underlying optional type
|
||||
// - [Lens]: Similar but for values that always exist
|
||||
Optional[S, T any] = optional.Optional[S, T]
|
||||
|
||||
// Prism is an optic that focuses on a case of a sum type.
|
||||
//
|
||||
// This is an alias for [prism.Prism][S, T], which provides a way to focus on one
|
||||
// variant of a sum type (like Result, Option, Either, etc.). Prisms enable pattern
|
||||
// matching and extraction of values from sum types in a functional way.
|
||||
//
|
||||
// In testing, Prisms are used with [FromPrism] to create assertions that verify
|
||||
// whether a value matches a specific case and, if so, whether the contained value
|
||||
// satisfies certain conditions.
|
||||
//
|
||||
// A Prism[S, T] focuses on a value of type T that may be contained within a sum type S.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestPrismUsage(t *testing.T) {
|
||||
// // Prism for extracting success value from Result
|
||||
// successPrism := prism.Success[int]()
|
||||
//
|
||||
// // Test successful result
|
||||
// successResult := result.Of[int](42)
|
||||
// assert.FromPrism(successPrism)(
|
||||
// assert.Equal(42),
|
||||
// )(successResult)(t) // Passes
|
||||
//
|
||||
// // Prism for extracting error from Result
|
||||
// failurePrism := prism.Failure[int]()
|
||||
//
|
||||
// // Test failed result
|
||||
// failureResult := result.Error[int](errors.New("failed"))
|
||||
// assert.FromPrism(failurePrism)(
|
||||
// assert.Error,
|
||||
// )(failureResult)(t) // Passes
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [FromPrism]: Creates assertions for prism-focused values
|
||||
// - [prism.Prism]: The underlying prism type
|
||||
// - [Optional]: Similar but for optional values
|
||||
Prism[S, T any] = prism.Prism[S, T]
|
||||
|
||||
// ReaderIOResult represents a context-aware, IO-based computation that may fail.
|
||||
//
|
||||
// This is an alias for [readerioresult.ReaderIOResult][A], which combines three
|
||||
// computational effects:
|
||||
// - Reader: Depends on a context (like context.Context)
|
||||
// - IO: Performs side effects (like file I/O, network calls)
|
||||
// - Result: May fail with an error
|
||||
//
|
||||
// In testing, ReaderIOResult is used with [FromReaderIOResult] to convert
|
||||
// context-aware, effectful computations into test assertions. This is useful
|
||||
// when your test assertions need to:
|
||||
// - Access a context for cancellation or deadlines
|
||||
// - Perform IO operations (database queries, API calls, file access)
|
||||
// - Handle potential errors gracefully
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestReaderIOResult(t *testing.T) {
|
||||
// // Create a ReaderIOResult that performs IO and may fail
|
||||
// checkDatabase := func(ctx context.Context) func() result.Result[assert.Reader] {
|
||||
// return func() result.Result[assert.Reader] {
|
||||
// // Perform database check with context
|
||||
// if err := db.PingContext(ctx); err != nil {
|
||||
// return result.Error[assert.Reader](err)
|
||||
// }
|
||||
// return result.Of[assert.Reader](assert.NoError(nil))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Convert to Reader and execute
|
||||
// assertion := assert.FromReaderIOResult(checkDatabase)
|
||||
// assertion(t)
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [FromReaderIOResult]: Converts ReaderIOResult to Reader
|
||||
// - [ReaderIO]: Similar but without error handling
|
||||
// - [readerioresult.ReaderIOResult]: The underlying type
|
||||
ReaderIOResult[A any] = readerioresult.ReaderIOResult[A]
|
||||
|
||||
// ReaderIO represents a context-aware, IO-based computation.
|
||||
//
|
||||
// This is an alias for [readerio.ReaderIO][A], which combines two computational effects:
|
||||
// - Reader: Depends on a context (like context.Context)
|
||||
// - IO: Performs side effects (like logging, metrics)
|
||||
//
|
||||
// In testing, ReaderIO is used with [FromReaderIO] to convert context-aware,
|
||||
// effectful computations into test assertions. This is useful when your test
|
||||
// assertions need to:
|
||||
// - Access a context for cancellation or deadlines
|
||||
// - Perform IO operations that don't fail (or handle failures internally)
|
||||
// - Integrate with context-aware utilities
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestReaderIO(t *testing.T) {
|
||||
// // Create a ReaderIO that performs IO
|
||||
// logAndCheck := func(ctx context.Context) func() assert.Reader {
|
||||
// return func() assert.Reader {
|
||||
// // Log with context
|
||||
// logger.InfoContext(ctx, "Running test")
|
||||
// // Return assertion
|
||||
// return assert.Equal(42)(computeValue())
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Convert to Reader and execute
|
||||
// assertion := assert.FromReaderIO(logAndCheck)
|
||||
// assertion(t)
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [FromReaderIO]: Converts ReaderIO to Reader
|
||||
// - [ReaderIOResult]: Similar but with error handling
|
||||
// - [readerio.ReaderIO]: The underlying type
|
||||
ReaderIO[A any] = readerio.ReaderIO[A]
|
||||
|
||||
// Seq2 represents a Go iterator that yields key-value pairs.
|
||||
//
|
||||
// This is an alias for [iter.Seq2][K, A], which is Go's standard iterator type
|
||||
// introduced in Go 1.23. It represents a sequence of key-value pairs that can be
|
||||
// iterated over using a for-range loop.
|
||||
//
|
||||
// In testing, Seq2 is used with [SequenceSeq2] to execute a sequence of named
|
||||
// test cases provided as an iterator. This enables:
|
||||
// - Lazy evaluation of test cases
|
||||
// - Memory-efficient testing of large test suites
|
||||
// - Integration with Go's iterator patterns
|
||||
// - Dynamic generation of test cases
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestSeq2Usage(t *testing.T) {
|
||||
// // Create an iterator of test cases
|
||||
// testCases := func(yield func(string, assert.Reader) bool) {
|
||||
// if !yield("test_addition", assert.Equal(4)(2+2)) {
|
||||
// return
|
||||
// }
|
||||
// if !yield("test_multiplication", assert.Equal(6)(2*3)) {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Execute all test cases
|
||||
// assert.SequenceSeq2[assert.Reader](testCases)(t)
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [SequenceSeq2]: Executes a Seq2 of test cases
|
||||
// - [TraverseArray]: Similar but for arrays
|
||||
// - [iter.Seq2]: The underlying iterator type
|
||||
Seq2[K, A any] = iter.Seq2[K, A]
|
||||
|
||||
// Pair represents a tuple of two values with potentially different types.
|
||||
//
|
||||
// This is an alias for [pair.Pair][L, R], which holds two values: a "head" (or "left")
|
||||
// of type L and a "tail" (or "right") of type R. Pairs are useful for grouping
|
||||
// related values together without defining a custom struct.
|
||||
//
|
||||
// In testing, Pairs are used with [TraverseArray] to associate test names with
|
||||
// their corresponding assertions. Each element in the array is transformed into
|
||||
// a Pair[string, Reader] where the string is the test name and the Reader is
|
||||
// the assertion to execute.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestPairUsage(t *testing.T) {
|
||||
// type TestCase struct {
|
||||
// Input int
|
||||
// Expected int
|
||||
// }
|
||||
//
|
||||
// testCases := []TestCase{
|
||||
// {Input: 2, Expected: 4},
|
||||
// {Input: 3, Expected: 9},
|
||||
// }
|
||||
//
|
||||
// // Transform each test case into a named assertion
|
||||
// traverse := assert.TraverseArray(func(tc TestCase) assert.Pair[string, assert.Reader] {
|
||||
// name := fmt.Sprintf("square(%d)=%d", tc.Input, tc.Expected)
|
||||
// assertion := assert.Equal(tc.Expected)(tc.Input * tc.Input)
|
||||
// return pair.MakePair(name, assertion)
|
||||
// })
|
||||
//
|
||||
// traverse(testCases)(t)
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [TraverseArray]: Uses Pairs to create named test cases
|
||||
// - [pair.Pair]: The underlying pair type
|
||||
// - [pair.MakePair]: Creates a Pair
|
||||
// - [pair.Head]: Extracts the first value
|
||||
// - [pair.Tail]: Extracts the second value
|
||||
Pair[L, R any] = pair.Pair[L, R]
|
||||
|
||||
// Void represents the absence of a meaningful value, similar to unit type in functional programming.
|
||||
//
|
||||
// This is an alias for [function.Void], which is used to represent operations that don't
|
||||
// return a meaningful value but may perform side effects. In the context of testing, Void
|
||||
// is used with IO operations that perform actions without producing a result.
|
||||
//
|
||||
// Void is conceptually similar to:
|
||||
// - Unit type in functional languages (Haskell's (), Scala's Unit)
|
||||
// - void in languages like C/Java (but as a value, not just a type)
|
||||
// - Empty struct{} in Go (but with clearer semantic meaning)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestWithSideEffect(t *testing.T) {
|
||||
// // An IO operation that logs but returns Void
|
||||
// logOperation := func() function.Void {
|
||||
// log.Println("Test executed")
|
||||
// return function.Void{}
|
||||
// }
|
||||
//
|
||||
// // Execute the operation
|
||||
// logOperation()
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [IO]: Wraps side-effecting operations
|
||||
// - [function.Void]: The underlying void type
|
||||
Void = function.Void
|
||||
|
||||
// IO represents a side-effecting computation that produces a value of type A.
|
||||
//
|
||||
// This is an alias for [io.IO][A], which encapsulates operations that perform side effects
|
||||
// (like I/O operations, logging, or state mutations) and return a value. IO is a lazy
|
||||
// computation - it describes an effect but doesn't execute it until explicitly run.
|
||||
//
|
||||
// In testing, IO is used to:
|
||||
// - Defer execution of side effects until needed
|
||||
// - Compose multiple side-effecting operations
|
||||
// - Maintain referential transparency in test setup
|
||||
// - Separate effect description from effect execution
|
||||
//
|
||||
// An IO[A] is essentially a function `func() A` that:
|
||||
// - Encapsulates a side effect
|
||||
// - Returns a value of type A when executed
|
||||
// - Can be composed with other IO operations
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestIOOperation(t *testing.T) {
|
||||
// // Define an IO operation that reads a file
|
||||
// readConfig := func() io.IO[string] {
|
||||
// return func() string {
|
||||
// data, _ := os.ReadFile("config.txt")
|
||||
// return string(data)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // The IO is not executed yet - it's just a description
|
||||
// configIO := readConfig()
|
||||
//
|
||||
// // Execute the IO to get the result
|
||||
// config := configIO()
|
||||
// assert.StringNotEmpty(config)(t)
|
||||
// }
|
||||
//
|
||||
// Example with composition:
|
||||
//
|
||||
// func TestIOComposition(t *testing.T) {
|
||||
// // Chain multiple IO operations
|
||||
// pipeline := io.Map(
|
||||
// func(s string) int { return len(s) },
|
||||
// )(readFileIO)
|
||||
//
|
||||
// // Execute the composed operation
|
||||
// length := pipeline()
|
||||
// assert.That(func(n int) bool { return n > 0 })(length)(t)
|
||||
// }
|
||||
//
|
||||
// See also:
|
||||
// - [ReaderIO]: Combines Reader and IO effects
|
||||
// - [ReaderIOResult]: Adds error handling to ReaderIO
|
||||
// - [io.IO]: The underlying IO type
|
||||
// - [Void]: Represents operations without meaningful return values
|
||||
IO[A any] = io.IO[A]
|
||||
)
|
||||
|
||||
@@ -452,7 +452,7 @@ func TapEitherK[A, B any](f either.Kleisli[error, A, B]) Operator[A, A] {
|
||||
// Returns a function that chains Option-returning functions into ReaderIOResult.
|
||||
//
|
||||
//go:inline
|
||||
func ChainOptionK[A, B any](onNone func() error) func(option.Kleisli[A, B]) Operator[A, B] {
|
||||
func ChainOptionK[A, B any](onNone Lazy[error]) func(option.Kleisli[A, B]) Operator[A, B] {
|
||||
return RIOR.ChainOptionK[context.Context, A, B](onNone)
|
||||
}
|
||||
|
||||
@@ -800,7 +800,7 @@ func FromReaderResult[A any](ma ReaderResult[A]) ReaderIOResult[A] {
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FromReaderOption[A any](onNone func() error) Kleisli[ReaderOption[context.Context, A], A] {
|
||||
func FromReaderOption[A any](onNone Lazy[error]) Kleisli[ReaderOption[context.Context, A], A] {
|
||||
return RIOR.FromReaderOption[context.Context, A](onNone)
|
||||
}
|
||||
|
||||
@@ -895,17 +895,17 @@ func TapReaderIOK[A, B any](f readerio.Kleisli[A, B]) Operator[A, A] {
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainReaderOptionK[A, B any](onNone func() error) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, B] {
|
||||
func ChainReaderOptionK[A, B any](onNone Lazy[error]) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, B] {
|
||||
return RIOR.ChainReaderOptionK[context.Context, A, B](onNone)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainFirstReaderOptionK[A, B any](onNone func() error) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, A] {
|
||||
func ChainFirstReaderOptionK[A, B any](onNone Lazy[error]) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, A] {
|
||||
return RIOR.ChainFirstReaderOptionK[context.Context, A, B](onNone)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func TapReaderOptionK[A, B any](onNone func() error) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, A] {
|
||||
func TapReaderOptionK[A, B any](onNone Lazy[error]) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, A] {
|
||||
return RIOR.TapReaderOptionK[context.Context, A, B](onNone)
|
||||
}
|
||||
|
||||
|
||||
@@ -667,12 +667,12 @@ func FromPredicate[R, E, A any](pred func(A) bool, onFalse func(A) E) func(A) Re
|
||||
// This is useful for converting a ReaderIOEither into a ReaderIO by handling all cases.
|
||||
//
|
||||
//go:inline
|
||||
func Fold[R, E, A, B any](onLeft func(E) ReaderIO[R, B], onRight func(A) ReaderIO[R, B]) func(ReaderIOEither[R, E, A]) ReaderIO[R, B] {
|
||||
func Fold[R, E, A, B any](onLeft readerio.Kleisli[R, E, B], onRight func(A) ReaderIO[R, B]) func(ReaderIOEither[R, E, A]) ReaderIO[R, B] {
|
||||
return eithert.MatchE(readerio.MonadChain[R, either.Either[E, A], B], onLeft, onRight)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadFold[R, E, A, B any](ma ReaderIOEither[R, E, A], onLeft func(E) ReaderIO[R, B], onRight func(A) ReaderIO[R, B]) ReaderIO[R, B] {
|
||||
func MonadFold[R, E, A, B any](ma ReaderIOEither[R, E, A], onLeft readerio.Kleisli[R, E, B], onRight func(A) ReaderIO[R, B]) ReaderIO[R, B] {
|
||||
return eithert.FoldE(readerio.MonadChain[R, either.Either[E, A], B], ma, onLeft, onRight)
|
||||
}
|
||||
|
||||
@@ -680,7 +680,7 @@ func MonadFold[R, E, A, B any](ma ReaderIOEither[R, E, A], onLeft func(E) Reader
|
||||
// The default is computed lazily via a ReaderIO.
|
||||
//
|
||||
//go:inline
|
||||
func GetOrElse[R, E, A any](onLeft func(E) ReaderIO[R, A]) func(ReaderIOEither[R, E, A]) ReaderIO[R, A] {
|
||||
func GetOrElse[R, E, A any](onLeft readerio.Kleisli[R, E, A]) func(ReaderIOEither[R, E, A]) ReaderIO[R, A] {
|
||||
return eithert.GetOrElse(readerio.MonadChain[R, either.Either[E, A], A], readerio.Of[R, A], onLeft)
|
||||
}
|
||||
|
||||
|
||||
@@ -636,7 +636,7 @@ func FromPredicate[R, A any](pred func(A) bool, onFalse func(A) error) Kleisli[R
|
||||
// This is useful for converting a ReaderIOResult into a ReaderIO by handling all cases.
|
||||
//
|
||||
//go:inline
|
||||
func Fold[R, A, B any](onLeft func(error) ReaderIO[R, B], onRight func(A) ReaderIO[R, B]) func(ReaderIOResult[R, A]) ReaderIO[R, B] {
|
||||
func Fold[R, A, B any](onLeft readerio.Kleisli[R, error, B], onRight func(A) ReaderIO[R, B]) func(ReaderIOResult[R, A]) ReaderIO[R, B] {
|
||||
return RIOE.Fold(onLeft, onRight)
|
||||
}
|
||||
|
||||
@@ -644,7 +644,7 @@ func Fold[R, A, B any](onLeft func(error) ReaderIO[R, B], onRight func(A) Reader
|
||||
// The default is computed lazily via a ReaderIO.
|
||||
//
|
||||
//go:inline
|
||||
func GetOrElse[R, A any](onLeft func(error) ReaderIO[R, A]) func(ReaderIOResult[R, A]) ReaderIO[R, A] {
|
||||
func GetOrElse[R, A any](onLeft readerio.Kleisli[R, error, A]) func(ReaderIOResult[R, A]) ReaderIO[R, A] {
|
||||
return RIOE.GetOrElse(onLeft)
|
||||
}
|
||||
|
||||
@@ -659,7 +659,7 @@ func OrElse[R, A any](onLeft Kleisli[R, error, A]) Operator[R, A, A] {
|
||||
// The success value is preserved unchanged.
|
||||
//
|
||||
//go:inline
|
||||
func OrLeft[A, R, E any](onLeft func(error) ReaderIO[R, E]) func(ReaderIOResult[R, A]) RIOE.ReaderIOEither[R, E, A] {
|
||||
func OrLeft[A, R, E any](onLeft readerio.Kleisli[R, error, E]) func(ReaderIOResult[R, A]) RIOE.ReaderIOEither[R, E, A] {
|
||||
return RIOE.OrLeft[A](onLeft)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user