testcase package - go.llib.dev/testcase - Go Packages (original) (raw)

Package testcase is an opinionated testing framework.

Repository + README:https://go.llib.dev/testcase

Guide:https://go.llib.dev/testcase/blob/master/docs/README.md

package main

import ( "math/rand" "testing" "time"

"go.llib.dev/testcase/assert"

)

func main() { waiter := assert.Waiter{ WaitDuration: time.Millisecond, Timeout: time.Second, } w := assert.Retry{Strategy: waiter}

var t *testing.T
// will attempt to wait until assertion block passes without a failing testCase result.
// The maximum time it is willing to wait is equal to the wait timeout duration.
// If the wait timeout reached, and there was no passing assertion run,
// the last failed assertion history is replied to the received testing.TB
//   In this case the failure would be replied to the *testing.T.
w.Assert(t, func(it testing.TB) {
    if rand.Intn(1) == 0 {
        it.Fatal(`boom`)
    }
})

}

package main

import ( "testing"

"go.llib.dev/testcase"
"go.llib.dev/testcase/assert"

)

func main() { var tb testing.TB s := testcase.NewSpec(tb)

s.Test(`flaky`, func(t *testcase.T) {
    // flaky test content here
}, testcase.Flaky(assert.RetryCount(42)))

}

package main

import ( "math/rand" "testing"

"go.llib.dev/testcase/assert"

)

func main() { r := assert.Retry{Strategy: assert.RetryCount(42)}

var t *testing.T
r.Assert(t, func(it testing.TB) {
    if rand.Intn(1) == 0 {
        it.Fatal(`boom`)
    }
})

}

package main

import ( "math/rand" "testing"

"go.llib.dev/testcase/assert"

)

func main() { // this approach ideal if you need to deal with asynchronous systems // where you know that if a workflow process ended already, // there is no point in retrying anymore the assertion.

while := func(isFailed func() bool) {
    for isFailed() {
        // just retry while assertion is failed
        // could be that assertion will be failed forever.
        // Make sure the assertion is not stuck in a infinite loop.
    }
}

r := assert.Retry{Strategy: assert.LoopFunc(while)}

var t *testing.T
r.Assert(t, func(it testing.TB) {
    if rand.Intn(1) == 0 {
        it.Fatal(`boom`)
    }
})

}

package main

import ( "math/rand" "testing" "time"

"go.llib.dev/testcase/assert"

)

func main() { r := assert.Retry{Strategy: assert.Waiter{ WaitDuration: time.Millisecond, Timeout: time.Second, }}

var t *testing.T
r.Assert(t, func(it testing.TB) {
    if rand.Intn(1) == 0 {
        it.Fatal(`boom`)
    }
})

}

package main

import ( "go.llib.dev/testcase/assert" )

func main() { _ = assert.Retry{Strategy: assert.RetryCount(42)} }

package main

import ( "time"

"go.llib.dev/testcase/assert"

)

func main() { w := assert.Waiter{WaitDuration: time.Millisecond}

w.Wait() // will wait 1 millisecond and attempt to schedule other go routines

}

package main

import ( "math/rand" "time"

"go.llib.dev/testcase/assert"

)

func main() { w := assert.Waiter{ WaitDuration: time.Millisecond, Timeout: time.Second, }

// will attempt to wait until condition returns false.
// The maximum time it is willing to wait is equal to the wait timeout duration.
w.While(func() bool {
    return rand.Intn(1) == 0
})

}

package main

import ( "testing" "time"

"go.llib.dev/testcase/clock"
"go.llib.dev/testcase/clock/timecop"

)

func main() { var tb testing.TB timecop.SetSpeed(tb, 5) // 5x time speed <-clock.After(time.Second) // but only wait 1/5 of the time }

package main

import ( "testing" "time"

"go.llib.dev/testcase/clock"
"go.llib.dev/testcase/clock/timecop"

)

func main() { var tb testing.TB clock.Sleep(time.Second) // normal 1 sec sleep timecop.SetSpeed(tb, 5) // 5x time speed clock.Sleep(time.Second) // but only sleeps 1/5 of the time }

package main

import ( "testing" "time"

"go.llib.dev/testcase/clock"
"go.llib.dev/testcase/clock/timecop"

"go.llib.dev/testcase/assert"

)

func main() { var tb testing.TB

type Entity struct {
    CreatedAt time.Time
}

MyFunc := func() Entity {
    return Entity{
        CreatedAt: clock.Now(),
    }
}

expected := Entity{
    CreatedAt: clock.Now(),
}

timecop.Travel(tb, expected.CreatedAt, timecop.Freeze)

assert.Equal(tb, expected, MyFunc())

}

package main

import ( "testing" "time"

"go.llib.dev/testcase/clock"
"go.llib.dev/testcase/clock/timecop"

)

func main() { var tb testing.TB

date := time.Date(2022, 01, 01, 12, 0, 0, 0, time.Local)
timecop.Travel(tb, date, timecop.Freeze) // freeze the time until it is read
time.Sleep(time.Second)
_ = clock.Now() // equals with date

}

package main

import ( "testing" "time"

"go.llib.dev/testcase/clock"
"go.llib.dev/testcase/clock/timecop"

)

func main() { var tb testing.TB

_ = clock.Now() // now
timecop.Travel(tb, time.Hour)
_ = clock.Now() // now + 1 hour

}

package main

import ( "context" "errors" "fmt"

"go.llib.dev/testcase/faultinject"

)

func main() { defer faultinject.Enable()()

ctx := context.Background()

// all fault field is optional.
// in case left as zero value,
// it will match every caller context,
// and returns on the first .Err() / .Value(faultinject.Fault{})
ctx = faultinject.Inject(ctx, faultinject.CallerFault{
    Package:  "targetpkg",
    Receiver: "*myreceiver",
    Function: "myfunction",
}, errors.New("boom"))

// from and after call stack reached: targetpkg.(*myreceiver).myfunction
if err := ctx.Err(); err != nil {
    fmt.Println(err) // in the position defined by the Fault, it will yield an error
}

}

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { testcase.Global.Before(func(t *testcase.T) { t.Log("each Spec configured with this") })

var tb testing.TB
s := testcase.NewSpec(tb)

s.Test("local spec", func(t *testcase.T) {
    // includes configuration from global config
})

}

View Source

const ( OrderingAsDefined testOrderingMod = defined OrderingAsRandom testOrderingMod = random )

Global configures all *Spec which is made afterward of this call. If you need a Spec#Before that runs in configured in every Spec, use this function. It can be called multiple times, and then configurations will stack.

func Append[V any](t *T, list Var[[]V], vs ...V)

Append will append a value[T] to a current value of Var[[]T]. Append only possible if the value type of Var is a slice type of T.

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var tb testing.TB s := testcase.NewSpec(tb)

list := testcase.Let(s, func(t *testcase.T) []int {
    return []int{}
})

s.Before(func(t *testcase.T) {
    t.Log(`some context where a value is expected in the testcase.Var[[]T] variable`)
    testcase.Append(t, list, 42)
})

s.Test(``, func(t *testcase.T) {
    t.Log(`list will include the appended value`)
    list.Get(t) // []int{42}
})

}

GetEnv will help to look up an environment variable which is mandatory for a given test.

GetEnv simplifies writing tests that depend on environment variables, making the process more convenient.

In some cases, you may need to conditionally skip tests based on the presence or absence of a specific environment variable.

In other scenarios, certain tests must always run, and their failure should indicate a development environment setup issue if the required environment variable is missing.

GetEnv helps streamline these situations, ensuring more reliable and controlled test execution.

If onNotFound func not provided, testing.TB#SkipNow will be opted as default.

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var tb testing.TB = &testing.T{} const EnvKey = "THE_ENV_KEY"

// get an environment variable, or skip the test
testcase.GetEnv(tb, EnvKey, tb.SkipNow)

// get an environment variable, or fail now the test
testcase.GetEnv(tb, EnvKey, tb.Fail)
testcase.GetEnv(tb, EnvKey, tb.FailNow)

}

func Let2[V, B any](spec Spec, blk func(T) (V, B)) (Var[V], Var[B])

Let2 is a tuple-style variable creation method, where an init block is shared between different variables.

func Let3[V, B, N any](spec Spec, blk func(T) (V, B, N)) (Var[V], Var[B], Var[N])

Let3 is a tuple-style variable creation method, where an init block is shared between different variables.

OnFail will execute a funcion block in case the test fails.

func Race(fn1, fn2 func(), more ...func())

