Practical examples for using the hydronica/trial testing framework.
- Principles
- Basic Template
- Testing with Input Structs
- Testing Functions with Multiple Arguments
- Error Handling
- Panic Testing
- Test() vs SubTest()
- Using Helpers
- Custom Comparers
- Timeout
- Parallel Execution
- Complete Example
- Keep it simple - Use primitives when possible for input and output
- Return errors - Test functions must return
(result, error) - Self-contained cases - Each case should not depend on others
- Limit scope - Define custom structs within the test function
func TestBasic(t *testing.T) {
fn := func(in string) (int, error) {
return len(in), nil
}
cases := trial.Cases[string, int]{
"empty string": {
Input: "",
Expected: 0,
},
"hello": {
Input: "hello",
Expected: 5,
},
}
trial.New(fn, cases).Test(t)
}When testing functions with multiple parameters, define an input struct:
func Add(i1, i2 int) int {
return i1 + i2
}
func TestAdd(t *testing.T) {
type input struct {
i1 int
i2 int
}
fn := func(in input) (int, error) {
return Add(in.i1, in.i2), nil
}
cases := trial.Cases[input, int]{
"add two positive numbers": {
Input: input{i1: 1, i2: 2},
Expected: 3,
},
"add negative numbers": {
Input: input{i1: -5, i2: -3},
Expected: -8,
},
}
trial.New(fn, cases).Test(t)
}Alternative: use trial.Args() with trial.Input for dynamic argument handling:
func TestAddWithArgs(t *testing.T) {
fn := func(in trial.Input) (int, error) {
return Add(in.Slice(0).Int(), in.Slice(1).Int()), nil
}
cases := trial.Cases[trial.Input, int]{
"add two numbers": {
Input: trial.Args(1, 2),
Expected: 3,
},
}
trial.New(fn, cases).Test(t)
}cases := trial.Cases[[]int, int]{
"divide by zero": {
Input: []int{1, 0},
ShouldErr: true,
},
}Uses strings.Contains to check the error message:
cases := trial.Cases[string, output]{
"invalid character": {
Input: `{"Value", "abc"}`,
ExpectedErr: errors.New("invalid character"),
},
}type ValidationError struct {
Field string
}
func (e ValidationError) Error() string {
return "validation failed: " + e.Field
}
cases := trial.Cases[string, string]{
"validation error": {
Input: "",
ExpectedErr: trial.ErrType(ValidationError{}),
},
}Use ShouldPanic: true to test functions that should panic. Each case is isolated, so a panic won't stop other cases.
func TestDivideWithPanic(t *testing.T) {
fn := func(in []int) (int, error) {
return in[0] / in[1], nil // panics if in[1] is 0
}
cases := trial.Cases[[]int, int]{
"normal division": {
Input: []int{10, 2},
Expected: 5,
},
"divide by zero panics": {
Input: []int{1, 0},
ShouldPanic: true,
},
}
trial.New(fn, cases).Test(t)
}Runs all cases in a single test function:
trial.New(fn, cases).Test(t)
// PASS: "case 1"
// PASS: "case 2"
// FAIL: "case 3"Runs each case as a separate Go subtest using t.Run():
trial.New(fn, cases).SubTest(t)
// === RUN TestFunction/case_1
// --- PASS: TestFunction/case_1
// === RUN TestFunction/case_2
// --- PASS: TestFunction/case_2Use SubTest() when you need:
- CI/CD systems that track individual test results
- Run specific cases:
go test -run "TestName/case_name" - Better test isolation and reporting
- Parallel execution with
.Parallel()
See helpers.md for complete documentation.
cases := trial.Cases[time.Time, string]{
"parse day": {
Input: trial.Day("2024-01-15"),
Expected: "January 15, 2024",
},
"parse hour": {
Input: trial.Hour("2024-01-15T14"),
Expected: "January 15, 2024",
},
"custom format": {
Input: trial.Time(time.RFC3339, "2024-01-15T14:30:00Z"),
Expected: "January 15, 2024",
},
}type Config struct {
Name *string
Count *int
Enabled *bool
}
cases := trial.Cases[Config, string]{
"with all fields": {
Input: Config{
Name: trial.Pointer("app"),
Count: trial.Pointer(42),
Enabled: trial.Pointer(true),
},
Expected: "app",
},
}See comparers.md for complete documentation.
type Example struct {
Field1 int
Field2 int
Timestamp time.Time
LastUpdate time.Time
}
trial.New(fn, cases).Comparer(
trial.EqualOpt(
trial.IgnoreAllUnexported,
trial.IgnoreFields("Timestamp", "LastUpdate"),
),
).SubTest(t)For substring/subset matching:
fn := func(in string) (string, error) {
return "Hello, " + in + "! Welcome.", nil
}
cases := trial.Cases[string, string]{
"greeting contains name": {
Input: "World",
Expected: "World", // actual contains "World"
},
}
trial.New(fn, cases).Comparer(trial.Contains).Test(t)trial.New(fn, cases).Comparer(
trial.EqualOpt(trial.ApproxTime(time.Second)),
).Test(t)Fail cases that take too long:
trial.New(fn, cases).Timeout(time.Second).Test(t)Run subtests concurrently for faster test execution. This is especially useful for I/O-bound tests (network calls, database operations, file I/O).
trial.New(fn, cases).Parallel().SubTest(t)Important: Test cases must be thread-safe when using Parallel(). Avoid:
- Shared mutable state between cases
- Tests that depend on execution order
- Resources that can't handle concurrent access
Example with I/O-bound tests:
func TestAPIEndpoints(t *testing.T) {
fn := func(endpoint string) (int, error) {
resp, err := http.Get(baseURL + endpoint)
if err != nil {
return 0, err
}
defer resp.Body.Close()
return resp.StatusCode, nil
}
cases := trial.Cases[string, int]{
"health check": {Input: "/health", Expected: 200},
"users list": {Input: "/users", Expected: 200},
"not found": {Input: "/invalid", Expected: 404},
}
// Run all HTTP calls concurrently
trial.New(fn, cases).Parallel().SubTest(t)
}Chaining with other options:
trial.New(fn, cases).
Parallel().
Timeout(5 * time.Second).
Comparer(trial.Contains).
SubTest(t)Note: Parallel() only works with SubTest(), not Test().
Copy-paste-ready test file:
package mypackage
import (
"errors"
"testing"
"github.com/hydronica/trial"
)
func Divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("cannot divide by zero")
}
return a / b, nil
}
func TestDivide(t *testing.T) {
type input struct {
a int
b int
}
fn := func(in input) (int, error) {
return Divide(in.a, in.b)
}
cases := trial.Cases[input, int]{
"10 / 2 = 5": {
Input: input{a: 10, b: 2},
Expected: 5,
},
"9 / 3 = 3": {
Input: input{a: 9, b: 3},
Expected: 3,
},
"divide by zero": {
Input: input{a: 1, b: 0},
ExpectedErr: errors.New("cannot divide by zero"),
},
"negative division": {
Input: input{a: -10, b: 2},
Expected: -5,
},
}
trial.New(fn, cases).SubTest(t)
}