Race is a test helper that allows you to create a race situation easily. Race will execute each provided anonymous lambda function in a different goroutine, and make sure they are scheduled at the same time.

This is useful when you work on a component that requires thread-safety. By using the Race helper, you can write an example use of your component, and run the testing suite with `go test -race`. The race detector then should be able to notice issues with your implementation.

package main

import ( "go.llib.dev/testcase" "go.llib.dev/testcase/internal/example/mydomain" )

func main() { v := mydomain.MyUseCase{}

// running `go test` with the `-race` flag should help you detect unsafe implementations.
// each block run at the same time in a race situation
testcase.Race(func() {
    v.ThreadSafeCall()
}, func() {
    v.ThreadSafeCall()
})

}

func RegisterImmutableTypeT any func()

RegisterImmutableType In some cases, certain types are actually immutable, but use a mutable type to represent that immutable value type. For example, time.Location is such case.

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { type T struct{}

testcase.RegisterImmutableType[T]()

s := testcase.NewSpec((testing.TB)(nil))
v := testcase.LetValue(s, T{})
_ = v

s.Test("", func(t *testcase.T) {})

}

func RunOpenSuite[OS OpenSuite, TBS anyTBOrSpec](tb TBS, contracts ...OS)

package main

import ( "go.llib.dev/testcase" )

type SampleContractType interface { testcase.Suite testcase.OpenSuite }

func SampleContracts() []SampleContractType { return []SampleContractType{} }

func main() { s := testcase.NewSpec(nil) testcase.RunOpenSuite(s, SampleContracts()...) }

func RunSuite[S Suite, TBS anyTBOrSpec](tb TBS, contracts ...S)

RunSuite is a helper function that makes execution one or many Suite easy. By using RunSuite, you don't have to distinguish between testing or benchmark execution mod. It supports *testing.T, *testing.B, *testcase.T, *testcase.Spec and CustomTB test runners.

package main

import ( "go.llib.dev/testcase" )

type SampleContractType interface { testcase.Suite testcase.OpenSuite }

func SampleContracts() []SampleContractType { return []SampleContractType{} }

func main() { s := testcase.NewSpec(nil) testcase.RunSuite(s, SampleContracts()...) }

func Sandbox

package main

import ( "fmt"

"go.llib.dev/testcase"
"go.llib.dev/testcase/internal/doubles"

)

func main() { stb := &doubles.TB{} outcome := testcase.Sandbox(func() { // some test helper function calls fatal, which cause runtime.Goexit after marking the test failed. stb.FailNow() })

fmt.Println("The sandbox run has finished without an issue", outcome.OK)
fmt.Println("runtime.Goexit was called:", outcome.Goexit)
fmt.Println("panic value:", outcome.PanicValue)

}

SetEnv will set the os environment variable for the current program to a given value, and prepares a cleanup function to restore the original state of the environment variable.

Spec using this helper should be flagged with Spec.HasSideEffect or Spec.Sequential.

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var tb testing.TB testcase.SetEnv(tb, MY_KEY, myvalue) // env will be restored after the test }

SkipUntil is equivalent to SkipNow if the test is executing prior to the given deadline time. SkipUntil is useful when you need to skip something temporarily, but you don't trust your memory enough to return to it on your own.

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var t *testing.T // make tests skip until the given day is reached, // then make the tests fail. // This helps to commit code which still work in progress. testcase.SkipUntil(t, 2020, 01, 01, 12) }

func TableTest[TBS anyTBOrSpec, TC func(s *Spec) | func(t *T) | any, Act func(t *T) | func(s Spec) | func(T, TC)]( tbs TBS, tcs map[string]TC, act Act, )

TableTest allows you to make table tests, without the need to use a boilerplate. It optionally allows to use a Spec instead of a testing.TB, and then the table tests will inherit the Spec context. It guards against mistakes such as using for+t.Run+t.Parallel without variable shadowing. TableTest allows a variety of use, please check examples for further information on that.

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var tb testing.TB s := testcase.NewSpec(tb)

var in = testcase.Let[int](s, nil)
testcase.TableTest(s, map[string]func(t *testcase.T){
    "when 42": func(t *testcase.T) {
        in.Set(t, 42)
    },
    "whe 24": func(t *testcase.T) {
        in.Set(t, 24)
    },
}, func(s *testcase.Spec) {
    // common test assertions which applies to all table test scenario
    // it can express "OR" relationship where every testing branch has the same assertions.

    s.Then("", func(t *testcase.T) {
        _ = in.Get(t)
    })

    s.Then("", func(t *testcase.T) {
        _ = in.Get(t)
    })
})

}

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { myFunc := func(in int) string { if in != 42 { return "Not the Answer" } return "The Answer" } var t *testing.T type TTCase struct { In int Expected string } testcase.TableTest(t, map[string]TTCase{ "when A": { In: 42, Expected: "The Answer", }, "when B": { In: 24, Expected: "Not the Answer", }, "when C": { In: 128, Expected: "Not the Answer", }, }, func(t *testcase.T, tc TTCase) { got := myFunc(tc.In) t.Must.Equal(tc.Expected, got) }) }

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var t *testing.T myFunc := func(in int) string { if in == 42 { return "The Answer" } return "Not the answer" } type Case struct { Input int Expected string } arrangements := map[string]Case{ "when the input is correct": { Input: 42, Expected: "The Answer", }, "when something else 1": { Input: 24, Expected: "Not the answer", }, "when someting else 2": { Input: 128, Expected: "Not the answer", }, } act := func(t *testcase.T, tc Case) { got := myFunc(tc.Input) t.Must.Equal(tc.Expected, got) } testcase.TableTest(t, arrangements, act) }

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var tb testing.TB s := testcase.NewSpec(tb)

var (
    in = testcase.Let[int](s, nil)
)
act := func(t *testcase.T) {
    // my act that use in
    _ = in.Get(t)
}

testcase.TableTest(s, map[string]func(s *testcase.Spec){
    "when 42": func(s *testcase.Spec) {
        in.LetValue(s, 42)
    },
    "whe 24": func(s *testcase.Spec) {
        in.LetValue(s, 42)
    },
}, act)

}

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var tb testing.TB s := testcase.NewSpec(tb)

var (
    in = testcase.Let[int](s, nil)
)
act := func(t *testcase.T) {
    // my act that use in
    _ = in.Get(t)
}

testcase.TableTest(s, map[string]func(t *testcase.T){
    "when 42": func(t *testcase.T) {
        in.Set(t, 42)
    },
    "whe 24": func(t *testcase.T) {
        in.Set(t, 24)
    },
}, act)

}

UnsetEnv will unset the os environment variable value for the current program, and prepares a cleanup function to restore the original state of the environment variable.

Spec using this helper should be flagged with Spec.HasSideEffect or Spec.Sequential.

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var tb testing.TB testcase.UnsetEnv(tb, MY_KEY) // env will be restored after the test }

FakeTB is a testing double fake implmentation of testing.TB

OpenSuite is a testcase independent testing suite interface standard.

type OpenSuiteAdapter struct{ OpenSuite }

func (c OpenSuiteAdapter) Spec(s *Spec)

Spec provides you a struct that makes building nested test spec easy with the core T#Context function.

spec structure is a simple wrapping around the testing.T#Context. It doesn't use any global singleton cache object or anything like that. It doesn't force you to use global vars.

It uses the same idiom as the core go testing pkg also provide you. You can use the same way as the core testing pkg

go run ./... -v -run "the/name/of/the/test/it/print/orderingOutput/in/case/of/failure"

It allows you to do spec preparation for each test in a way, that it will be safe for use with testing.T#Parallel.

package main

import ( "fmt" "strings" "testing"

"go.llib.dev/testcase"
"go.llib.dev/testcase/assert"
"go.llib.dev/testcase/internal/example/mydomain"
"go.llib.dev/testcase/random"

)

func main() { var tb testing.TB

// spec do not use any global magic
// it is just a simple abstraction around testing.T#Context
// Basically you can easily can run it as you would any other go testCase
//   -> `go run ./... -v -run "my/edge/case/nested/block/I/want/to/run/only"`
//
spec := testcase.NewSpec(tb)

// when you have no side effects in your testing suite,
// you can enable parallel execution.
// You can play parallel even from nested specs to apply parallel testing for that spec and below.
spec.Parallel()
// or
spec.NoSideEffect()

// testcase.variables are thread safe way of setting up complex contexts
// where some variable need to have different values for edge cases.
// and I usually work with in-memory implementation for certain shared specs,
// to make my testCase coverage run fast and still close to somewhat reality in terms of integration.
// and to me, it is a necessary thing to have "T#parallel" SpecOption safely available
var myType = func(t *testcase.T) *mydomain.MyUseCase {
    return &mydomain.MyUseCase{}
}

// Each describe has a testing subject as an "act" function
spec.Describe(`IsLower`, func(s *testcase.Spec) {
    var ( // inputs for the Act
        input = testcase.Var[string]{ID: `input`}
    )
    act := func(t *testcase.T) bool {
        return myType(t).IsLower(input.Get(t))
    }

    s.When(`input string has lower case characters`, func(s *testcase.Spec) {
        input.Let(s, func(t *testcase.T) string {
            return t.Random.StringNWithCharset(t.Random.Int(), strings.ToLower(random.CharsetAlpha()))
        })

        s.And(`the first character is capitalized`, func(s *testcase.Spec) {
            // you can add more nesting for more concrete specifications,
            // in each nested block, you work on a separate variable stack,
            // so even if you overwrite something here,
            // that has no effect outside of this scope
            s.Before(func(t *testcase.T) {
                upperCaseLetter := t.Random.StringNC(1, strings.ToUpper(random.CharsetAlpha()))
                input.Set(t, upperCaseLetter+input.Get(t))
            })

            s.Then(`it will report false`, func(t *testcase.T) {
                t.Must.True(act(t),
                    assert.Message(fmt.Sprintf(`it was expected that %q will be reported to be not lowercase`, input.Get(t))))
            })

        })

        s.Then(`it will return true`, func(t *testcase.T) {
            t.Must.True(act(t),
                assert.Message(fmt.Sprintf(`it was expected that the %q will re reported to be lowercase`, input.Get(t))))
        })
    })

    s.When(`input string has upcase case characters`, func(s *testcase.Spec) {
        input.Let(s, func(t *testcase.T) string {
            return t.Random.StringNWithCharset(t.Random.Int(), strings.ToUpper(random.CharsetAlpha()))
        })

        s.Then(`it will return false`, func(t *testcase.T) {
            t.Must.False(act(t))
        })
    })
})

}

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var tb testing.TB s := testcase.NewSpec(tb) s.Context("my example testing suite", exampleSuite().Spec) }

func exampleSuite() testcase.Suite { s := testcase.NewSpec(nil) s.Test("foo", func(t *testcase.T) {

})
return s

}

package main

import ( "testing"

"go.llib.dev/testcase"
"go.llib.dev/testcase/internal/example/mydomain"
"go.llib.dev/testcase/internal/example/spechelper"

)

func main() { var t *testing.T s := testcase.NewSpec(t)

myType := func() *mydomain.MyUseCase { return &mydomain.MyUseCase{} }

s.Describe(`#MyFunc`, func(s *testcase.Spec) {
    var (
        something = spechelper.GivenWeHaveSomething(s)
        // .. other givens
    )
    act := func(t *testcase.T) {
        myType().MyFuncThatNeedsSomething(something.Get(t))
    }

    s.Then(`test case described here`, func(t *testcase.T) {
        act(t)
    })
})

}

package main

import ( "testing"

"go.llib.dev/testcase"
"go.llib.dev/testcase/internal/example/mydomain"

)

func main() { var b *testing.B s := testcase.NewSpec(b)

myType := func(t *testcase.T) *mydomain.MyUseCase {
    return &mydomain.MyUseCase{}
}

s.When(`something`, func(s *testcase.Spec) {
    s.Before(func(t *testcase.T) {
        t.Log(`setup`)
    })

    s.Then(`this benchmark block will be executed by *testing.B.N times`, func(t *testcase.T) {
        myType(t).IsLower(`Hello, World!`)
    })
})

}

NewSpec create new Spec struct that is ready for usage.

func ToSpec[TBS anyTBOrSpec](tbs TBS) *Spec

func (spec *Spec) After(afterBlock func(t *T))

After give you the ability to run a block after each test case. This is ideal for running cleanups. The received *testing.T object is the same as the Then block *testing.T object This hook applied to this scope and anything that is nested from here. All setup block is stackable.

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var t *testing.T s := testcase.NewSpec(t)

s.After(func(t *testcase.T) {
    // this will run after the test cases.
    // this hook applied to this scope and anything that is nested from here.
    // hooks can be stacked with each call.
})

}

func (spec *Spec) AfterAll(blk func(tb testing.TB))

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { s := testcase.NewSpec(nil) s.AfterAll(func(tb testing.TB) { // do something after all the test finished running }) s.Test("this test will run before the AfterAll hook", func(t *testcase.T) {}) }

func (*Spec) And

func (spec *Spec) And(desc string, blk func(s *Spec), opts ...SpecOption)

And is an alias for testcase#Spec.Context And is used to represent additional requirement for reaching a certain testing runtime contexts.

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var t *testing.T s := testcase.NewSpec(t)

s.When(`some spec`, func(s *testcase.Spec) {
    // fulfil the spec

    s.And(`additional spec`, func(s *testcase.Spec) {

        s.Then(`assert`, func(t *testcase.T) {

        })
    })

    s.And(`additional spec opposite`, func(s *testcase.Spec) {

        s.Then(`assert`, func(t *testcase.T) {

        })
    })
})

}

func (spec Spec) Around(block func(T) func())

Around give you the ability to create "Before" setup for each test case, with the additional ability that the returned function will be deferred to run after the Then block is done. This is ideal for setting up mocks, and then return the assertion request calls in the return func. This hook applied to this scope and anything that is nested from here. All setup block is stackable.

Deprecated: use Spec.Before with T.Cleanup or Spec.Before with T.Defer instead

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var t *testing.T s := testcase.NewSpec(t)

s.Around(func(t *testcase.T) func() {
    // this will run before the test cases

    // this hook applied to this scope and anything that is nested from here.
    // hooks can be stacked with each call
    return func() {
        // The content of the returned func will be deferred to run after the test cases.
    }
})

}

func (spec *Spec) AsSuite(name ...string) SpecSuite

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { suite := exampleOpenSuite()

var t *testing.T
suite.Test(t)

var b *testing.B
suite.Benchmark(b)

}

func exampleOpenSuite() testcase.OpenSuite { s := testcase.NewSpec(nil) s.Test("foo", func(t *testcase.T) {

})
return s.AsSuite()

}

func (spec *Spec) Before(beforeBlock func(t *T))

Before give you the ability to run a block before each test case. This is ideal for doing clean ahead before each test case. The received *testing.T object is the same as the Test block *testing.T object This hook applied to this scope and anything that is nested from here. All setup block is stackable.

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var t *testing.T s := testcase.NewSpec(t)

s.Before(func(t *testcase.T) {
    // this will run before the test cases.
})

}

func (spec *Spec) BeforeAll(blk func(tb testing.TB))

BeforeAll give you the ability to create a hook that runs only once before the test cases.

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var t *testing.T s := testcase.NewSpec(t)

s.BeforeAll(func(tb testing.TB) {
    // this will run once before every test cases.
})

}

func (spec *Spec) Benchmark(desc string, test func(t *T), opts ...SpecOption)

Benchmark creates a becnhmark in the given Spec context.

Creating a Benchmark will signal the Spec that test and benchmark happens seperately, and a test should not double as a benchmark.

package main

import ( "go.llib.dev/testcase" )

func main() { s := testcase.NewSpec(nil)

s.Before(func(t *testcase.T) {
    // arrangement for everything, including the Benchmark
})

s.Benchmark("bench scenario", func(t *testcase.T) {
    // OK
})

}

func (spec *Spec) Context(desc string, blk func(s *Spec), opts ...SpecOption)

Context allow you to create a sub specification for a given spec. In the sub-specification it is expected to add more contextual information to the test in a form of hook of variable setting. With Context you can set your custom test description, without any forced prefix like describe/when/and.

It is basically piggybacking the testing#T.Context and create new subspec in that nested testing#T.Context scope. It is used to add more description spec for the given subject. It is highly advised to always use When + Before/Around together, in which you should setup exactly what you wrote in the When description input. You can Context as many When/And within each other, as you want to achieve the most concrete edge case you want to test.

To verify easily your state-machine, you can count the `if`s in your implementation, and check that each `if` has 2 `When` block to represent the two possible path.

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var t *testing.T s := testcase.NewSpec(t)

s.Context(`description of the testing spec`, func(s *testcase.Spec) {
    s.Before(func(t *testcase.T) {
        // prepare for the testing spec
    })

    s.Then(`assert expected outcome`, func(t *testcase.T) {

    })
})

}

func (spec *Spec) Describe(subjectTopic string, blk func(s *Spec), opts ...SpecOption)

Describe creates a new spec scope, where you usually describe a subject.

By convention it is highly advised to create a variable `subject` with function that share the return signature of the method you test on a structure, and take *testcase.variables as the only input value. If your method require input values, you should strictly set those values within a `When`/`And` scope. This ensures you have to think trough the possible state-machines paths that are based on the input values.

For functions where 2 value is returned, and the second one is an error, in order to avoid repetitive test cases in the `Then` I often define a `onSuccess` variable, with a function that takes `testcase#variables` as well and test error return value there with `testcase#variables.T()`.

package main

import ( "testing"

"go.llib.dev/testcase"
"go.llib.dev/testcase/internal/example/mydomain"

)

func main() { var t *testing.T s := testcase.NewSpec(t)

myType := testcase.Let(s, func(t *testcase.T) *mydomain.MyUseCase {
    return &mydomain.MyUseCase{}
})

// Describe description points orderingOutput the subject of the tests
s.Describe(`#IsLower`, func(s *testcase.Spec) {
    var (
        input   = testcase.Var[string]{ID: `input`}
        subject = func(t *testcase.T) bool {
            // subject should represent what will be tested in the describe block
            return myType.Get(t).IsLower(input.Get(t))
        }
    )

    s.Test(``, func(t *testcase.T) { subject(t) })
})

}

func (spec *Spec) Finish()

Finish executes all unfinished test and mark them finished. Finish can be used when it is important to run the test before the Spec's testing#TB.Cleanup would execute.

Such case can be when a resource leaked inside a testing scope and resource closed with a deferred function, but the spec is still not ran.

func (spec *Spec) H() testingHelper

func (spec *Spec) HasSideEffect()

HasSideEffect means that after this call things defined that has software side effect during runtime. This suggest on its own that execution should be sequential in order to avoid retry tests.

HasSideEffect and NoSideEffect can be used together to describe a given piece of specification properties. Using them at the same location makes little sense, it was intended to be used in spec helper package where setup function handles what resource should be used in the spec variables. This allows flexibility for the developers to use side effect free variant for local development that has quick feedback loop, and replace them with the production implementation during CI/CD pipeline which less time critical.

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var t *testing.T s := testcase.NewSpec(t) // this mark the testCase to contain side effects. // this forbids any parallel testCase execution to avoid retry tests. // // Under the hood this is a syntax sugar for Sequential s.HasSideEffect()

s.Test(`this will run in sequence`, func(t *testcase.T) {})

s.Context(`some spec`, func(s *testcase.Spec) {
    s.Test(`this run in sequence`, func(t *testcase.T) {})

    s.Test(`this run in sequence`, func(t *testcase.T) {})
})

}

func (spec *Spec) Let(varName VarID, blk VarInit[any]) Var[any]

Let is a method to provide backward compatibility with the existing testing suite. Due to how Go type parameters work, methods are not allowed to have type parameters, thus Let has moved to be a pkg-level function in the package.

Deprecated: use testcase.Let instead testcase#Spec.Let.

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var t *testing.T s := testcase.NewSpec(t)

myTestVar := testcase.Let(s, func(t *testcase.T) interface{} {
    return "value that needs complex construction or can be mutated"
})

s.Then(`test case`, func(t *testcase.T) {
    t.Log(myTestVar.Get(t)) // -> returns the value set in the current spec spec for MyTestVar
})

}

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var t *testing.T s := testcase.NewSpec(t)

myTestVar := testcase.Let(s, func(t *testcase.T) interface{} {
    return "value that will be eager loaded before the testCase/then block reached"
}).EagerLoading(s)
// EagerLoading will ensure that the value of this Spec Var will be evaluated during the preparation of the testCase.

s.Then(`test case`, func(t *testcase.T) {
    t.Log(myTestVar.Get(t))
})

}

package main

import ( "context" "database/sql" "testing"

"go.llib.dev/testcase"

)

type SupplierWithDBDependency struct { DB interface { QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) } }

func (s SupplierWithDBDependency) DoSomething(ctx context.Context) error { rows, err := s.DB.QueryContext(ctx, SELECT 1 = 1) if err != nil { return err } return rows.Close() }

func main() { var t *testing.T s := testcase.NewSpec(t)

var (
    tx = testcase.Let(s, func(t *testcase.T) *sql.Tx {
        // it is advised to use a persistent db connection between multiple specification runs,
        // because otherwise `go testCase -count $times` can receive random connection failures.
        tx, err := getDBConnection(t).Begin()
        if err != nil {
            t.Fatal(err.Error())
        }
        // testcase.T#Defer will execute the received function after the current testCase edge case
        // where the `tx` testCase variable were accessed.
        t.Defer(tx.Rollback)
        return tx
    })
    supplier = testcase.Let(s, func(t *testcase.T) SupplierWithDBDependency {
        return SupplierWithDBDependency{DB: tx.Get(t)}
    })
)

s.Describe(`#DoSomething`, func(s *testcase.Spec) {
    var (
        ctx = testcase.Let(s, func(t *testcase.T) context.Context {
            return context.Background()
        })
        subject = func(t *testcase.T) error {
            return supplier.Get(t).DoSomething(ctx.Get(t))
        }
    )

    s.When(`...`, func(s *testcase.Spec) {
        s.Before(func(t *testcase.T) {
            //...
        })

        s.Then(`...`, func(t *testcase.T) {
            t.Must.Nil(subject(t))
        })
    })
})

}

func getDBConnection(_ testing.TB) *sql.DB {

return nil

}

package main

import ( "testing"

"go.llib.dev/testcase"
"go.llib.dev/testcase/internal/doubles"

)

func main() { var t *testing.T s := testcase.NewSpec(t)

stubTB := testcase.Let(s, func(t *testcase.T) *doubles.TB {
    stub := &doubles.TB{}
    t.Defer(stub.Finish)
    return stub
})

s.When(`some scope where double should behave in a certain way`, func(s *testcase.Spec) {
    s.Before(func(t *testcase.T) {
        stubTB.Get(t).StubName = "my stubbed name"
    })

    s.Then(`double will be available in every test case and finishNow called afterwards`, func(t *testcase.T) {
        // ...
    })
})

}

package main

import ( "testing"

"go.llib.dev/testcase"
"go.llib.dev/testcase/internal/example/mydomain"

)

func main() { var t *testing.T s := testcase.NewSpec(t)

var myType = func(t *testcase.T) *mydomain.MyUseCase { return &mydomain.MyUseCase{} }

s.Describe(`#IsLower`, func(s *testcase.Spec) {
    var (
        input   = testcase.Var[string]{ID: `input`}
        subject = func(t *testcase.T) bool {
            return myType(t).IsLower(input.Get(t))
        }
    )

    s.When(`input characters are list lowercase`, func(s *testcase.Spec) {
        testcase.Let(s, func(t *testcase.T) interface{} {
            return "list lowercase"
        })
        // or
        input.Let(s, func(t *testcase.T) string {
            return "list lowercase"
        })

        s.Then(`it will report true`, func(t *testcase.T) {
            t.Must.True(subject(t))
        })
    })

    s.When(`input is a capitalized`, func(s *testcase.Spec) {
        testcase.Let(s, func(t *testcase.T) interface{} {
            return "Capitalized"
        })
        // or
        input.Let(s, func(t *testcase.T) string {
            return "Capitalized"
        })

        s.Then(`it will report false`, func(t *testcase.T) {
            t.Must.True(!subject(t))
        })
    })
})

}

func (spec *Spec) LetValue(varName VarID, value any) Var[any]

LetValue is a method to provide backward compatibility with the existing testing suite. Due to how Go type parameters work, methods are not allowed to have type parameters, thus LetValue has moved to be a pkg-level function in the package.

Deprecated: use testcase.LetValue instead testcase#Spec.LetValue.

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var t *testing.T s := testcase.NewSpec(t)

variable := testcase.LetValue(s, "value")

s.Then(`test case`, func(t *testcase.T) {
    t.Log(variable.Get(t)) // -> "value"
})

}

package main

import ( "testing"

"go.llib.dev/testcase"
"go.llib.dev/testcase/internal/example/mydomain"

)

func main() { var t *testing.T s := testcase.NewSpec(t)

var myType = func(t *testcase.T) *mydomain.MyUseCase { return &mydomain.MyUseCase{} }

s.Describe(`#IsLower`, func(s *testcase.Spec) {
    var (
        input   = testcase.Var[string]{ID: `input`}
        subject = func(t *testcase.T) bool {
            return myType(t).IsLower(input.Get(t))
        }
    )

    s.When(`input characters are list lowercase`, func(s *testcase.Spec) {
        testcase.LetValue(s, "list lowercase")
        // or
        input.LetValue(s, "list lowercase")

        s.Then(`it will report true`, func(t *testcase.T) {
            t.Must.True(subject(t))
        })
    })

    s.When(`input is a capitalized`, func(s *testcase.Spec) {
        testcase.LetValue(s, "Capitalized")
        // or
        input.LetValue(s, "Capitalized")

        s.Then(`it will report false`, func(t *testcase.T) {
            t.Must.True(!subject(t))
        })
    })
})

}

func (spec *Spec) NoSideEffect()

NoSideEffect gives a hint to the reader of the current test that during the test execution, no side effect outside from the test specification scope is expected to be observable. It is important to note that this flag primary meant to represent the side effect possibility to the outside of the current testing specification, and not about the test specification's subject.

It is safe to state that if the subject of the test specification has no side effect, then the test specification must have no side effect as well.

If the subject of the test specification do side effect on an input value, then the test specification must have no side effect, as long Let memorization is used.

If the subject of the test specification does mutation on global variables such as OS Variable states for the current process, then it is likely, that even if the changes by the mutation is restored as part of the test specification, the test specification has side effects that would affect other test specification results, and, as such, must be executed sequentially.

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var t *testing.T s := testcase.NewSpec(t) // this is an idiom to express that the subject in the tests here are not expected to have any side-effect. // this means they are safe to be executed in parallel. s.NoSideEffect()

s.Test(`this will run in parallel`, func(t *testcase.T) {})

s.Context(`some spec`, func(s *testcase.Spec) {
    s.Test(`this run in parallel`, func(t *testcase.T) {})

    s.Test(`this run in parallel`, func(t *testcase.T) {})
})

}

func (spec *Spec) Parallel()

Parallel allows you to set list test case for the spec where this is being called, and below to nested contexts, to be executed in parallel (concurrently). Keep in mind that you can call Parallel even from nested specs to apply Parallel testing for that spec and below. This is useful when your test suite has no side effects at list. Using values from *vars when Parallel is safe. It is a shortcut for executing *testing.T#Parallel() for each test

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var t *testing.T s := testcase.NewSpec(t) s.Parallel() // tells the specs to run list test case in parallel

s.Test(`this will run in parallel`, func(t *testcase.T) {})

s.Context(`some spec`, func(s *testcase.Spec) {
    s.Test(`this run in parallel`, func(t *testcase.T) {})

    s.Test(`this run in parallel`, func(t *testcase.T) {})
})

}

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var t *testing.T s := testcase.NewSpec(t)

s.Context(`spec marked parallel`, func(s *testcase.Spec) {
    s.Parallel()

    s.Test(`this run in parallel`, func(t *testcase.T) {})
})

s.Context(`spec without parallel`, func(s *testcase.Spec) {

    s.Test(`this will run in sequence`, func(t *testcase.T) {})
})

}

func (spec *Spec) Sequential()

Sequential allows you to set list test case for the spec where this is being called, and below to nested contexts, to be executed sequentially. It will negate any testcase.Spec#Parallel call effect. This is useful when you want to create a spec helper package and there you want to manage if you want to use components side effects or not.

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var t *testing.T s := testcase.NewSpec(t) s.Sequential() // tells the specs to run list test case in sequence

s.Test(`this will run in sequence`, func(t *testcase.T) {})

s.Context(`some spec`, func(s *testcase.Spec) {
    s.Test(`this run in sequence`, func(t *testcase.T) {})

    s.Test(`this run in sequence`, func(t *testcase.T) {})
})

}

package main

import ( "testing"

"go.llib.dev/testcase"
"go.llib.dev/testcase/internal/example/spechelper"

)

func main() { var t *testing.T s := testcase.NewSpec(t)

// might or might not allow parallel execution
// It depends on the
storage := spechelper.Storage.Bind(s)

// Tells that the subject of this specification should be software side effect free on its own.
s.NoSideEffect()

s.Test("will only run parallel if no dependency has side effect", func(t *testcase.T) {
    t.Logf("%#v", storage.Get(t))
})

}

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var t *testing.T s := testcase.NewSpec(t)

s.Parallel() // on top level, spec marked as parallel

s.Context(`spec marked sequential`, func(s *testcase.Spec) {
    s.Sequential() // but in subcontext the testCase marked as sequential

    s.Test(`this run in sequence`, func(t *testcase.T) {})
})

s.Context(`spec that inherit parallel flag`, func(s *testcase.Spec) {

    s.Test(`this will run in parallel`, func(t *testcase.T) {})
})

}

func (spec *Spec) SkipBenchmark()

SkipBenchmark will flag the current Spec / Context to be skipped during Benchmark mode execution. If you wish to skip only a certain test, not the whole Spec / Context, use the SkipBenchmark SpecOption instead.

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var b *testing.B s := testcase.NewSpec(b) s.SkipBenchmark()

s.Test(`this will be skipped during benchmark`, func(t *testcase.T) {})

s.Context(`some spec`, func(s *testcase.Spec) {
    s.Test(`this as well`, func(t *testcase.T) {})
})

}

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var b *testing.B s := testcase.NewSpec(b)

s.When(`rainy path`, func(s *testcase.Spec) {
    s.SkipBenchmark()

    s.Test(`will be skipped during benchmark`, func(t *testcase.T) {})
})

s.Context(`happy path`, func(s *testcase.Spec) {
    s.Test(`this will run as benchmark`, func(t *testcase.T) {})
})

}

func (spec *Spec) Spec(oth *Spec)

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { sharedSuite := testcase.NewSpec(nil) sharedSuite.Test("1", func(t *testcase.T) {}) sharedSuite.Test("2", func(t *testcase.T) {}) sharedSuite.Test("3", func(t *testcase.T) {})

{
    var tb testing.TB // real one, not nil
    s := testcase.NewSpec(tb)
    s.Describe("something", sharedSuite.Spec)
}
{
    var t *testing.T // func Test(t *testing.T)
    sharedSuite.AsSuite("x").Test(t)
}
{
    var b *testing.B // func Benchmark(b *testing.B)
    sharedSuite.AsSuite("x").Benchmark(b)
}

}

TODO allows you to leave notes for a given specification's context, which will be visible when the test specification is executed.

func (spec *Spec) Tag(tags ...string)

Tag allow you to mark tests in the current and below specification scope with tags. This can be used to provide additional documentation about the nature of the testing scope. This later might be used as well to filter your test in your CI/CD pipeline to build separate testing stages like integration, e2e and so on.

To select or exclude tests with certain tags, you can provide a comma separated list to the following environment variables:

They can be combined as well.

example usage:

TESTCASE_TAG_INCLUDE='E2E' go test ./... TESTCASE_TAG_EXCLUDE='E2E' go test ./... TESTCASE_TAG_INCLUDE='E2E' TESTCASE_TAG_EXCLUDE='list,of,excluded,tags' go test ./...

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { // example usage: // TESTCASE_TAG_INCLUDE='E2E' go testCase ./... // TESTCASE_TAG_EXCLUDE='E2E' go testCase ./... // TESTCASE_TAG_INCLUDE='E2E' TESTCASE_TAG_EXCLUDE='list,of,excluded,tags' go testCase ./... // var t *testing.T s := testcase.NewSpec(t)

s.Context(`E2E`, func(s *testcase.Spec) {
    // by tagging the spec spec, we can filter tests orderingOutput later in our CI/CD pipeline.
    // A comma separated list can be set with TESTCASE_TAG_INCLUDE env variable to filter down to tests with certain tags.
    // And/Or a comma separated list can be provided with TESTCASE_TAG_EXCLUDE to exclude tests tagged with certain tags.
    s.Tag(`E2E`)

    s.Test(`some E2E testCase`, func(t *testcase.T) {
        // ...
    })
})

}

func (spec *Spec) Test(desc string, test func(t *T), opts ...SpecOption)

Test creates a test case block where you receive the fully configured `testcase#T` object. Hook contents that meant to run before the test edge cases will run before the function the Test receives, and hook contents that meant to run after the test edge cases will run after the function is done. After hooks are deferred after the received function block, so even in case of panic, it will still be executed.

It should not contain anything that modify the test subject input. It should focus only on asserting the result of the subject.

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var t *testing.T s := testcase.NewSpec(t)

s.Test(`my testCase description`, func(t *testcase.T) {
    // ...
})

}

func (spec *Spec) Then(desc string, test func(t *T), opts ...SpecOption)

Then is an alias for Test

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var t *testing.T s := testcase.NewSpec(t)

s.Then(`it is expected.... so this is the testCase description here`, func(t *testcase.T) {
    // ...
})

}

func (spec *Spec) When(desc string, blk func(s *Spec), opts ...SpecOption)

When is an alias for testcase#Spec.Context When is used usually to represent `if` based decision reasons about your testing subject.

package main

import ( "testing"

"go.llib.dev/testcase"
"go.llib.dev/testcase/internal/example/mydomain"

)

func main() { var t *testing.T s := testcase.NewSpec(t)

var (
    myType  = func(t *testcase.T) *mydomain.MyUseCase { return &mydomain.MyUseCase{} }
    input   = testcase.Var[string]{ID: `input`}
    subject = func(t *testcase.T) bool { return myType(t).IsLower(input.Get(t)) }
)

s.When(`input has only upcase letter`, func(s *testcase.Spec) {
    input.LetValue(s, "UPPER")

    s.Then(`it will be false`, func(t *testcase.T) {
        t.Must.True(!subject(t))
    })
})

s.When(`input has only lowercase letter`, func(s *testcase.Spec) {
    input.LetValue(s, "lower")

    s.Then(`it will be true`, func(t *testcase.T) {
        t.Must.True(subject(t))
    })
})

}

type SpecOption interface {

}

func Flaky(CountOrTimeout interface{}) SpecOption

Flaky will mark the spec/testCase as unstable. Flaky testCase execution is tolerant towards failing assertion and these tests will be rerun in case of a failure. A Wait Timeout for a successful flaky testCase must be provided.

The primary use-case is that when a team focus on shipping orderingOutput the value, and time is short till deadlines. These flaky tests prevent CI/CD pipelines often turned off in the heat of the moment to let pass the latest changes. The motivation behind is to gain time for the team to revisit these tests after the release and then learn from it. At the same time, they intend to fix it as well. These tests, however often forgotten, and while they are not the greatest assets of the CI pipeline, they often still serve essential value.

As a Least wrong solution, instead of skipping these tests, you can mark them as flaky, so in a later time, finding these flaky tests in the project should be easy. When you flag a testCase as flaky, you must provide a timeout value that will define a testing time window where the testCase can be rerun multiple times by the framework. If the testCase can't run successfully within this time-window, the testCase will fail. This failure potentially means that the underlying functionality is broken, and the committer should reevaluate the changes in the last commit.

While this functionality might help in tough times, it is advised to pair the usage with a scheduled monthly CI pipeline job. The Job should check the testing code base for the flaky flag.

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var tb testing.TB s := testcase.NewSpec(tb)

s.Test(`testCase with "random" fails`, func(t *testcase.T) {
    // This testCase might fail "randomly" but the retry flag will allow some tolerance
    // This should be used to find time in team's calendar
    // and then allocate time outside of death-march times to learn to avoid retry tests in the future.
}, testcase.Flaky(42))

}

package main

import ( "testing" "time"

"go.llib.dev/testcase"

)

func main() { var tb testing.TB s := testcase.NewSpec(tb)

s.Test(`testCase with "random" fails`, func(t *testcase.T) {
    // This testCase might fail "randomly" but the retry flag will allow some tolerance
    // This should be used to find time in team's calendar
    // and then allocate time outside of death-march times to learn to avoid retry tests in the future.
}, testcase.Flaky(time.Minute))

}

Group creates a testing group in the specification. During testCase execution, a group will be bundled together, and parallel tests will run concurrently within the the testing group.

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var tb testing.TB s := testcase.NewSpec(tb)

s.Context(`description`, func(s *testcase.Spec) {

    s.Test(``, func(t *testcase.T) {})

}, testcase.Group(`testing-group-group-that-can-be-even-targeted-with-testCase-run-cli-option`))

}

RetryStrategyForEventually

Deprecated: use testcase.WithRetryStrategy instead

func SkipBenchmark() SpecOption

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var tb testing.TB s := testcase.NewSpec(tb)

s.Test(`will run`, func(t *testcase.T) {
    // this will run during benchmark execution
})

s.Test(`will skip`, func(t *testcase.T) {
    // this will skip the benchmark execution
}, testcase.SkipBenchmark())

}

type SpecSuite struct { N string S *Spec }

func (suite SpecSuite) Spec(s *Spec)

StubTB

Deprecated: use FakeTB, the naming was off as it was not a stub but a fully fleshed out fake with contracts and all

package main

import ( "fmt" "testing"

"go.llib.dev/testcase/assert"
"go.llib.dev/testcase/internal/doubles"

)

func main() { stub := &doubles.TB{} stub.Log("hello", "world") fmt.Println(stub.Logs.String())

myTestHelper := func(tb testing.TB) {
    tb.FailNow()
}

var tb testing.TB
assert.Must(tb).Panic(func() {
    myTestHelper(stub)
})
assert.Must(tb).True(stub.IsFailed)

}

type Suite interface {

Spec(s *[Spec](#Spec))

}

Suite meant to represent a testing suite. A test Suite is a collection of test cases. In a test suite, the test cases are organized in a logical order. A Suite is a great tool to define interface testing suites (contracts).

T embeds both testcase vars, and testing#T functionality. This leave place open for extension and but define a stable foundation for the hooks and testCase edge case function signatures

Works as a drop in replacement for packages where they depend on one of the function of testing#T

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var tb testing.TB s := testcase.NewSpec(tb) s.Test(``, func(t *testcase.T) { // failed test will stop with FailNow t.Must.Equal(1, 1, "must be equal") }) }

package main

import ( "testing" "time"

"go.llib.dev/testcase"

)

func main() { var tb testing.TB s := testcase.NewSpec(tb) s.Test(``, func(t testcase.T) { _ = t.Random.Int() _ = t.Random.IntBetween(0, 42) _ = t.Random.IntN(42) _ = t.Random.Float32() _ = t.Random.Float64() _ = t.Random.String() _ = t.Random.StringN(42) _ = t.Random.StringNWithCharset(42, "abc") _ = t.Random.Bool() _ = t.Random.Time() _ = t.Random.TimeN(time.Now(), 0, 4, 2) _ = t.Random.TimeBetween(time.Now().Add(-1time.Hour), time.Now().Add(time.Hour)) _ = t.Random.Pick([]int{1, 2, 3}).(int) }) }

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var tb testing.TB s := testcase.NewSpec(tb) s.Test(``, func(t *testcase.T) { // failed test will proceed, but mart the test failed t.Should.Equal(1, 1, "should be equal") }) }

NewT returns a *testcase.T prepared for the given testing.TB

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var tb testing.TB // placeholder _ = testcase.NewT(tb) }

NewTWithSpec returns a *testcase.T prepared for the given testing.TB using the context of the passed *Spec.

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { s := testcase.NewSpec(nil) // some spec specific configuration s.Before(func(t *testcase.T) {})

var tb testing.TB // placeholder
tc := testcase.NewTWithSpec(tb, s)
_ = tc

}

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { variable := testcase.Var[int]{ID: "variable", Init: func(t *testcase.T) int { return t.Random.Int() }}

// flat test case with test runtime variable caching
var tb testing.TB
t := testcase.NewTWithSpec(tb, testcase.NewSpec(tb))
value1 := variable.Get(t)
value2 := variable.Get(t)
t.Logf(`test case variable caching works even in flattened tests: v1 == v2 -> %v`, value1 == value2)

}

func ToT[TBs anyTB](tb TBs) *T

func (t *T) Cleanup(fn func())

func (t *T) Defer(fn interface{}, args ...interface{})

Defer function defers the execution of a function until the current test case returns. Deferred functions are guaranteed to run, regardless of panics during the test case execution. Deferred function calls are pushed onto a testcase runtime stack. When an function passed to the Defer function, it will be executed as a deferred call in last-in-first-orderingOutput order.

It is advised to use this inside a testcase.Spec#Let memorization function when spec variable defined that has finalizer requirements. This allow the specification to ensure the object finalizer requirements to be met, without using an testcase.Spec#After where the memorized function would be executed always, regardless of its actual need.

In a practical example, this means that if you have common vars defined with testcase.Spec#Let memorization, which needs to be Closed for example, after the test case already run. Ensuring such objects Close call in an after block would cause an initialization of the memorized object list the time, even in tests where this is not needed.

e.g.:

package main

import ( "database/sql" "testing"

"go.llib.dev/testcase"

)

func main() { var t *testing.T s := testcase.NewSpec(t)

// db for example is something that needs to defer an action after the testCase run
db := testcase.Let(s, func(t *testcase.T) *sql.DB {
    db, err := sql.Open(`driverName`, `dataSourceName`)

    // asserting error here with the *testcase.T ensure that the testCase will don't have some spooky failure.
    t.Must.Nil(err)

    // db.Close() will be called after the current test case reach the teardown hooks
    t.Defer(db.Close)

    // check if connection is OK
    t.Must.Nil(db.Ping())

    // return the verified db instance for the caller
    // this db instance will be memorized during the runtime of the test case
    return db
})

s.Test(`a simple test case`, func(t *testcase.T) {
    db := db.Get(t)
    t.Must.Nil(db.Ping()) // just to do something with it.
})

}

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var t *testing.T s := testcase.NewSpec(t)

something := testcase.Let(s, func(t *testcase.T) *ExampleDeferTeardownWithArgs {
    ptr := &ExampleDeferTeardownWithArgs{}
    // T#Defer arguments copied upon pass by value
    // and then passed to the function during the execution of the deferred function call.
    //
    // This is ideal for situations where you need to guarantee that a value cannot be muta
    t.Defer(ptr.SomeTeardownWithArg, `Hello, World!`)
    return ptr
})

s.Test(`a simple test case`, func(t *testcase.T) {
    entity := something.Get(t)

    entity.DoSomething()
})

}

type ExampleDeferTeardownWithArgs struct{}

func (*ExampleDeferTeardownWithArgs) SomeTeardownWithArg(_ string) {}

func (*ExampleDeferTeardownWithArgs) DoSomething() {}

func (t *T) Done() <-chan struct{}

Done function notifies the end of the test. If a test involves goroutines, listening to the done channel from the test can notify them about the test's end, preventing goroutine leaks.

package main

import ( "go.llib.dev/testcase" )

func main() { s := testcase.NewSpec(nil)

s.Test("", func(t *testcase.T) {
    go func() {
        select {
        // case do something for the test
        case <-t.Done():
            return // test is over, time to garbage collect
        }
    }()
})

}

func (t *T) Eventually(blk func(t *T))

Eventually helper allows you to write expectations to results that will only be eventually true. A common scenario where using Eventually will benefit you is testing concurrent operations. Due to the nature of async operations, one might need to wait and observe the system with multiple tries before the outcome can be seen. Eventually will attempt to assert multiple times with the assertion function block, until the expectations in the function body yield no testing failure. Calling multiple times the assertion function block content should be a safe and repeatable operation. For more, read the documentation of Eventually and Eventually.Assert. In case Spec doesn't have a configuration for how to retry Eventually, the DefaultEventually will be used.

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var tb testing.TB s := testcase.NewSpec(tb) s.Test(``, func(t *testcase.T) { // Eventually this will pass eventually t.Eventually(func(it *testcase.T) { it.Must.True(t.Random.Bool()) }) }) }

package main

import ( "context" "database/sql" "testing"

"go.llib.dev/testcase"

)

func main() { var t *testing.T var s = testcase.NewSpec(t)

type DB interface { // header interface in supplier pkg
    QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row
}
testcase.Let(s, func(t *testcase.T) DB {
    db, err := sql.Open(`driverName`, `dataSourceName`)
    t.Must.Nil(err)

    if t.HasTag(`black box`) {
        // tests with black box  use http testCase server or similar things and high level tx management not maintainable.
        t.Defer(db.Close)
        return db
    }

    tx, err := db.BeginTx(context.Background(), nil)
    t.Must.Nil(err)
    t.Defer(tx.Rollback)
    return tx
})

}

func (t *T) LogPretty(vs ...any)

LogPretty will Log out values in pretty print format (pp.Format).

package main

import ( "go.llib.dev/testcase" )

func main() { var t *testcase.T

type X struct {
    Foo string
}

t.LogPretty(X{Foo: "hello"})
// Logs:
// 	testcase_test.X{
// 		Foo: "hello",
// 	}

}

func (t *T) OnFail(fn func())

package main

import ( "go.llib.dev/testcase" )

func main() { s := testcase.NewSpec(nil)

s.Before(func(t *testcase.T) {
    t.OnFail(func() {
        // executes only when a test fails.
    })
})

s.Test("", func(t *testcase.T) {
    t.FailNow()
})

}

func (t *T) SetEnv(key, value string)

SetEnv will set the os environment variable for the current program to a given value, and prepares a cleanup function to restore the original state of the environment variable.

This cannot be used in parallel tests.

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var tb testing.TB s := testcase.NewSpec(tb)

s.Test("", func(t *testcase.T) {
    t.SetEnv("key", "value")
})

}

func (t *T) Setenv(key, value string)

Setenv calls os.Setenv(key, value) and uses Cleanup to restore the environment variable to its original value after the test.

This cannot be used in parallel tests.

SkipUntil is equivalent to SkipNow if the test is executing prior to the given deadline time. SkipUntil is useful when you need to skip something temporarily, but you don't trust your memory enough to return to it on your own.

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var t *testing.T s := testcase.NewSpec(t)

s.Test(`will be skipped`, func(t *testcase.T) {
    // make tests skip until the given day is reached,
    // then make the tests fail.
    // This helps to commit code which still work in progress.
    t.SkipUntil(2020, 01, 01, 12)
})

s.Test(`will not be skipped`, func(t *testcase.T) {})

}

func (t *T) UnsetEnv(key string)

UnsetEnv will unset the os environment variable value for the current program, and prepares a cleanup function to restore the original state of the environment variable.

This cannot be used in parallel tests.

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var tb testing.TB s := testcase.NewSpec(tb)

s.Test("", func(t *testcase.T) {
    t.UnsetEnv("key")
})

}

TBRunner defines the interface you need to implement if you want to create a custom TB that is compatible with Spec. To implement TBRunner correctly please use contracts.TB

import ( "go.llib.dev/testcase/contracts" "testing" )

func TestMyTestRunner(t *testing.T) { contracts.TB{Subject: func(tb testing.TB) testcase.TBRunner { return MyTestRunner{TB: tb} }}.Test(t) }

type Var[V any] struct {

ID [VarID](#VarID)


Init [VarInit](#VarInit)[V]


Before func(t *[T](#T), v [Var](#Var)[V])


OnLet func(s *[Spec](#Spec), v [Var](#Var)[V])


Deps [Vars](#Vars)

}

Var is a testCase helper structure, that allows easy way to access testCase runtime variables. In the future it will be updated to use Go2 type parameters.

Var allows creating testCase variables in a modular way. By modular, imagine that you can have commonly used values initialized and then access it from the testCase runtime spec. This approach allows an easy dependency injection maintenance at project level for your testing suite. It also allows you to have parallel testCase execution where you don't expect side effect from your subject.

e.g.: HTTP JSON API testCase and GraphQL testCase both use the business rule instances. Or multiple business rules use the same storage dependency.

The last use-case it allows is to define dependencies for your testCase subject before actually assigning values to it. Then you can focus on building up the testing spec and assign values to the variables at the right testing subcontext. With variables, it is easy to forget to assign a value to a variable or forgot to clean up the value of the previous run and then scratch the head during debugging. If you forgot to set a value to the variable in testcase, it warns you that this value is not yet defined to the current testing scope.

package main

import ( "testing"

"go.llib.dev/testcase"
"go.llib.dev/testcase/internal/example/memory"
"go.llib.dev/testcase/internal/example/mydomain"

)

func main() { var t *testing.T s := testcase.NewSpec(t)

var (
    storage = testcase.Let[mydomain.Storage](s, func(t *testcase.T) mydomain.Storage {
        return memory.NewStorage()
    })
    subject = testcase.Let(s, func(t *testcase.T) *mydomain.MyUseCase {
        return &mydomain.MyUseCase{Storage: storage.Get(t)}
    })
)

s.Describe(`#MyFunc`, func(s *testcase.Spec) {
    var subject = func(t *testcase.T) {
        // after GO2 this will be replaced with concrete Types instead of interface{}
        subject.Get(t).MyFunc()
    }

    s.Then(`do some testCase`, func(t *testcase.T) {
        subject(t) // act
        // assertions here.
    })

    // ...
    // other cases with resource xy state change
})

}

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var tb testing.TB s := testcase.NewSpec(tb) v := testcase.Var[int]{ ID: "myvar", Init: func(t *testcase.T) int { return 42 }, Before: func(t *testcase.T, v testcase.Var[int]) { t.Logf(I'm from the Var.Before block, and the value: %#v, v.Get(t)) }, } s.Test(``, func(t *testcase.T) { _ = v.Get(t) // log: I'm from the Var.Before block // -> 42 }) }

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var tb testing.TB s := testcase.NewSpec(tb)

value := testcase.Var[int]{
    ID: `value`,
    Init: func(t *testcase.T) int {
        return 42
    },
}

s.Test(`some testCase`, func(t *testcase.T) {
    _ = value.Get(t) // 42
})

}

package main

import ( "database/sql" "testing"

"go.llib.dev/testcase"

)

func main() { // package spechelper var db = testcase.Var[*sql.DB]{ ID: db, Init: func(t *testcase.T) *sql.DB { db, err := sql.Open(driver, dataSourceName) if err != nil { t.Fatal(err.Error()) } return db }, OnLet: func(s *testcase.Spec, _ testcase.Var[*sql.DB]) { s.Tag(database) s.Sequential() }, }

var tb testing.TB
s := testcase.NewSpec(tb)
db.Let(s, nil)
s.Test(`some testCase`, func(t *testcase.T) {
    _ = db.Get(t)
    t.HasTag(`database`) // true
})

}

func Let[V any](spec *Spec, blk VarInit[V]) Var[V]

Let define a memoized helper method. Let creates lazily-evaluated test execution bound variables. Let variables don't exist until called into existence by the actual tests, so you won't waste time loading them for examples that don't use them. They're also memoized, so they're useful for encapsulating database objects, due to the cost of making a database request. The value will be cached across list use within the same test execution but not across different test cases. You can eager load a value defined in let by referencing to it in a Before hook. Let is threadsafe, the parallel running test will receive they own test variable instance.

Defining a value in a spec Context will ensure that the scope and it's nested scopes of the current scope will have access to the value. It cannot leak its value outside from the current scope. Calling Let in a nested/sub scope will apply the new value for that value to that scope and below.

It will panic if it is used after a When/And/Then scope definition, because those scopes would have no clue about the later defined variable. In order to keep the specification reading mental model requirement low, it is intentionally not implemented to handle such case. Defining test vars always expected in the beginning of a specification scope, mainly for readability reasons.

vars strictly belong to a given `Describe`/`When`/`And` scope, and configured before any hook would be applied, therefore hooks always receive the most latest version from the `Let` vars, regardless in which scope the hook that use the variable is define.

Let can enhance readability when used sparingly in any given example group, but that can quickly degrade with heavy overuse.

func LetValue[V any](spec *Spec, value V) Var[V]

LetValue is a shorthand for defining immutable vars with Let under the hood. So the function blocks can be skipped, which makes tests more readable.

func (v Var[V]) Bind(s *Spec) Var[V]

Bind is a syntax sugar shorthand for Var.Let(*Spec, nil), where skipping providing a block meant to be explicitly expressed.

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var tb testing.TB s := testcase.NewSpec(tb) v := testcase.Var[int]{ID: "myvar", Init: func(t *testcase.T) int { return 42 }} v.Bind(s) s.Test(``, func(t *testcase.T) { _ = v.Get(t) // -> 42 }) }

func (v Var[V]) EagerLoading(s *Spec) Var[V]

EagerLoading allows the variable to be loaded before the action and assertion block is reached. This can be useful when you want to have a variable that cause side effect on your system. Like it should be present in some sort of attached resource/storage.

For example, you may persist the value in a storage as part of the initialization block, and then when the testCase/then block is reached, the entity is already present in the resource.

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var t *testing.T s := testcase.NewSpec(t)

value := testcase.Let(s, func(t *testcase.T) interface{} {
    return 42
})

// will be loaded early on, before the test case block reached.
// This can be useful when you want to have variables,
// that also must be present in some sort of attached resource,
// and as part of the constructor, you want to save it.
// So when the testCase block is reached, the entity is already present in the resource.
value.EagerLoading(s)

s.Test(`some testCase`, func(t *testcase.T) {
    _ = value.Get(t) // -> 42
    // value returned from cache instead of triggering first time initialization.
})

}

func (v Var[V]) Get(t *T) V

Get returns the current cached value of the given Variable Get is a thread safe operation. When Go2 released, it will replace type casting

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var t *testing.T s := testcase.NewSpec(t)

value := testcase.Let(s, func(t *testcase.T) interface{} {
    return 42
})

s.Test(`some testCase`, func(t *testcase.T) {
    _ = value.Get(t) // -> 42
})

}

func (v Var[V]) Let(s *Spec, blk VarInit[V]) Var[V]

Let allow you to set the variable value to a given spec

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var t *testing.T s := testcase.NewSpec(t)

value := testcase.Var[int]{
    ID: `the variable group`,
    Init: func(t *testcase.T) int {
        return 42
    },
}

value.Let(s, nil)

s.Test(`some testCase`, func(t *testcase.T) {
    _ = value.Get(t) // -> 42
})

}

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var t *testing.T s := testcase.NewSpec(t)

value := testcase.Var[int]{ID: `value`}

value.Let(s, func(t *testcase.T) int {
    return 42
}).EagerLoading(s)

s.Test(`some testCase`, func(t *testcase.T) {
    _ = value.Get(t) // -> 42
    // value returned from cache instead of triggering first time initialization.
})

}

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var t *testing.T s := testcase.NewSpec(t)

value := testcase.Var[int]{ID: `the variable group`}

value.Let(s, func(t *testcase.T) int {
    return 42
})

s.Test(`some testCase`, func(t *testcase.T) {
    _ = value.Get(t) // -> 42
})

}

func (v Var[V]) LetValue(s *Spec, value V) Var[V]

LetValue set the value of the variable to a given block

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var t *testing.T s := testcase.NewSpec(t)

value := testcase.Var[int]{ID: `the variable group`}

value.LetValue(s, 42)

s.Test(`some testCase`, func(t *testcase.T) {
    _ = value.Get(t) // -> 42
})

}

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var t *testing.T s := testcase.NewSpec(t)

value := testcase.Var[int]{ID: `value`}
value.LetValue(s, 42).EagerLoading(s)

s.Test(`some testCase`, func(t *testcase.T) {
    _ = value.Get(t) // -> 42
    // value returned from cache instead of triggering first time initialization.
})

}

func (v Var[V]) PreviousValue(t *T) V

PreviousValue is a syntax sugar for Var.Super to access the previous declaration's value.

func (v Var[V]) Set(t *T, value V)

Set sets a value to a given variable during testCase runtime Set is a thread safe operation.

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var t *testing.T s := testcase.NewSpec(t)

value := testcase.Let(s, func(t *testcase.T) interface{} {
    return 42
})

s.Before(func(t *testcase.T) {
    value.Set(t, 24)
})

s.Test(`some testCase`, func(t *testcase.T) {
    _ = value.Get(t) // -> 24
})

}

func (v Var[V]) Super(t *T) V

Super will return the inherited Super value of your Var. This means that if you declared Var in an outer Spec.Context, or your Var has an Var.Init field, then Var.Super will return its content. This allows you to incrementally extend with values the inherited value until you reach your testing scope. This also allows you to wrap your Super value with a Spy or Stub wrapping layer, and pry the interactions with the object while using the original value as a base.

package main

import ( "testing"

"go.llib.dev/testcase"

)

func main() { var tb testing.TB s := testcase.NewSpec(tb)

v := testcase.Let[int](s, func(t *testcase.T) int {
    return 32
})

s.Context("some sub context", func(s *testcase.Spec) {
    v.Let(s, func(t *testcase.T) int {
        return v.Super(t) + 10 // where super == 32 from the parent context
    })

    s.Test("the result of the V", func(t *testcase.T) {
        t.Must.Equal(42, v.Get(t))
    })
})

}

type VarGetter[V any] interface{ Get(*T) V }

func Implements[Interface any, V any](v Var[V]) (VarGetter[Interface], bool)

package main

import ( "testing"

"go.llib.dev/testcase/internal/testent"
"go.llib.dev/testcase/let"

"go.llib.dev/testcase"

)

func main() { var tb testing.TB s := testcase.NewSpec(tb)

foo := let.Var(s, func(t *testcase.T) testent.Foo {
    return testent.Foo{ID: t.Random.HexN(42)}
})

if fooer, ok := testcase.Implements[testent.Fooer](foo); ok {
    s.Test("as Fooer, it will do xy", func(t *testcase.T) {
        fooer.Get(t).Foo()
    })
}

}

type VarInit[V any] func(*T) V

type VarInitFunc[V any] VarInit[T]

VarInitFunc is a backward compatibility type for VarInit.

Deprecated: use VarInit type instead.