├── go.sum ├── go.mod ├── random ├── internal │ └── fixture │ │ ├── assets │ │ ├── llm.txt │ │ ├── errors.txt │ │ ├── nosql.txt │ │ ├── contacts │ │ │ ├── malenames.txt │ │ │ ├── lastnames.txt │ │ │ └── femalenames.txt │ │ └── emaildomains.txt │ │ └── naughtystrings │ │ └── nosql.txt ├── sextype │ └── const.go ├── sextype.go ├── charset_test.go ├── crypto_test.go ├── deprecated.go ├── crypto.go ├── charset.go ├── Make.go └── Unique.go ├── .gitignore ├── Spec_internal_test.go ├── scripts ├── example-output ├── provision ├── setup ├── doctoc-update ├── test-cover ├── test-go ├── testing └── test-output ├── notes.md ├── tools.go ├── httpspec ├── backward.go ├── contenttype.go ├── debug.go ├── example_shared_test.go ├── example_Method_test.go ├── example_Query_test.go ├── example_Header_test.go ├── server.go ├── ServeHTTP.go ├── example_Context_test.go ├── HandlerMiddleware_test.go ├── RoundTripperMiddleware_test.go ├── example_test.go ├── contenttype_test.go ├── example_ContentTypeIsJSON_test.go ├── utils.go ├── README.md ├── body.go ├── doubles.go ├── example_usageWithDotImport_test.go ├── server_test.go └── let.go ├── faultinject ├── fihttp │ ├── propagation_test.go │ ├── RoundTripper.go │ ├── propagate.go │ ├── Handler.go │ ├── RoundTripper_test.go │ └── example_test.go ├── errs.go ├── helper.go ├── backward.go ├── spechelper_test.go ├── CallerFault.go ├── Enabled.go ├── Enabled_internal_test.go ├── fault.go └── Enabled_test.go ├── internal ├── vargetterfunc.go ├── teardown │ └── offset_helper_test.go ├── backward.go ├── vargetterfunc_test.go ├── testent │ └── testent.go ├── example │ ├── spechelper │ │ ├── given.go │ │ └── extres.go │ ├── memory │ │ └── Storage.go │ ├── mydomain │ │ └── MyUseCase.go │ └── someextres │ │ └── Storage.go ├── reflects │ ├── is.go │ ├── IsMutable.go │ ├── is_test.go │ ├── access.go │ └── IsMutable_test.go ├── spechelper │ └── ordering.go ├── name.go ├── toggles.go ├── verbose.go ├── warn.go ├── fixtures │ └── test_output_test.go ├── recover.go ├── recover_test.go ├── person.go ├── slicekit │ └── slicekit.go ├── proxy │ ├── proxy_test.go │ └── proxy.go ├── assertlite │ └── assertlite.go ├── environ │ ├── environ_test.go │ └── environ.go ├── wait │ ├── wait_test.go │ └── wait.go ├── env │ └── env.go ├── Log.go ├── fmterror │ ├── Message.go │ └── Message_test.go ├── suite.go └── caller │ └── caller_test.go ├── Sandbox.go ├── .envrc ├── assert ├── helper_test.go ├── message.go ├── Diff.go ├── Diff_test.go ├── spike_test.go ├── It.go ├── message_test.go ├── README.md ├── Waiter.go ├── equal.go ├── deprecated.go └── Eventually.go ├── README.go ├── let ├── deprecated.go └── README.md ├── docs ├── examples │ ├── ValidateName.go │ ├── MyStruct.go │ ├── role │ │ └── main.go │ ├── ValidateName_test.go │ ├── if_test.go │ ├── func_test.go │ ├── spechelper_sharedResource_test.go │ ├── header │ │ └── main.go │ ├── immutableAct_test.go │ └── MyStruct_test.go ├── testing-double │ ├── stub_method_test.go │ ├── stub_test.go │ ├── fake_test.go │ └── spec_helper_test.go ├── aaa.md ├── spechelper.md ├── why-to-test.md ├── interface.md ├── what-problem-testcase-solves.md └── nesting.md ├── clock ├── internal │ ├── lock.go │ ├── glob.go │ ├── notify.go │ └── chronos.go ├── timecop │ ├── opts.go │ └── timecop.go ├── examples_test.go ├── spike_test.go └── Clock.go ├── dsl ├── dsl.go └── dsl_test.go ├── pkg └── synctest │ ├── locker.go │ ├── syntest_test.go │ └── phaser.go ├── pp ├── debug.go ├── default.go ├── PP.go ├── unexported_test.go ├── examples_test.go ├── PP_test.go ├── default_test.go └── README.md ├── Global.go ├── .travis.yml ├── sandbox ├── trace.go ├── Run.go └── Run_test.go ├── DSL_test.go ├── Global_test.go ├── deprecated_test.go ├── todo-linter.md ├── tags.go ├── Sandbox_test.go ├── seed.go ├── Race.go ├── deprecated.go ├── spec_helper_test.go ├── Spec_bc_test.go ├── spike_test.go ├── seed_test.go ├── contracts └── CustomTB.go ├── Race_test.go ├── TableTest.go ├── ordering.go └── Suite.go /go.sum: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module go.llib.dev/testcase 2 | 3 | go 1.18 4 | -------------------------------------------------------------------------------- /random/internal/fixture/assets/llm.txt: -------------------------------------------------------------------------------- 1 | /think 2 | /no_think 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *_mocks_test.go 2 | *_gen.go 3 | bin/mockgen 4 | /.tools 5 | -------------------------------------------------------------------------------- /Spec_internal_test.go: -------------------------------------------------------------------------------- 1 | package testcase 2 | 3 | var _ visitable = &Spec{} 4 | -------------------------------------------------------------------------------- /scripts/example-output: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | go test -v -run TestOutput 3 | -------------------------------------------------------------------------------- /scripts/provision: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eE 3 | go generate tools.go 4 | go generate ./... 5 | -------------------------------------------------------------------------------- /notes.md: -------------------------------------------------------------------------------- 1 | - [ ] add assertion test for time.Time with the same value just different *loc (nil vs UTC) 2 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | //go:generate rm -rf .tools 5 | //go:generate mkdir -p .tools 6 | package testcase 7 | -------------------------------------------------------------------------------- /httpspec/backward.go: -------------------------------------------------------------------------------- 1 | package httpspec 2 | 3 | // Request 4 | // 5 | // Deprecated: use InboundRequest instead 6 | var Request = InboundRequest 7 | -------------------------------------------------------------------------------- /scripts/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e -u 3 | 4 | cd "${WDP}" 5 | go generate -x -modfile tools.mod tools.go 6 | go generate -x ./... 7 | -------------------------------------------------------------------------------- /faultinject/fihttp/propagation_test.go: -------------------------------------------------------------------------------- 1 | package fihttp 2 | 3 | import "testing" 4 | 5 | func Test_propagation(t *testing.T) { 6 | t.Skip("TODO") 7 | } 8 | -------------------------------------------------------------------------------- /internal/vargetterfunc.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | type VarGetterFunc[T any, V any] func(t *T) V 4 | 5 | func (fn VarGetterFunc[T, V]) Get(t *T) V { return fn(t) } 6 | -------------------------------------------------------------------------------- /faultinject/errs.go: -------------------------------------------------------------------------------- 1 | package faultinject 2 | 3 | type errT string 4 | 5 | func (err errT) Error() string { return string(err) } 6 | 7 | const DefaultErr errT = "fault injected" 8 | -------------------------------------------------------------------------------- /Sandbox.go: -------------------------------------------------------------------------------- 1 | package testcase 2 | 3 | import ( 4 | "go.llib.dev/testcase/sandbox" 5 | ) 6 | 7 | func Sandbox(fn func()) sandbox.RunOutcome { 8 | return sandbox.Run(fn) 9 | } 10 | -------------------------------------------------------------------------------- /scripts/doctoc-update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e -u 3 | 4 | ( 5 | type find 6 | type doctoc 7 | ) 1>/dev/null 8 | 9 | find . -type f -name '*.md' -exec doctoc --notitle --github {} \; 10 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | export WDP="${WDP:-"${PWD}"}" 2 | export PATH="${WDP}/scripts:${PATH}" 3 | export PATH="${WDP}/.tools:${PATH}" 4 | 5 | export GO111MODULE=on 6 | export CGO_ENABLED=1 7 | export TESTCASE_DEBUG=TRUE 8 | 9 | -------------------------------------------------------------------------------- /random/sextype/const.go: -------------------------------------------------------------------------------- 1 | package sextype 2 | 3 | import ( 4 | "go.llib.dev/testcase/internal" 5 | ) 6 | 7 | const ( 8 | Male = internal.SexTypeMale 9 | Female = internal.SexTypeFemale 10 | ) 11 | -------------------------------------------------------------------------------- /assert/helper_test.go: -------------------------------------------------------------------------------- 1 | package assert_test 2 | 3 | type Greeter interface{ Greet() } 4 | 5 | type Foo struct{} 6 | 7 | func (foo Foo) Greet() {} 8 | 9 | type Bar struct{} 10 | 11 | func (bar Bar) Greet() {} 12 | -------------------------------------------------------------------------------- /README.go: -------------------------------------------------------------------------------- 1 | // Package testcase is an opinionated testing framework. 2 | // 3 | // Repository + README: 4 | // https://go.llib.dev/testcase 5 | // 6 | // Guide: 7 | // https://go.llib.dev/testcase/blob/master/docs/README.md 8 | package testcase 9 | -------------------------------------------------------------------------------- /internal/teardown/offset_helper_test.go: -------------------------------------------------------------------------------- 1 | package teardown_test 2 | 3 | import ( 4 | "go.llib.dev/testcase/internal/teardown" 5 | ) 6 | 7 | func offsetHelper(td *teardown.Teardown, fn interface{}, args ...interface{}) { td.Defer(fn, args...) } 8 | -------------------------------------------------------------------------------- /internal/backward.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "go.llib.dev/testcase/internal/doubles" 5 | "go.llib.dev/testcase/internal/teardown" 6 | ) 7 | 8 | type RecorderTB = doubles.RecorderTB 9 | type Teardown = teardown.Teardown 10 | -------------------------------------------------------------------------------- /let/deprecated.go: -------------------------------------------------------------------------------- 1 | package let 2 | 3 | import "go.llib.dev/testcase" 4 | 5 | // ElementFrom 6 | // 7 | // Deprecated: use let.OneOf instead 8 | func ElementFrom[V any](s *testcase.Spec, vs ...V) testcase.Var[V] { 9 | return OneOf(s, vs...) 10 | } 11 | -------------------------------------------------------------------------------- /faultinject/helper.go: -------------------------------------------------------------------------------- 1 | package faultinject 2 | 3 | import ( 4 | "runtime" 5 | "time" 6 | ) 7 | 8 | func wait() { 9 | for i, ngr := 0, runtime.NumGoroutine(); i < ngr*42; i++ { 10 | runtime.Gosched() 11 | time.Sleep(time.Nanosecond) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /httpspec/contenttype.go: -------------------------------------------------------------------------------- 1 | package httpspec 2 | 3 | import ( 4 | "go.llib.dev/testcase" 5 | ) 6 | 7 | func ContentTypeIsJSON(s *testcase.Spec) { 8 | s.Before(func(t *testcase.T) { 9 | Header.Get(t).Set(`Content-Type`, `application/json`) 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /docs/examples/ValidateName.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "errors" 4 | 5 | var ErrTooLong = errors.New(`validate error: too long`) 6 | 7 | func ValidateName(name string) error { 8 | if len(name) > 128 { 9 | return ErrTooLong 10 | } 11 | 12 | return nil 13 | } 14 | -------------------------------------------------------------------------------- /faultinject/backward.go: -------------------------------------------------------------------------------- 1 | package faultinject 2 | 3 | import "context" 4 | 5 | // Finish is an alias for After 6 | // 7 | // Deprecated: use After instead 8 | func Finish(returnErr *error, ctx context.Context, faults ...any) { 9 | After(returnErr, ctx, faults...) 10 | } 11 | -------------------------------------------------------------------------------- /clock/internal/lock.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | var mutex sync.RWMutex 8 | 9 | func lock() func() { 10 | mutex.Lock() 11 | return mutex.Unlock 12 | } 13 | 14 | func rlock() func() { 15 | mutex.RLock() 16 | return mutex.RUnlock 17 | } 18 | -------------------------------------------------------------------------------- /internal/vargetterfunc_test.go: -------------------------------------------------------------------------------- 1 | package internal_test 2 | 3 | import ( 4 | "go.llib.dev/testcase" 5 | "go.llib.dev/testcase/internal" 6 | "go.llib.dev/testcase/internal/testent" 7 | ) 8 | 9 | var _ testcase.VarGetter[testent.Foo] = internal.VarGetterFunc[testcase.T, testent.Foo](nil) 10 | -------------------------------------------------------------------------------- /let/README.md: -------------------------------------------------------------------------------- 1 | # Package let 2 | 3 | Package `let` contains Common Testcase variable `#Let` declarations for testing purpose. 4 | 5 | ```go 6 | var ( 7 | ctx = let.Context(s) 8 | name = let.FirstName(s) 9 | ) 10 | act := func(t *testcase.T) error { 11 | return MyFunc(ctx.Get(t), name.Get(t)) 12 | } 13 | ``` -------------------------------------------------------------------------------- /random/sextype.go: -------------------------------------------------------------------------------- 1 | package random 2 | 3 | import ( 4 | "go.llib.dev/testcase/internal" 5 | "go.llib.dev/testcase/random/sextype" 6 | ) 7 | 8 | func randomSexType(random *Random) internal.SexType { 9 | if random.Bool() { 10 | return sextype.Male 11 | } else { 12 | return sextype.Female 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /internal/testent/testent.go: -------------------------------------------------------------------------------- 1 | package testent 2 | 3 | type Fooer interface{ Foo() } 4 | 5 | type Foo struct{ ID string } 6 | 7 | var _ Fooer = Foo{} 8 | 9 | func (Foo) Foo() {} 10 | 11 | type Bazer interface{ Baz() } 12 | 13 | type Baz struct{ ID string } 14 | 15 | var _ Bazer = Baz{} 16 | 17 | func (Baz) Baz() {} 18 | -------------------------------------------------------------------------------- /internal/example/spechelper/given.go: -------------------------------------------------------------------------------- 1 | package spechelper 2 | 3 | import "go.llib.dev/testcase" 4 | 5 | func GivenWeHaveSomething(s *testcase.Spec) testcase.Var[any] { 6 | return testcase.Let(s, func(t *testcase.T) interface{} { 7 | // use user manager to create random user with fixtures maybe 8 | return nil 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /internal/reflects/is.go: -------------------------------------------------------------------------------- 1 | package reflects 2 | 3 | import "reflect" 4 | 5 | func IsNil(v any) bool { 6 | if v == nil { 7 | return true 8 | } 9 | defer func() { _ = recover() }() 10 | return reflect.ValueOf(v).IsNil() 11 | } 12 | 13 | func IsStruct(v any) bool { 14 | return reflect.ValueOf(v).Kind() == reflect.Struct 15 | } 16 | -------------------------------------------------------------------------------- /dsl/dsl.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "go.llib.dev/testcase" 5 | ) 6 | 7 | func Let[V any](spec *testcase.Spec, blk func(*testcase.T) V) testcase.Var[V] { 8 | return testcase.Let[V](spec, blk) 9 | } 10 | 11 | func LetValue[V any](spec *testcase.Spec, value V) testcase.Var[V] { 12 | return testcase.LetValue[V](spec, value) 13 | } 14 | -------------------------------------------------------------------------------- /assert/message.go: -------------------------------------------------------------------------------- 1 | package assert 2 | 3 | import "fmt" 4 | 5 | type Message string 6 | 7 | func MessageF(format string, args ...any) Message { 8 | return Message(fmt.Sprintf(format, args...)) 9 | } 10 | 11 | func toMsg(msg []Message) []any { 12 | var out []any 13 | for _, m := range msg { 14 | out = append(out, m) 15 | } 16 | return out 17 | } 18 | -------------------------------------------------------------------------------- /httpspec/debug.go: -------------------------------------------------------------------------------- 1 | package httpspec 2 | 3 | import "go.llib.dev/testcase" 4 | 5 | var debug = testcase.Var[bool]{ 6 | ID: `httpspec:debug`, 7 | Init: func(t *testcase.T) bool { return false }, 8 | } 9 | 10 | func Debug(s *testcase.Spec) { 11 | debug.LetValue(s, true) 12 | } 13 | 14 | func isDebugEnabled(t *testcase.T) bool { 15 | return debug.Get(t) 16 | } 17 | -------------------------------------------------------------------------------- /internal/spechelper/ordering.go: -------------------------------------------------------------------------------- 1 | package spechelper 2 | 3 | import ( 4 | "testing" 5 | 6 | "go.llib.dev/testcase" 7 | "go.llib.dev/testcase/internal" 8 | "go.llib.dev/testcase/internal/environ" 9 | ) 10 | 11 | func OrderAsDefined(tb testing.TB) { 12 | internal.SetupCacheFlush(tb) 13 | testcase.SetEnv(tb, environ.KeyOrdering, string(testcase.OrderingAsDefined)) 14 | } 15 | -------------------------------------------------------------------------------- /assert/Diff.go: -------------------------------------------------------------------------------- 1 | package assert 2 | 3 | import "go.llib.dev/testcase/pp" 4 | 5 | type diffFn func(value, othValue any) string 6 | 7 | // DiffFunc is the function that will be used to print out two object if they are not equal. 8 | // You can use your preferred diff implementation if you are not happy with the pretty print diff format. 9 | var DiffFunc diffFn = pp.DiffFormat[any] 10 | -------------------------------------------------------------------------------- /scripts/test-cover: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | openCover() { 5 | local coverPath=${1:?"cover file path"} 6 | trap 'rm "'"${coverPath}"'"' EXIT 7 | go tool cover -html="${coverPath}" 8 | } 9 | export -f openCover 10 | 11 | test-go -cover -coverprofile=coverage.txt -covermode=atomic 12 | find . -type f -name 'coverage.txt' -exec bash -c 'openCover "$@"' bash {} \; 13 | -------------------------------------------------------------------------------- /assert/Diff_test.go: -------------------------------------------------------------------------------- 1 | package assert_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "go.llib.dev/testcase/assert" 7 | ) 8 | 9 | func TestDiffFunc(t *testing.T) { 10 | diff := assert.DiffFunc(1, 2) 11 | if diff == "" { 12 | t.Fatalf("diff function returned empty value") 13 | } 14 | if diff != assert.DiffFunc(1, 2) { 15 | t.Fatalf("diff function is not deterministic") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /random/charset_test.go: -------------------------------------------------------------------------------- 1 | package random_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "go.llib.dev/testcase/assert" 7 | "go.llib.dev/testcase/random" 8 | ) 9 | 10 | func TestCharset(t *testing.T) { 11 | assert.NotEmpty(t, random.Charset()) 12 | assert.NotEmpty(t, random.CharsetASCII()) 13 | assert.NotEmpty(t, random.CharsetAlpha()) 14 | assert.NotEmpty(t, random.CharsetDigit()) 15 | } 16 | -------------------------------------------------------------------------------- /internal/name.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "reflect" 7 | ) 8 | 9 | func SymbolicName(T interface{}) string { 10 | t := reflect.TypeOf(T) 11 | for t.Kind() == reflect.Ptr { 12 | t = t.Elem() 13 | } 14 | 15 | if t.PkgPath() == "" { 16 | return fmt.Sprintf("%s", t.Name()) 17 | } 18 | 19 | return fmt.Sprintf("%s.%s", filepath.Base(t.PkgPath()), t.Name()) 20 | } 21 | -------------------------------------------------------------------------------- /pkg/synctest/locker.go: -------------------------------------------------------------------------------- 1 | package synctest 2 | 3 | import "sync" 4 | 5 | type nopLocker struct{} 6 | 7 | func (*nopLocker) Lock() {} 8 | 9 | func (*nopLocker) Unlock() {} 10 | 11 | type multiLocker []sync.Locker 12 | 13 | func (ls multiLocker) Lock() { 14 | for _, l := range ls { 15 | l.Lock() 16 | } 17 | } 18 | 19 | func (ls multiLocker) Unlock() { 20 | for _, l := range ls { 21 | l.Unlock() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /scripts/test-go: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | ( 5 | type go 6 | ) 1>/dev/null 7 | 8 | main() ( 9 | set -e 10 | shopt -s nullglob globstar 11 | local gmpath path 12 | for gmpath in **/go.mod; do 13 | path=${gmpath%"go.mod"} 14 | cd "${path}" 15 | testCurrent "${@}" 16 | done 17 | ) 18 | 19 | testCurrent() { 20 | go test ./... -race -count 1 -bench '^BenchmarkTest' "${@}" 21 | } 22 | 23 | main "${@}" 24 | -------------------------------------------------------------------------------- /assert/spike_test.go: -------------------------------------------------------------------------------- 1 | //go:build spike 2 | 3 | package assert_test 4 | 5 | import ( 6 | "testing" 7 | 8 | "go.llib.dev/testcase/assert" 9 | ) 10 | 11 | func Test_spike(t *testing.T) { 12 | type T struct { 13 | ID string 14 | V1 string 15 | V2 int 16 | } 17 | 18 | var vs []T 19 | 20 | assert.OneOf(t, vs, func(t testing.TB, got T) { 21 | assert.Equal(t, got.V1, "The Answer") 22 | assert.Equal(t, got.V2, 42) 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /internal/toggles.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func SetupCacheFlush(tb testing.TB) { 8 | CacheFlush() 9 | tb.Cleanup(CacheFlush) 10 | } 11 | 12 | var cacheFlushFns []func() 13 | 14 | func RegisterCacheFlush(fn func()) struct{} { 15 | cacheFlushFns = append(cacheFlushFns, fn) 16 | return struct{}{} 17 | } 18 | 19 | func CacheFlush() { 20 | for _, fn := range cacheFlushFns { 21 | fn() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /scripts/testing: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eE 3 | 4 | ( 5 | type test-go 6 | type test-output 7 | ) 1>/dev/null 8 | 9 | main() { 10 | if testAll "${@}"; then 11 | echo 12 | echo PASS[ALL] 13 | return 0 14 | else 15 | echo 16 | echo FAIL[ALL] 17 | return 1 18 | fi 19 | } 20 | 21 | testAll() ( 22 | if ! test-go; then 23 | return 1 24 | fi 25 | if ! test-output; then 26 | return 1 27 | fi 28 | ) 29 | 30 | main "${@}" 31 | -------------------------------------------------------------------------------- /internal/verbose.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Verbose() bool { 8 | return verbose() 9 | } 10 | 11 | var verbose = testing.Verbose 12 | 13 | func StubVerbose[T bool | func() bool](tb testing.TB, v T) { 14 | tb.Cleanup(func() { verbose = testing.Verbose }) 15 | switch v := any(v).(type) { 16 | case bool: 17 | verbose = func() bool { return v } 18 | case func() bool: 19 | verbose = v 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /httpspec/example_shared_test.go: -------------------------------------------------------------------------------- 1 | package httpspec_test 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | ) 7 | 8 | var testingT *testing.T 9 | 10 | type MyHandler struct{} 11 | 12 | func (m MyHandler) ServeHTTP(http.ResponseWriter, *http.Request) {} 13 | 14 | type ListResponse struct { 15 | Resources []string 16 | } 17 | 18 | type ShowResponse struct { 19 | Resources []string 20 | } 21 | 22 | type CreateResponse struct { 23 | ID string 24 | } 25 | -------------------------------------------------------------------------------- /clock/timecop/opts.go: -------------------------------------------------------------------------------- 1 | package timecop 2 | 3 | import "go.llib.dev/testcase/clock/internal" 4 | 5 | type TravelOption interface { 6 | configure(option *internal.Option) 7 | } 8 | 9 | func toOption(tos []TravelOption) internal.Option { 10 | var o internal.Option 11 | for _, opt := range tos { 12 | opt.configure(&o) 13 | } 14 | return o 15 | } 16 | 17 | type fnTravelOption func(option *internal.Option) 18 | 19 | func (fn fnTravelOption) configure(o *internal.Option) { fn(o) } 20 | -------------------------------------------------------------------------------- /docs/testing-double/stub_method_test.go: -------------------------------------------------------------------------------- 1 | package testingdouble_test 2 | 3 | import "context" 4 | 5 | type StubMethodCreateXY struct { 6 | XYStorage 7 | 8 | CreateXYFunc func(ctx context.Context, ptr *XY) error 9 | } 10 | 11 | func (stub StubMethodCreateXY) CreateXY(ctx context.Context, ptr *XY) error { 12 | if stub.CreateXYFunc != nil { 13 | return stub.CreateXYFunc(ctx, ptr) 14 | } 15 | 16 | return stub.XYStorage.CreateXY(ctx, ptr) 17 | } 18 | 19 | var _ XYStorage = StubMethodCreateXY{} 20 | -------------------------------------------------------------------------------- /pp/debug.go: -------------------------------------------------------------------------------- 1 | package pp 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | ) 8 | 9 | var debug = false 10 | 11 | func init() { 12 | v, ok := os.LookupEnv("TESTCASE_DEBUG") 13 | if !ok { 14 | return 15 | } 16 | state, err := strconv.ParseBool(v) 17 | if err != nil { 18 | panic(err.Error()) 19 | } 20 | debug = state 21 | } 22 | 23 | func debugRecover() { 24 | r := recover() 25 | if r == nil { 26 | return 27 | } 28 | if !debug { 29 | return 30 | } 31 | fmt.Println(r) 32 | } 33 | -------------------------------------------------------------------------------- /internal/warn.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | "testing" 9 | ) 10 | 11 | var warnOutput io.Writer = os.Stderr 12 | 13 | func Warn(vs ...any) { 14 | out := append([]any{"[WARN]", "[TESTCASE]"}, vs...) 15 | fmt.Fprintln(warnOutput, out...) 16 | } 17 | 18 | func StubWarn(tb testing.TB) *bytes.Buffer { 19 | original := warnOutput 20 | tb.Cleanup(func() { warnOutput = original }) 21 | var buf bytes.Buffer 22 | warnOutput = &buf 23 | return &buf 24 | } 25 | -------------------------------------------------------------------------------- /internal/fixtures/test_output_test.go: -------------------------------------------------------------------------------- 1 | // Do not change this file, the test-output acceptance test depends on it. 2 | package fixtures_test 3 | 4 | import ( 5 | "testing" 6 | 7 | "go.llib.dev/testcase" 8 | ) 9 | 10 | func TestFixtureOutput(t *testing.T) { 11 | if !testing.Verbose() { 12 | t.Skip() 13 | } 14 | s := testcase.NewSpec(t) 15 | s.Test(``, func(t *testcase.T) { t.Log(`foo`) }) 16 | s.Test(``, func(t *testcase.T) { t.Log(`bar`) }) 17 | s.Test(``, func(t *testcase.T) { t.Log(`baz`) }) 18 | } 19 | -------------------------------------------------------------------------------- /internal/recover.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "go.llib.dev/testcase/sandbox" 5 | ) 6 | 7 | // RecoverGoexit helps overcome the testing.TB#FailNow's behaviour 8 | // where on failure the goroutine exits to finish earlier. 9 | func RecoverGoexit(fn func()) sandbox.RunOutcome { 10 | runOutcome := sandbox.Run(fn) 11 | if runOutcome.Goexit { // ignore goexit 12 | return runOutcome 13 | } 14 | if !runOutcome.OK { // propagate panic 15 | panic(runOutcome.PanicValue) 16 | } 17 | return runOutcome 18 | } 19 | -------------------------------------------------------------------------------- /internal/recover_test.go: -------------------------------------------------------------------------------- 1 | package internal_test 2 | 3 | import ( 4 | "runtime" 5 | "testing" 6 | 7 | "go.llib.dev/testcase/assert" 8 | "go.llib.dev/testcase/sandbox" 9 | ) 10 | 11 | func TestRecoverFromGoexit(t *testing.T) { 12 | var total int 13 | var hasRun bool 14 | var survived bool 15 | defer func() { assert.Must(t).True(survived) }() 16 | sandbox.Run(func() { 17 | total++ 18 | hasRun = true 19 | runtime.Goexit() 20 | }) 21 | survived = true 22 | assert.Must(t).Equal(1, total) 23 | assert.Must(t).True(hasRun) 24 | } 25 | -------------------------------------------------------------------------------- /dsl/dsl_test.go: -------------------------------------------------------------------------------- 1 | package dsl_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "go.llib.dev/testcase" 7 | . "go.llib.dev/testcase/dsl" 8 | ) 9 | 10 | func Test(t *testing.T) { 11 | testcase.NewSpec(t).Describe(`smoke testing of testcase DSL`, func(s *testcase.Spec) { 12 | num := Let[int](s, func(t *testcase.T) int { 13 | return t.Random.Int() + 1 14 | }) 15 | str := LetValue[string](s, "42") 16 | 17 | s.Test(``, func(t *testcase.T) { 18 | t.Should.Equal("42", str.Get(t)) 19 | t.Must.NotEqual(0, num.Get(t)) 20 | }) 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /pkg/synctest/syntest_test.go: -------------------------------------------------------------------------------- 1 | package synctest_test 2 | 3 | import "sync/atomic" 4 | 5 | type StubLocker struct { 6 | _LockingN, _UnlockingN int32 7 | } 8 | 9 | func (stub *StubLocker) LockingN() int32 { 10 | return atomic.LoadInt32(&stub._LockingN) 11 | } 12 | 13 | func (stub *StubLocker) UnlockingN() int32 { 14 | return atomic.LoadInt32(&stub._UnlockingN) 15 | } 16 | 17 | func (stub *StubLocker) Lock() { 18 | atomic.AddInt32(&stub._LockingN, 1) 19 | } 20 | 21 | func (stub *StubLocker) Unlock() { 22 | atomic.AddInt32(&stub._UnlockingN, 1) 23 | } 24 | -------------------------------------------------------------------------------- /httpspec/example_Method_test.go: -------------------------------------------------------------------------------- 1 | package httpspec_test 2 | 3 | import ( 4 | "net/http" 5 | 6 | "go.llib.dev/testcase" 7 | "go.llib.dev/testcase/httpspec" 8 | ) 9 | 10 | func ExampleMethod_letValue() { 11 | s := testcase.NewSpec(testingT) 12 | 13 | httpspec.Handler.Let(s, func(t *testcase.T) http.Handler { 14 | return MyHandler{} 15 | }) 16 | 17 | // set the HTTP Method to get for the *http.InboundRequest 18 | httpspec.Method.LetValue(s, http.MethodGet) 19 | 20 | s.Test(`GET /`, func(t *testcase.T) { 21 | httpspec.ServeHTTP(t) 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /docs/testing-double/stub_test.go: -------------------------------------------------------------------------------- 1 | package testingdouble_test 2 | 3 | import "context" 4 | 5 | type StubXYStorage struct { 6 | CreateXYFunc func(ctx context.Context, ptr *XY) error 7 | FindXYByIDFunc func(ctx context.Context, ptr *XY, id string) (found bool, err error) 8 | } 9 | 10 | func (stub StubXYStorage) CreateXY(ctx context.Context, ptr *XY) error { 11 | return stub.CreateXYFunc(ctx, ptr) 12 | } 13 | 14 | func (stub StubXYStorage) FindXYByID(ctx context.Context, ptr *XY, id string) (found bool, err error) { 15 | return stub.FindXYByIDFunc(ctx, ptr, id) 16 | } 17 | 18 | var _ XYStorage = StubXYStorage{} 19 | -------------------------------------------------------------------------------- /faultinject/fihttp/RoundTripper.go: -------------------------------------------------------------------------------- 1 | package fihttp 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | ) 7 | 8 | type RoundTripper struct { 9 | Next http.RoundTripper 10 | // ServiceName is the name of the service of which this http.Client meant to do requests. 11 | ServiceName string 12 | } 13 | 14 | func (rt RoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { 15 | if faults, ok := lookupFaults(r.Context()); ok { 16 | bs, err := json.Marshal(*faults) 17 | if err != nil { 18 | return nil, err 19 | } 20 | r.Header.Set(Header, string(bs)) 21 | } 22 | return rt.Next.RoundTrip(r) 23 | } 24 | -------------------------------------------------------------------------------- /httpspec/example_Query_test.go: -------------------------------------------------------------------------------- 1 | package httpspec_test 2 | 3 | import ( 4 | "net/http" 5 | 6 | "go.llib.dev/testcase" 7 | "go.llib.dev/testcase/httpspec" 8 | ) 9 | 10 | func ExampleQuery() { 11 | s := testcase.NewSpec(testingT) 12 | 13 | httpspec.Handler.Let(s, func(t *testcase.T) http.Handler { return MyHandler{} }) 14 | 15 | s.Before(func(t *testcase.T) { 16 | // this is ideal to represent query string inputs 17 | httpspec.Query.Get(t).Set(`foo`, `bar`) 18 | }) 19 | 20 | s.Test(`the *http.InboundRequest URL QueryGet will have foo=bar`, func(t *testcase.T) { 21 | httpspec.ServeHTTP(t) 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /httpspec/example_Header_test.go: -------------------------------------------------------------------------------- 1 | package httpspec_test 2 | 3 | import ( 4 | "net/http" 5 | 6 | "go.llib.dev/testcase" 7 | "go.llib.dev/testcase/httpspec" 8 | ) 9 | 10 | func ExampleHeader() { 11 | s := testcase.NewSpec(testingT) 12 | 13 | httpspec.Handler.Let(s, func(t *testcase.T) http.Handler { return MyHandler{} }) 14 | 15 | s.Before(func(t *testcase.T) { 16 | // this is ideal to represent query string inputs 17 | httpspec.Header.Get(t).Set(`Foo`, `bar`) 18 | }) 19 | 20 | s.Test(`the *http.InboundRequest URL QueryGet will have 'Foo: bar'`, func(t *testcase.T) { 21 | httpspec.ServeHTTP(t) 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /docs/examples/MyStruct.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type MyStruct struct{} 9 | 10 | func (ms MyStruct) Say() string { 11 | return `Hello, World!` 12 | } 13 | 14 | func (ms MyStruct) Foo() string { 15 | return `Foo` 16 | } 17 | 18 | func (ms MyStruct) Bar() string { 19 | return `Bar` 20 | } 21 | 22 | func (ms MyStruct) Baz() string { 23 | return `Baz` 24 | } 25 | 26 | func (ms MyStruct) Shrug(msg string) string { 27 | const shrugEmoji = `¯\_(ツ)_/¯` 28 | if !strings.HasSuffix(msg, shrugEmoji) { 29 | msg = fmt.Sprintf(`%s ¯\_(ツ)_/¯`, msg) 30 | } 31 | return msg 32 | } 33 | -------------------------------------------------------------------------------- /random/internal/fixture/assets/errors.txt: -------------------------------------------------------------------------------- 1 | to, err := human() 2 | Task failed successfully. 3 | Keyboard not found; press F1 to resume... 4 | Something happened. 5 | Mission not accomplished! 6 | An error occurred while handling the previous error. 7 | The program has encountered an error which should not have occurred. 8 | Error: Success! 9 | A software or hardware error occurs that prevents the application from working correctly. 10 | The wrong floppy disk is present, and it cannot continue. 11 | The object reference is not set to an instance of an object. 12 | I'm sorry, Dave. I'm afraid I can't do that. 13 | YOU SHALL NOT PASS! 14 | -------------------------------------------------------------------------------- /pp/default.go: -------------------------------------------------------------------------------- 1 | package pp 2 | 3 | import ( 4 | "io" 5 | "os" 6 | ) 7 | 8 | func init() { 9 | initDefaultWriter() 10 | } 11 | 12 | var defaultWriter io.Writer = os.Stderr 13 | 14 | func initDefaultWriter() { 15 | fpath, ok := os.LookupEnv("PP") 16 | if !ok { 17 | return 18 | } 19 | if fpath == "" { 20 | return 21 | } 22 | stat, err := os.Stat(fpath) 23 | if err != nil && !os.IsNotExist(err) { 24 | return 25 | } 26 | if stat != nil && stat.IsDir() { 27 | return 28 | } 29 | out, err := os.OpenFile(fpath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600) 30 | if err != nil { 31 | panic(err) 32 | } 33 | defaultWriter = out 34 | } 35 | -------------------------------------------------------------------------------- /docs/aaa.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | - [Arrange Act Assert](#arrange-act-assert) 5 | 6 | 7 | 8 | # Arrange Act Assert 9 | 10 | A pattern for arranging and formatting code in UnitTest methods: 11 | 12 | - Arrange all necessary preconditions and inputs to build a testing context. 13 | - Act with the subject of given testing scope, which can be acting on the object or method under test. 14 | - Assert that the expected results have occurred. 15 | -------------------------------------------------------------------------------- /faultinject/spechelper_test.go: -------------------------------------------------------------------------------- 1 | package faultinject_test 2 | 3 | import ( 4 | "go.llib.dev/testcase" 5 | "go.llib.dev/testcase/faultinject" 6 | ) 7 | 8 | var enabled = testcase.Var[bool]{ 9 | ID: "faultinject is enabled", 10 | Init: func(t *testcase.T) bool { 11 | return true 12 | }, 13 | OnLet: func(s *testcase.Spec, enabled testcase.Var[bool]) { 14 | s.Before(func(t *testcase.T) { 15 | if enabled.Get(t) { 16 | faultinject.EnableForTest(t) 17 | } 18 | }) 19 | }, 20 | } 21 | 22 | var exampleErr = testcase.Var[error]{ 23 | ID: "example error", 24 | Init: func(t *testcase.T) error { 25 | return t.Random.Error() 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /internal/example/memory/Storage.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // example factory 8 | func NewStorage() *Storage { 9 | //sql.Open(`driver`, connstr) ... 10 | return &Storage{} 11 | } 12 | 13 | type Storage struct { 14 | table map[string]any 15 | } 16 | 17 | func (p Storage) Close() error { 18 | panic("implement me") 19 | } 20 | 21 | func (p Storage) BeginTx(ctx context.Context) (context.Context, error) { 22 | panic("implement me") 23 | } 24 | 25 | func (p Storage) CommitTx(ctx context.Context) error { 26 | panic("implement me") 27 | } 28 | 29 | func (p Storage) RollbackTx(ctx context.Context) error { 30 | panic("implement me") 31 | } 32 | -------------------------------------------------------------------------------- /random/crypto_test.go: -------------------------------------------------------------------------------- 1 | package random_test 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | "time" 7 | 8 | "go.llib.dev/testcase/random" 9 | 10 | "go.llib.dev/testcase" 11 | ) 12 | 13 | var _ rand.Source64 = random.CryptoSeed{} 14 | 15 | func TestCryptoSeed(t *testing.T) { 16 | s := testcase.NewSpec(t) 17 | 18 | var seed = func(t *testcase.T) random.CryptoSeed { 19 | return random.CryptoSeed{} 20 | } 21 | 22 | s.Describe(`usage with Random`, func(s *testcase.Spec) { 23 | randomizer := testcase.Let(s, func(t *testcase.T) *random.Random { 24 | return random.New(seed(t)) 25 | }) 26 | 27 | SpecRandomMethods(s, randomizer) 28 | }, testcase.Flaky(5*time.Second)) 29 | } 30 | -------------------------------------------------------------------------------- /assert/It.go: -------------------------------------------------------------------------------- 1 | package assert 2 | 3 | import "testing" 4 | 5 | // MakeIt will make an It. 6 | // 7 | // Deprecated: use assert top level functions directly, like Must and Should 8 | func MakeIt(tb testing.TB) It { 9 | return It{ 10 | TB: tb, 11 | Must: Must(tb), 12 | Should: Should(tb), 13 | } 14 | } 15 | 16 | // It 17 | // 18 | // Deprecated: assert package functions instead. 19 | type It struct { 20 | testing.TB 21 | // Must Asserter will use FailNow on a failed assertion. 22 | // This will make test exit early on. 23 | Must Asserter 24 | // Should Asserter's will allow to continue the test scenario, 25 | // but mark test failed on a failed assertion. 26 | Should Asserter 27 | } 28 | -------------------------------------------------------------------------------- /faultinject/fihttp/propagate.go: -------------------------------------------------------------------------------- 1 | package fihttp 2 | 3 | import "context" 4 | 5 | const Header = `Fault-Inject` 6 | 7 | type Fault struct { 8 | ServiceName string `json:"service_name,omitempty"` 9 | Name string `json:"name"` 10 | } 11 | 12 | type propagateCtxKey struct{} 13 | 14 | func Propagate(ctx context.Context, fs ...Fault) context.Context { 15 | if cfs, ok := lookupFaults(ctx); ok { 16 | *cfs = append(*cfs, fs...) 17 | return ctx 18 | } 19 | return context.WithValue(ctx, propagateCtxKey{}, &fs) 20 | } 21 | 22 | func lookupFaults(ctx context.Context) (*[]Fault, bool) { 23 | value := ctx.Value(propagateCtxKey{}) 24 | faultsPtr, ok := value.(*[]Fault) 25 | return faultsPtr, ok 26 | } 27 | -------------------------------------------------------------------------------- /random/internal/fixture/assets/nosql.txt: -------------------------------------------------------------------------------- 1 | true, $where: '1 == 1' 2 | , $where: '1 == 1' 3 | $where: '1 == 1' 4 | ', $where: '1 == 1' 5 | 1, $where: '1 == 1' 6 | { $ne: 1 } 7 | ', $or: [ {}, { 'a':'a 8 | ' } ], $comment:'successful MongoDB injection' 9 | db.injection.insert({success:1}); 10 | db.injection.insert({success:1});return 1;db.stores.mapReduce(function() { { emit(1,1 11 | || 1==1 12 | ' && this.password.match(/.*/)//+%00 13 | ' && this.passwordzz.match(/.*/)//+%00 14 | '%20%26%26%20this.password.match(/.*/)//+%00 15 | '%20%26%26%20this.passwordzz.match(/.*/)//+%00 16 | {$gt: ''} 17 | [$ne]=1 18 | ';sleep(5000); 19 | ';sleep(5000);' 20 | ';sleep(5000);+' 21 | ';it=new%20Date();do{pt=new%20Date();}while(pt-it<5000); -------------------------------------------------------------------------------- /internal/example/mydomain/MyUseCase.go: -------------------------------------------------------------------------------- 1 | package mydomain 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | ) 7 | 8 | type MyUseCase struct { 9 | Storage // example dependency 10 | } 11 | 12 | type Storage interface { 13 | BeginTx(context.Context) (context.Context, error) 14 | CommitTx(context.Context) error 15 | RollbackTx(context.Context) error 16 | } 17 | 18 | func (i *MyUseCase) MyFunc() {} 19 | 20 | func (i *MyUseCase) MyFuncThatNeedsSomething(something any) {} 21 | 22 | func (i *MyUseCase) Foo(ctx context.Context) (string, error) { 23 | return `bar`, nil 24 | } 25 | 26 | func (i *MyUseCase) IsLower(s string) bool { 27 | return strings.ToLower(s) == s 28 | } 29 | 30 | func (i *MyUseCase) ThreadSafeCall() {} 31 | -------------------------------------------------------------------------------- /random/internal/fixture/naughtystrings/nosql.txt: -------------------------------------------------------------------------------- 1 | true, $where: '1 == 1' 2 | , $where: '1 == 1' 3 | $where: '1 == 1' 4 | ', $where: '1 == 1' 5 | 1, $where: '1 == 1' 6 | { $ne: 1 } 7 | ', $or: [ {}, { 'a':'a 8 | ' } ], $comment:'successful MongoDB injection' 9 | db.injection.insert({success:1}); 10 | db.injection.insert({success:1});return 1;db.stores.mapReduce(function() { { emit(1,1 11 | || 1==1 12 | ' && this.password.match(/.*/)//+%00 13 | ' && this.passwordzz.match(/.*/)//+%00 14 | '%20%26%26%20this.password.match(/.*/)//+%00 15 | '%20%26%26%20this.passwordzz.match(/.*/)//+%00 16 | {$gt: ''} 17 | [$ne]=1 18 | ';sleep(5000); 19 | ';sleep(5000);' 20 | ';sleep(5000);+' 21 | ';it=new%20Date();do{pt=new%20Date();}while(pt-it<5000); -------------------------------------------------------------------------------- /internal/example/someextres/Storage.go: -------------------------------------------------------------------------------- 1 | package someextres 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | ) 7 | 8 | // example factory 9 | func NewStorage(connstr string) (*Storage, error) { 10 | //sql.Open(`driver`, connstr) ... 11 | return &Storage{}, nil 12 | } 13 | 14 | type Storage struct { 15 | DB *sql.DB 16 | } 17 | 18 | func (p Storage) Close() error { 19 | panic("implement me") 20 | } 21 | 22 | func (p Storage) BeginTx(ctx context.Context) (context.Context, error) { 23 | panic("implement me") 24 | } 25 | 26 | func (p Storage) CommitTx(ctx context.Context) error { 27 | panic("implement me") 28 | } 29 | 30 | func (p Storage) RollbackTx(ctx context.Context) error { 31 | panic("implement me") 32 | } 33 | -------------------------------------------------------------------------------- /httpspec/server.go: -------------------------------------------------------------------------------- 1 | package httpspec 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "net/url" 7 | 8 | "go.llib.dev/testcase" 9 | ) 10 | 11 | func LetServer(s *testcase.Spec, handler testcase.VarInit[http.Handler]) testcase.Var[*httptest.Server] { 12 | return testcase.Let(s, func(t *testcase.T) *httptest.Server { 13 | srv := httptest.NewServer(handler(t)) 14 | t.Defer(srv.Close) 15 | return srv 16 | }) 17 | } 18 | 19 | func ClientDo(t *testcase.T, srv *httptest.Server, r *http.Request) (*http.Response, error) { 20 | r = r.Clone(r.Context()) 21 | us, err := url.Parse(srv.URL) 22 | t.Must.NoError(err) 23 | r.URL.Scheme = us.Scheme 24 | r.URL.Host = us.Host 25 | r.RequestURI = "" 26 | return srv.Client().Do(r) 27 | } 28 | -------------------------------------------------------------------------------- /internal/person.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | type ContactOption interface { 4 | configure(*ContactConfig) 5 | } 6 | 7 | func ToContactConfig(opts ...ContactOption) ContactConfig { 8 | var c ContactConfig 9 | for _, opt := range opts { 10 | opt.configure(&c) 11 | } 12 | return c 13 | } 14 | 15 | type ContactConfig struct { 16 | SexType SexType 17 | } 18 | 19 | type SexType int 20 | 21 | func (st SexType) configure(c *ContactConfig) { 22 | if c.SexType == 0 { 23 | c.SexType = st 24 | return 25 | } 26 | if c.SexType == st { 27 | return 28 | } 29 | if c.SexType != st { 30 | c.SexType = SexTypeAny 31 | return 32 | } 33 | } 34 | 35 | const ( 36 | _ SexType = iota 37 | SexTypeMale 38 | SexTypeFemale 39 | SexTypeAny 40 | ) 41 | -------------------------------------------------------------------------------- /Global.go: -------------------------------------------------------------------------------- 1 | package testcase 2 | 3 | import "sync" 4 | 5 | // Global configures all *Spec which is made afterward of this call. 6 | // If you need a Spec#Before that runs in configured in every Spec, use this function. 7 | // It can be called multiple times, and then configurations will stack. 8 | var Global global 9 | 10 | type global struct { 11 | mutex sync.RWMutex 12 | beforeFns []func(t *T) 13 | } 14 | 15 | func (gc *global) Before(block func(t *T)) { 16 | gc.mutex.Lock() 17 | defer gc.mutex.Unlock() 18 | gc.beforeFns = append(gc.beforeFns, block) 19 | } 20 | 21 | func applyGlobal(s *Spec) { 22 | Global.mutex.RLock() 23 | defer Global.mutex.RUnlock() 24 | for _, block := range Global.beforeFns { 25 | s.Before(block) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This is a weird way of telling Travis to use the fast container-based test 3 | # runner instead of the slow VM-based runner. 4 | sudo: false 5 | 6 | language: go 7 | 8 | install: 9 | - . .envrc 10 | - go generate tools.go 11 | - go generate ./... 12 | 13 | script: 14 | - ./bin/test-go 15 | - ./bin/test-output 16 | 17 | after_success: 18 | - ./bin/test-go -coverprofile=coverage.txt -covermode=atomic 19 | - bash <(curl -s https://codecov.io/bash) 20 | 21 | os: 22 | - linux 23 | - osx 24 | 25 | go: 26 | - "1.x" 27 | - "master" 28 | 29 | matrix: 30 | allow_failures: 31 | - go: master 32 | 33 | branches: 34 | only: 35 | - master 36 | 37 | notifications: 38 | email: 39 | - adamluzsi@gmail.com 40 | -------------------------------------------------------------------------------- /internal/slicekit/slicekit.go: -------------------------------------------------------------------------------- 1 | package slicekit 2 | 3 | func ReverseLookup[T any](vs []T, index int) (T, bool) { 4 | return Lookup[T](vs, (-1*index)-1) 5 | } 6 | 7 | func Lookup[T any](vs []T, index int) (T, bool) { 8 | index, ok := normaliseIndex(len(vs), index) 9 | if !ok { 10 | var zero T 11 | return zero, false 12 | } 13 | return vs[index], true 14 | } 15 | 16 | // Merge will merge every []T slice into a single one. 17 | func Merge[T any](slices ...[]T) []T { 18 | var out []T 19 | for _, slice := range slices { 20 | out = append(out, slice...) 21 | } 22 | return out 23 | } 24 | 25 | func normaliseIndex(length, index int) (int, bool) { 26 | if index < 0 { 27 | n := length + index 28 | return n, 0 <= n 29 | } 30 | return index, index < length 31 | } 32 | -------------------------------------------------------------------------------- /sandbox/trace.go: -------------------------------------------------------------------------------- 1 | package sandbox 2 | 3 | import ( 4 | "path" 5 | "runtime" 6 | "strings" 7 | 8 | "go.llib.dev/testcase/internal/caller" 9 | ) 10 | 11 | func getFrames() (frames []runtime.Frame) { 12 | caller.Until(caller.NonTestCaseFrame, isNotSandboxPkg, func(frame runtime.Frame) bool { 13 | frames = append(frames, frame) 14 | return false 15 | }) 16 | return 17 | } 18 | 19 | var pkgDir string 20 | 21 | func init() { 22 | _, filePath, _, _ := runtime.Caller(0) // this caller 23 | pkgDir = path.Dir(filePath) 24 | } 25 | 26 | func isNotSandboxPkg(frame runtime.Frame) bool { 27 | switch { 28 | case caller.IsTestFileFrame(frame): 29 | return true 30 | case strings.HasPrefix(frame.File, pkgDir): 31 | return false 32 | default: 33 | return true 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /httpspec/ServeHTTP.go: -------------------------------------------------------------------------------- 1 | package httpspec 2 | 3 | import ( 4 | "net/http/httptest" 5 | 6 | "go.llib.dev/testcase" 7 | ) 8 | 9 | var serveOnce = testcase.Var[struct{}]{ 10 | ID: "httpspec:ServeHTTP", 11 | Init: func(t *testcase.T) struct{} { 12 | Handler.Get(t).ServeHTTP(ResponseRecorder.Get(t), InboundRequest.Get(t)) 13 | return struct{}{} 14 | }, 15 | } 16 | 17 | // ServeHTTP will make a request to the spec context 18 | // it requires the following spec variables 19 | // - Method -> http MethodGet 20 | // - Path -> http PathGet 21 | // - Query -> http query string 22 | // - Body -> http payload 23 | func ServeHTTP(t *testcase.T) *httptest.ResponseRecorder { 24 | serveOnce.Get(t) 25 | return ResponseRecorder.Get(t) 26 | } 27 | -------------------------------------------------------------------------------- /assert/message_test.go: -------------------------------------------------------------------------------- 1 | package assert_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "go.llib.dev/testcase/assert" 8 | "go.llib.dev/testcase/internal/doubles" 9 | "go.llib.dev/testcase/random" 10 | ) 11 | 12 | func ExampleMessage() { 13 | var tb testing.TB 14 | 15 | assert.True(tb, true, "this is a const which is interpreted as assertion.Message") 16 | } 17 | 18 | func TestMessage(t *testing.T) { 19 | dtb := &doubles.TB{} 20 | a := asserter(dtb) 21 | rnd := random.New(random.CryptoSeed{}) 22 | exp := assert.Message(rnd.String()) 23 | a.True(false, exp) 24 | assert.Contains(t, dtb.Logs.String(), strings.TrimSpace(string(exp))) 25 | } 26 | 27 | func TestMessagef(t *testing.T) { 28 | exp := assert.MessageF("answer:%d", 42) 29 | assert.Equal[assert.Message](t, exp, "answer:42") 30 | } 31 | -------------------------------------------------------------------------------- /internal/proxy/proxy_test.go: -------------------------------------------------------------------------------- 1 | package proxy_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "go.llib.dev/testcase/assert" 8 | "go.llib.dev/testcase/internal/doubles" 9 | "go.llib.dev/testcase/internal/proxy" 10 | ) 11 | 12 | func TestStubTimeNow(t *testing.T) { 13 | t.Run("default", func(t *testing.T) { 14 | now := time.Now() 15 | time.Sleep(time.Microsecond) 16 | assert.NotEqual(t, proxy.TimeNow(), now) 17 | }) 18 | 19 | t.Run("stub", func(t *testing.T) { 20 | now := time.Now() 21 | 22 | var dtb doubles.TB 23 | proxy.StubTimeNow(&dtb, func() time.Time { 24 | return now 25 | }) 26 | 27 | for i := 0; i < 42; i++ { 28 | assert.Equal(t, proxy.TimeNow(), now) 29 | } 30 | 31 | dtb.Finish() 32 | time.Sleep(time.Microsecond) 33 | assert.NotEqual(t, proxy.TimeNow(), now) 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /httpspec/example_Context_test.go: -------------------------------------------------------------------------------- 1 | package httpspec_test 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "go.llib.dev/testcase" 8 | "go.llib.dev/testcase/httpspec" 9 | ) 10 | 11 | func ExampleContext_withValue() { 12 | s := testcase.NewSpec(testingT) 13 | 14 | httpspec.Handler.Let(s, func(t *testcase.T) http.Handler { return MyHandler{} }) 15 | 16 | s.Before(func(t *testcase.T) { 17 | // This approach can help you representing middleware prerequisites. 18 | // Use httpspec.Context.Set only if you can't solve your goal 19 | // with httpspec.Context.Let or httpspec.Context.LetValue. 20 | httpspec.Context.Set(t, context.WithValue(httpspec.Context.Get(t), `foo`, `bar`)) 21 | }) 22 | 23 | s.Test(`the *http.InboundRequest#Context() will have foo-bar`, func(t *testcase.T) { 24 | httpspec.ServeHTTP(t) 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /internal/proxy/proxy.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | var rwm sync.RWMutex 10 | 11 | var _TimeNow = time.Now 12 | 13 | // TimeNow is designed to be independent of the clock, allowing it to function autonomously. 14 | // Features that are intended to be controlled by the clock should operate independently of any proxies. 15 | // For instance, generating a random unique value should remain unaffected by time travel. 16 | func TimeNow() time.Time { 17 | rwm.RLock() 18 | defer rwm.RUnlock() 19 | return _TimeNow() 20 | } 21 | 22 | func StubTimeNow(tb testing.TB, stub func() time.Time) { 23 | rwm.Lock() 24 | defer rwm.Unlock() 25 | prev := _TimeNow 26 | tb.Cleanup(func() { 27 | rwm.Lock() 28 | defer rwm.Unlock() 29 | _TimeNow = prev 30 | }) 31 | _TimeNow = stub 32 | } 33 | -------------------------------------------------------------------------------- /pp/PP.go: -------------------------------------------------------------------------------- 1 | package pp 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "runtime" 7 | "strings" 8 | "sync" 9 | 10 | "go.llib.dev/testcase/internal/caller" 11 | ) 12 | 13 | var l sync.Mutex 14 | 15 | func PP(vs ...any) { 16 | l.Lock() 17 | defer l.Unlock() 18 | _, file, line, _ := runtime.Caller(1) 19 | _, _ = fmt.Fprintf(defaultWriter, "%s ", caller.AsLocation(true, file, line)) 20 | _, _ = fpp(defaultWriter, vs...) 21 | } 22 | 23 | func FPP(w io.Writer, vs ...any) (int, error) { 24 | return fpp(w, vs...) 25 | } 26 | 27 | func fpp(w io.Writer, vs ...any) (int, error) { 28 | var ( 29 | form string 30 | args []any 31 | ) 32 | for _, v := range vs { 33 | form += "\t%s" 34 | args = append(args, Format(v)) 35 | } 36 | form = strings.TrimPrefix(form, "\t") 37 | form += fmt.Sprintln() 38 | return fmt.Fprintf(w, form, args...) 39 | } 40 | -------------------------------------------------------------------------------- /DSL_test.go: -------------------------------------------------------------------------------- 1 | package testcase_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "go.llib.dev/testcase" 7 | "go.llib.dev/testcase/assert" 8 | "go.llib.dev/testcase/internal" 9 | "go.llib.dev/testcase/internal/doubles" 10 | ) 11 | 12 | func TestSpec_TODO(t *testing.T) { 13 | dtb := &doubles.TB{} 14 | internal.StubVerbose(t, true) 15 | 16 | todos := []string{"abc", "bcd", "cde"} 17 | s := testcase.NewSpec(dtb) 18 | for _, todo := range todos { 19 | s.TODO(todo) 20 | } 21 | s.Finish() 22 | dtb.Finish() 23 | 24 | assert.False(t, dtb.IsFailed) 25 | assert.False(t, dtb.IsSkipped) 26 | 27 | for _, todo := range todos { 28 | assert.OneOf(t, dtb.Tests, func(t testing.TB, got *doubles.TB) { 29 | assert.Contains(t, got.Name(), "TODO: "+todo) 30 | assert.False(t, got.IsFailed) 31 | assert.True(t, got.IsSkipped) 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /docs/examples/role/main.go: -------------------------------------------------------------------------------- 1 | package role 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | //---------------------------------------------------------------------------------------------------------------------- 8 | // package mydomain 9 | 10 | type Entity struct { 11 | ID string 12 | Field1 string 13 | Field2 int 14 | //... 15 | } 16 | 17 | type Consumer struct { 18 | Storage Storage 19 | } 20 | 21 | // role interface 22 | type Storage interface { 23 | CreateEntity(ctx context.Context, ent *Entity) error 24 | FindEntityByID(ctx context.Context, ent *Entity, id string) (bool, error) 25 | } 26 | 27 | func (m Consumer) A(ctx context.Context, ent Entity) error { 28 | // validate entity 29 | 30 | if err := m.Storage.CreateEntity(ctx, &ent); err != nil { 31 | return err 32 | } 33 | 34 | // further steps here on entity 35 | 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /pp/unexported_test.go: -------------------------------------------------------------------------------- 1 | package pp_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "go.llib.dev/testcase/assert" 8 | "go.llib.dev/testcase/pp" 9 | ) 10 | 11 | const tmplFormatUnexportedFields = ` 12 | pp_test.X{ 13 | a: 1, 14 | b: 2, 15 | c: 3, 16 | d: "4", 17 | e: map[int]int{ 18 | 5: 6, 19 | }, 20 | f: []int{ 21 | 42, 22 | }, 23 | g: make(chan string, 1), 24 | } 25 | ` 26 | 27 | func TestFormat_unexportedFields(t *testing.T) { 28 | type X struct { 29 | a int 30 | b uint 31 | c float32 32 | d string 33 | e map[int]int 34 | f []int 35 | g chan string 36 | } 37 | v := X{ 38 | a: 1, 39 | b: 2, 40 | c: 3, 41 | d: "4", 42 | e: map[int]int{5: 6}, 43 | f: []int{42}, 44 | g: make(chan string, 1), 45 | } 46 | expected := strings.TrimSpace(tmplFormatUnexportedFields) 47 | assert.Equal(t, expected, pp.Format(v)) 48 | } 49 | -------------------------------------------------------------------------------- /Global_test.go: -------------------------------------------------------------------------------- 1 | package testcase 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | ) 7 | 8 | func TestGlobal_Before(t *testing.T) { 9 | t.Cleanup(func() { Global = global{} }) 10 | v := Var[int]{ 11 | ID: "v", 12 | Init: func(t *T) int { 13 | return 0 14 | }, 15 | } 16 | n1 := rand.Intn(10) 17 | Global.Before(func(t *T) { 18 | v.Set(t, v.Get(t)+n1) 19 | }) 20 | n2 := rand.Intn(10) 21 | Global.Before(func(t *T) { 22 | v.Set(t, v.Get(t)+n2) 23 | }) 24 | for i := 0; i < 42; i++ { 25 | s := NewSpec(t) 26 | s.Test("", func(t *T) { t.Must.Equal(n1+n2, v.Get(t)) }) 27 | } 28 | } 29 | 30 | func TestGlobal_Before_race(t *testing.T) { 31 | t.Cleanup(func() { Global = global{} }) 32 | Race(func() { 33 | Global.Before(func(t *T) {}) 34 | }, func() { 35 | Global.Before(func(t *T) {}) 36 | }, func() { 37 | Global.Before(func(t *T) {}) 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /httpspec/HandlerMiddleware_test.go: -------------------------------------------------------------------------------- 1 | package httpspec_test 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "go.llib.dev/testcase" 8 | "go.llib.dev/testcase/httpspec" 9 | ) 10 | 11 | func TestItBehavesLikeHandlerMiddleware(t *testing.T) { 12 | s := testcase.NewSpec(t) 13 | httpspec.ItBehavesLikeHandlerMiddleware(s, func(t *testcase.T, next http.Handler) http.Handler { 14 | return ExampleHandler{Next: next} 15 | }) 16 | } 17 | 18 | func TestHandlerMiddlewareContract_Spec(t *testing.T) { 19 | testcase.RunSuite(t, httpspec.HandlerMiddlewareContract{ 20 | Subject: func(t *testcase.T, next http.Handler) http.Handler { 21 | return ExampleHandler{Next: next} 22 | }, 23 | }) 24 | } 25 | 26 | type ExampleHandler struct { 27 | Next http.Handler 28 | } 29 | 30 | func (h ExampleHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 31 | h.Next.ServeHTTP(w, r) 32 | } 33 | -------------------------------------------------------------------------------- /deprecated_test.go: -------------------------------------------------------------------------------- 1 | package testcase 2 | 3 | import ( 4 | "testing" 5 | 6 | "go.llib.dev/testcase/assert" 7 | "go.llib.dev/testcase/random" 8 | ) 9 | 10 | func TestSpec_Let_andLetValue_backwardCompatibility(t *testing.T) { 11 | s := NewSpec(t) 12 | 13 | rnd := random.New(random.CryptoSeed{}) 14 | r1 := rnd.Int() 15 | r2 := rnd.Int() 16 | 17 | v1 := s.Let(`answer`, func(t *T) interface{} { return r1 }) 18 | v2 := s.LetValue(`count`, r2) 19 | 20 | s.Test(``, func(t *T) { 21 | t.Must.Equal(r1, v1.Get(t)) 22 | t.Must.Equal(r2, v2.Get(t)) 23 | }) 24 | } 25 | 26 | func TestSpec_LetValue_returnsVar(t *testing.T) { 27 | s := NewSpec(t) 28 | 29 | counter := s.LetValue(`counter`, 0) 30 | 31 | s.Test(``, func(t *T) { 32 | assert.Must(t).Equal(0, counter.Get(t)) 33 | counter.Set(t, 1) 34 | assert.Must(t).Equal(1, counter.Get(t)) 35 | counter.Set(t, 2) 36 | assert.Must(t).Equal(2, counter.Get(t)) 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /httpspec/RoundTripperMiddleware_test.go: -------------------------------------------------------------------------------- 1 | package httpspec_test 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "go.llib.dev/testcase" 8 | "go.llib.dev/testcase/httpspec" 9 | ) 10 | 11 | func TestItBehavesLikeRoundTripper(t *testing.T) { 12 | s := testcase.NewSpec(t) 13 | httpspec.ItBehavesLikeRoundTripperMiddleware(s, func(t *testcase.T, next http.RoundTripper) http.RoundTripper { 14 | return ExampleRoundTripper{Next: next} 15 | }) 16 | } 17 | 18 | func TestRoundTripperContract_Spec(t *testing.T) { 19 | testcase.RunSuite(t, httpspec.RoundTripperMiddlewareContract{ 20 | Subject: func(t *testcase.T, next http.RoundTripper) http.RoundTripper { 21 | return ExampleRoundTripper{Next: next} 22 | }, 23 | }) 24 | } 25 | 26 | type ExampleRoundTripper struct { 27 | Next http.RoundTripper 28 | } 29 | 30 | func (rt ExampleRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { 31 | return rt.Next.RoundTrip(r) 32 | } 33 | -------------------------------------------------------------------------------- /clock/internal/glob.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | var ( 9 | NowFunc func() time.Time 10 | SleepFunc func(d time.Duration) 11 | AfterFunc func(d time.Duration) <-chan time.Time 12 | SinceFunc func(start time.Time) time.Duration 13 | NewTickerFunc func(d time.Duration) *Ticker 14 | ) 15 | 16 | func init() { 17 | if testing.Testing() { 18 | useClockFunctions() 19 | } else { 20 | useTimeFunctions() 21 | } 22 | } 23 | 24 | func useTimeFunctions() struct{} { 25 | NowFunc = time.Now 26 | SleepFunc = time.Sleep 27 | AfterFunc = time.After 28 | NewTickerFunc = timeNewTicker 29 | SinceFunc = time.Since 30 | return struct{}{} 31 | } 32 | 33 | // useClockFunctions will enable the ability to time travel during testing. 34 | func useClockFunctions() { 35 | NowFunc = Now 36 | SleepFunc = Sleep 37 | AfterFunc = After 38 | NewTickerFunc = NewTicker 39 | SinceFunc = Since 40 | } 41 | -------------------------------------------------------------------------------- /internal/assertlite/assertlite.go: -------------------------------------------------------------------------------- 1 | package assertlite 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func True(tb testing.TB, ok bool, msg ...any) { 10 | tb.Helper() 11 | 12 | if !ok { 13 | tb.Fatal(msg...) 14 | } 15 | } 16 | 17 | func False(tb testing.TB, nok bool, msg ...any) { 18 | tb.Helper() 19 | 20 | True(tb, !nok, msg...) 21 | } 22 | 23 | func Equal[T any](tb testing.TB, x, y T, msg ...any) { 24 | tb.Helper() 25 | 26 | True(tb, reflect.DeepEqual(x, y), msg...) 27 | } 28 | 29 | func Contains(tb testing.TB, haystack, needle string) { 30 | tb.Helper() 31 | 32 | if !strings.Contains(haystack, needle) { 33 | tb.Fatalf("\nhaystack: %#v\nneedle: %#v\n", haystack, needle) 34 | } 35 | } 36 | 37 | func NotContains(tb testing.TB, haystack, needle string) { 38 | tb.Helper() 39 | 40 | if strings.Contains(haystack, needle) { 41 | tb.Fatalf("\nShould not contain!\nhaystack: %#v\nneedle: %#v\n", haystack, needle) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /docs/examples/ValidateName_test.go: -------------------------------------------------------------------------------- 1 | package examples_test 2 | 3 | import ( 4 | "math/rand" 5 | "strings" 6 | "testing" 7 | 8 | "go.llib.dev/testcase" 9 | "go.llib.dev/testcase/assert" 10 | "go.llib.dev/testcase/docs/examples" 11 | ) 12 | 13 | func TestValidateName(t *testing.T) { 14 | s := testcase.NewSpec(t) 15 | 16 | name := testcase.Var[string]{ID: `name`} 17 | 18 | var subject = func(t *testcase.T) error { 19 | return examples.ValidateName(name.Get(t)) 20 | } 21 | 22 | s.When(`is perfect`, func(s *testcase.Spec) { 23 | name.LetValue(s, `The answer is 42`) 24 | 25 | s.Then(`it will be accepted without a problem`, func(t *testcase.T) { 26 | assert.Must(t).Nil(subject(t)) 27 | }) 28 | }) 29 | 30 | s.When(`is really long`, func(s *testcase.Spec) { 31 | name.LetValue(s, strings.Repeat(`x`, 128+rand.Intn(42)+1)) 32 | 33 | s.Then(`it will that the name is too long`, func(t *testcase.T) { 34 | assert.Must(t).Equal(examples.ErrTooLong, subject(t)) 35 | }) 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /docs/examples/if_test.go: -------------------------------------------------------------------------------- 1 | package examples_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "go.llib.dev/testcase" 7 | "go.llib.dev/testcase/assert" 8 | ) 9 | 10 | func IfSubject(condition bool) string { 11 | if condition { 12 | return `A` 13 | } else { 14 | return `B` 15 | } 16 | } 17 | 18 | func TestIfSubject(t *testing.T) { 19 | s := testcase.NewSpec(t) 20 | 21 | var ( 22 | condition = testcase.Var[bool]{ID: `condition`} 23 | subject = func(t *testcase.T) string { 24 | return IfSubject(condition.Get(t)) 25 | } 26 | ) 27 | 28 | s.When(`condition described`, func(s *testcase.Spec) { 29 | condition.LetValue(s, true) 30 | 31 | s.Then(`it will return ...`, func(t *testcase.T) { 32 | assert.Must(t).Equal(`A`, subject(t)) 33 | }) 34 | }) 35 | 36 | s.When(`condition opposite described`, func(s *testcase.Spec) { 37 | condition.LetValue(s, false) 38 | 39 | s.Then(`it will return ...`, func(t *testcase.T) { 40 | assert.Must(t).Equal(`B`, subject(t)) 41 | }) 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /httpspec/example_test.go: -------------------------------------------------------------------------------- 1 | package httpspec_test 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "testing" 7 | 8 | "go.llib.dev/testcase" 9 | "go.llib.dev/testcase/httpspec" 10 | ) 11 | 12 | func Example_usage() { 13 | var tb testing.TB 14 | s := testcase.NewSpec(tb) 15 | 16 | // subject 17 | httpspec.Handler.Let(s, func(t *testcase.T) http.Handler { 18 | return MyHandler{} 19 | }) 20 | 21 | // Arrange 22 | httpspec.ContentTypeIsJSON(s) 23 | httpspec.Method.LetValue(s, http.MethodPost) 24 | httpspec.Path.LetValue(s, `/`) 25 | httpspec.Body.Let(s, func(t *testcase.T) interface{} { 26 | // this will end up as {"foo":"bar"} in the request body 27 | return map[string]string{"foo": "bar"} 28 | }) 29 | 30 | s.Then(`it will...`, func(t *testcase.T) { 31 | // ServeHTTP 32 | rr := httpspec.ServeHTTP(t) 33 | 34 | // Assert 35 | t.Must.Equal(http.StatusOK, rr.Code) 36 | var resp CreateResponse 37 | t.Must.Nil(json.Unmarshal(rr.Body.Bytes(), &resp)) 38 | // ... 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /docs/spechelper.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | - [Spec Helper](#spec-helper) 5 | 6 | 7 | 8 | # Spec Helper 9 | 10 | draft: 11 | - the goal of TDD in Software Design 12 | - the goal of software design concepts such as domain driven design and how to embrace them in TDD 13 | - common premature optimizations which can be harmful without proper measurement in the design 14 | - The initially invisible issues with tests made for different architecture layers from maintainability aspects 15 | - the 5 type of double entity, and they costs 16 | - the long term cost of ignoring maintainability aspects of the testing suite 17 | - what problem a `testcase` `spechelper` package solves? 18 | - how to use it 19 | - how to build testing components 20 | - optimization techniques to avoid testing suite maintainability 21 | - examples 22 | -------------------------------------------------------------------------------- /pp/examples_test.go: -------------------------------------------------------------------------------- 1 | package pp_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "go.llib.dev/testcase/pp" 9 | ) 10 | 11 | type ExampleStruct struct { 12 | A string 13 | B int 14 | } 15 | 16 | func ExampleFormat() { 17 | _ = pp.Format(ExampleStruct{ 18 | A: "The Answer", 19 | B: 42, 20 | }) 21 | } 22 | 23 | func ExampleDiff() { 24 | pp.DiffFormat(ExampleStruct{ 25 | A: "The Answer", 26 | B: 42, 27 | }, ExampleStruct{ 28 | A: "The Question", 29 | B: 42, 30 | }) 31 | } 32 | 33 | func ExampleDiffFormat() { 34 | fmt.Println(pp.DiffFormat(ExampleStruct{ 35 | A: "The Answer", 36 | B: 42, 37 | }, ExampleStruct{ 38 | A: "The Question", 39 | B: 42, 40 | })) 41 | } 42 | 43 | func ExampleDiffString() { 44 | _ = pp.DiffFormat("aaa\nbbb\nccc\n", "aaa\nccc\n") 45 | } 46 | 47 | func ExamplePP_unexportedFields() { 48 | var buf bytes.Buffer 49 | bs, _ := json.Marshal(ExampleStruct{ 50 | A: "The Answer", 51 | B: 42, 52 | }) 53 | buf.Write(bs) 54 | 55 | pp.PP(buf) 56 | } 57 | -------------------------------------------------------------------------------- /assert/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | - [testcase/assert](#testcaseassert) 5 | 6 | 7 | 8 | # testcase/assert 9 | 10 | This package meant to provide a small and dependency lightweight implementation for common assertion related 11 | requirements. 12 | 13 | - [Go pkg documentation](https://pkg.go.dev/go.llib.dev/testcase/assert) 14 | 15 | Example: 16 | 17 | ```go 18 | assert.Should(tb).True(true) 19 | assert.Must(tb).Equal(expected, actual, "error message") 20 | assert.Must(tb).NotEqual(true, false, "exp") 21 | assert.Must(tb).Contain([]int{1, 2, 3}, 3, "exp") 22 | assert.Must(tb).Contain([]int{1, 2, 3}, []int{1, 2}, "exp") 23 | assert.Must(tb).Contain(map[string]int{"The Answer": 42, "oth": 13}, map[string]int{"The Answer": 42}, "exp") 24 | ``` 25 | 26 | For more examples, check out the [example_test.go](./example_test.go) file. -------------------------------------------------------------------------------- /httpspec/contenttype_test.go: -------------------------------------------------------------------------------- 1 | package httpspec_test 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/http" 7 | "testing" 8 | 9 | "go.llib.dev/testcase" 10 | "go.llib.dev/testcase/httpspec" 11 | ) 12 | 13 | func TestContentTypeIsJSON(t *testing.T) { 14 | s := testcase.NewSpec(t) 15 | 16 | var actually map[string]string 17 | httpspec.Handler.Let(s, func(t *testcase.T) http.Handler { 18 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 19 | defer r.Body.Close() 20 | t.Must.Equal(`application/json`, r.Header.Get(`Content-Type`)) 21 | bs, err := ioutil.ReadAll(r.Body) 22 | t.Must.Nil(err) 23 | t.Must.Nil(json.Unmarshal(bs, &actually)) 24 | }) 25 | }) 26 | 27 | httpspec.ContentTypeIsJSON(s) 28 | 29 | expected := map[string]string{"hello": "world"} 30 | httpspec.Body.Let(s, func(t *testcase.T) interface{} { return expected }) 31 | 32 | s.Test(`test json encoding for actually`, func(t *testcase.T) { 33 | httpspec.ServeHTTP(t) 34 | 35 | t.Must.Equal(expected, actually) 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /random/deprecated.go: -------------------------------------------------------------------------------- 1 | package random 2 | 3 | import "go.llib.dev/testcase/internal" 4 | 5 | // Name 6 | // 7 | // Deprecated: please use Random.Contact instead 8 | func (r *Random) Name() contactGenerator { 9 | return contactGenerator{Random: r} 10 | } 11 | 12 | // First 13 | // 14 | // Deprecated: please use Contact.FirstName from Random.Contact instead 15 | func (cg contactGenerator) First(opts ...internal.ContactOption) string { 16 | return cg.first(internal.ToContactConfig(opts...)) 17 | } 18 | 19 | // Last 20 | // 21 | // Deprecated: please use Contact.LastName from Random.Contact instead 22 | func (cg contactGenerator) Last() string { 23 | return cg.last() 24 | } 25 | 26 | // Email 27 | // 28 | // Deprecated: please use Contact.Email from Random.Contact instead 29 | func (r *Random) Email() string { 30 | ng := contactGenerator{Random: r} 31 | return ng.email(ng.first(internal.ToContactConfig()), ng.last()) 32 | } 33 | 34 | // SliceElement 35 | // 36 | // Deprecated: use random.Random#Pick instead 37 | func (r *Random) SliceElement(slice any) any { return r.Pick(slice) } 38 | -------------------------------------------------------------------------------- /faultinject/CallerFault.go: -------------------------------------------------------------------------------- 1 | package faultinject 2 | 3 | import ( 4 | "strings" 5 | 6 | "go.llib.dev/testcase/internal/caller" 7 | ) 8 | 9 | // CallerFault allows you to inject Fault by Caller stack position. 10 | type CallerFault struct { 11 | Package string 12 | Receiver string 13 | Function string 14 | } 15 | 16 | func (ff CallerFault) check() bool { 17 | return caller.MatchFunc(func(fn caller.Func) bool { 18 | if !ff.isPackage(fn) { 19 | return false 20 | } 21 | if !ff.isReceiver(fn) { 22 | return false 23 | } 24 | if !ff.isFunction(fn) { 25 | return false 26 | } 27 | return true 28 | }) 29 | } 30 | 31 | func (ff CallerFault) isPackage(fn caller.Func) bool { 32 | return ff.Package == "" || ff.Package == fn.Package 33 | } 34 | 35 | func (ff CallerFault) isReceiver(fn caller.Func) bool { 36 | return ff.Receiver == "" || 37 | ff.Receiver == fn.Receiver || 38 | ff.Receiver == strings.TrimPrefix(ff.Receiver, "*") 39 | } 40 | 41 | func (ff CallerFault) isFunction(fn caller.Func) bool { 42 | return ff.Function == "" || ff.Function == fn.Funcion 43 | } 44 | -------------------------------------------------------------------------------- /todo-linter.md: -------------------------------------------------------------------------------- 1 | Understandable 2 | Tests should shout out what it is that they are testing and asserting. 3 | 4 | Maintainable 5 | Good tests should be easy to change, and should make it easy to change the code that they are testing too. 6 | 7 | Repeatable 8 | We should get the same result for the same version of the code every time we run them. So no timezone or concurrency problems. 9 | 10 | Atomic 11 | A good test should stand alone and not depend on arrangements it doesn't control. 12 | 13 | Necessary 14 | Good tests are there for a reason, they aren't randomly testing things, they express a new, different perspective on our code. 15 | 16 | Granular 17 | A test should assert a single outcome. We don't what to have to wade through logs to understand why a test fails, it should be crystal clear. 18 | 19 | Fast 20 | Good tests are fast, they run quickly enough that we are happy to run them after even tiny changes to our code. 21 | 22 | Simple 23 | As well as asserting a single outcome, good tests in good systems are very simple. My favourite description is from 24 | @JonJagger 25 | "A good test has a cyclomatic complexity of 1" 26 | -------------------------------------------------------------------------------- /internal/environ/environ_test.go: -------------------------------------------------------------------------------- 1 | package environ_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "go.llib.dev/testcase/assert" 8 | "go.llib.dev/testcase/internal" 9 | "go.llib.dev/testcase/internal/env" 10 | "go.llib.dev/testcase/internal/environ" 11 | "go.llib.dev/testcase/random" 12 | ) 13 | 14 | func Test_checkEnvKeys(t *testing.T) { 15 | t.Run("when invalid testcase env variable is present in the env", func(t *testing.T) { 16 | out := internal.StubWarn(t) 17 | rnd := random.New(random.CryptoSeed{}) 18 | key := fmt.Sprintf("TESTCASE_%s", rnd.StringNC(rnd.IntB(0, 10), random.CharsetAlpha())) 19 | val := rnd.StringNC(5, random.CharsetAlpha()+random.CharsetDigit()) 20 | env.SetEnv(t, key, val) 21 | environ.CheckEnvKeys() 22 | assert.NotEmpty(t, out.String()) 23 | assert.Contains(t, out.String(), key) 24 | assert.Contains(t, out.String(), "typo") 25 | }) 26 | t.Run("when only valid env variables are present in the env", func(t *testing.T) { 27 | out := internal.StubWarn(t) 28 | env.SetEnv(t, environ.KeySeed, "123") 29 | env.SetEnv(t, environ.KeyOrdering, "defined") 30 | environ.CheckEnvKeys() 31 | assert.Empty(t, out.String()) 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /httpspec/example_ContentTypeIsJSON_test.go: -------------------------------------------------------------------------------- 1 | package httpspec_test 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "go.llib.dev/testcase" 8 | "go.llib.dev/testcase/assert" 9 | 10 | . "go.llib.dev/testcase/httpspec" 11 | ) 12 | 13 | func ExampleContentTypeIsJSON() { 14 | s := testcase.NewSpec(testingT) 15 | 16 | Handler.Let(s, func(t *testcase.T) http.Handler { return MyHandler{} }) 17 | ContentTypeIsJSON(s) 18 | 19 | s.Describe(`POST / - create X`, func(s *testcase.Spec) { 20 | Method.LetValue(s, http.MethodPost) 21 | Path.LetValue(s, `/`) 22 | 23 | Body.Let(s, func(t *testcase.T) interface{} { 24 | // this will end up as {"foo":"bar"} in the request body 25 | return map[string]string{"foo": "bar"} 26 | }) 27 | 28 | var onSuccess = func(t *testcase.T) CreateResponse { 29 | rr := ServeHTTP(t) 30 | assert.Must(t).Equal(http.StatusOK, rr.Code) 31 | var resp CreateResponse 32 | assert.Must(t).Nil(json.Unmarshal(rr.Body.Bytes(), &resp)) 33 | return resp 34 | } 35 | 36 | s.Then(`it will create a new resource`, func(t *testcase.T) { 37 | createResponse := onSuccess(t) 38 | // assert 39 | _ = createResponse 40 | }) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /docs/testing-double/fake_test.go: -------------------------------------------------------------------------------- 1 | package testingdouble_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "go.llib.dev/testcase" 8 | "go.llib.dev/testcase/random" 9 | ) 10 | 11 | type FakeXYStorage map[string]XY 12 | 13 | func (f FakeXYStorage) CreateXY(ctx context.Context, ptr *XY) error { 14 | rnd := random.New(random.CryptoSeed{}) 15 | if ptr.ID == `` { 16 | ptr.ID = rnd.StringN(42) // not safe 17 | } 18 | f[ptr.ID] = *ptr 19 | return nil 20 | } 21 | 22 | func (f FakeXYStorage) FindXYByID(ctx context.Context, ptr *XY, id string) (found bool, err error) { 23 | ent, ok := f[id] 24 | if !ok { 25 | return false, nil 26 | } 27 | *ptr = ent 28 | return true, nil 29 | } 30 | 31 | // file: FakeXYEntityStorage_test.go 32 | 33 | var _ XYStorage = FakeXYStorage{} 34 | 35 | func TestFakeXYEntityStorage_suppliesXYStorageContract(t *testing.T) { 36 | XYStorageContract{ 37 | Subject: func(tb testing.TB) XYStorage { 38 | return make(FakeXYStorage) 39 | }, 40 | MakeCtx: func(tb testing.TB) context.Context { 41 | return context.Background() 42 | }, 43 | MakeXY: func(tb testing.TB) *XY { 44 | t := testcase.NewTWithSpec(tb, nil) 45 | return t.Random.Make(new(XY)).(*XY) 46 | }, 47 | }.Test(t) 48 | } 49 | -------------------------------------------------------------------------------- /assert/Waiter.go: -------------------------------------------------------------------------------- 1 | package assert 2 | 3 | import ( 4 | "time" 5 | 6 | "go.llib.dev/testcase/internal/wait" 7 | ) 8 | 9 | // Waiter is a component that waits for a time, event, or opportunity. 10 | type Waiter struct { 11 | // WaitDuration is the time how lone Waiter.Wait should wait between attempting a new retry during Waiter.While. 12 | WaitDuration time.Duration 13 | // Timeout is used to calculate the deadline for the Waiter.While call. 14 | // If the retry takes longer than the Timeout, the retry will be cancelled. 15 | Timeout time.Duration 16 | } 17 | 18 | // Wait will attempt to wait a bit and leave breathing space for other goroutines to steal processing time. 19 | // It will also attempt to schedule other goroutines. 20 | func (w Waiter) Wait() { 21 | wait.For(w.WaitDuration) 22 | } 23 | 24 | // While will wait until a condition met, or until the wait timeout. 25 | // By default, if the timeout is not defined, it just attempts to execute the condition once. 26 | // Calling multiple times the condition function should be a safe operation. 27 | func (w Waiter) While(condition func() bool) { 28 | finishTime := time.Now().Add(w.Timeout) 29 | for condition() && time.Now().Before(finishTime) { 30 | w.Wait() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tags.go: -------------------------------------------------------------------------------- 1 | package testcase 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "sync" 7 | ) 8 | 9 | type tagSettings struct { 10 | Include map[string]struct{} 11 | Exclude map[string]struct{} 12 | } 13 | 14 | const ( 15 | envKeyTagIncludeList = `TESTCASE_TAG_INCLUDE` 16 | envKeyTagExcludeList = `TESTCASE_TAG_EXCLUDE` 17 | ) 18 | 19 | func getTagSettings() tagSettings { 20 | var settings = tagSettings{ 21 | Include: map[string]struct{}{}, 22 | Exclude: map[string]struct{}{}, 23 | } 24 | 25 | if rawList, ok := os.LookupEnv(envKeyTagIncludeList); ok { 26 | for _, rawTag := range strings.Split(rawList, `,`) { 27 | tag := strings.TrimSpace(rawTag) 28 | settings.Include[tag] = struct{}{} 29 | } 30 | } 31 | 32 | if rawList, ok := os.LookupEnv(envKeyTagExcludeList); ok { 33 | for _, rawTag := range strings.Split(rawList, `,`) { 34 | tag := strings.TrimSpace(rawTag) 35 | settings.Exclude[tag] = struct{}{} 36 | } 37 | } 38 | 39 | return settings 40 | } 41 | 42 | var ( 43 | tagSettingsSetup sync.Once 44 | tagSettingsCache tagSettings 45 | ) 46 | 47 | func getCachedTagSettings() tagSettings { 48 | tagSettingsSetup.Do(func() { 49 | tagSettingsCache = getTagSettings() 50 | }) 51 | 52 | return tagSettingsCache 53 | } 54 | -------------------------------------------------------------------------------- /docs/why-to-test.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | - [Why to Test?](#why-to-test) 5 | 6 | 7 | 8 | # Why to Test? 9 | 10 | The primary goal of testing is increase the speed at you gain feedback on the current system design. 11 | Usually when something is difficult to test, it is a good sign, 12 | that the design might suffer from coupling, violates design principles. 13 | In the below guide, each convention of the framework will be explained what design problem it tries to indicate. 14 | 15 | The secondary goal of testing is to decouple the workflow into smaller units, 16 | thus increase the efficiency by reducing the need for high mental model capacity. 17 | As a bonus, working with tests allow you to be more resilient against random interruptions, 18 | or get back faster to an older code base which you didn't touch since long time if ever. 19 | 20 | As a side effect of these conventions, the project naturally gain greater architecture flexibility and maintainability. 21 | The lack of these aspects often credited for projects labelled as "legacy". -------------------------------------------------------------------------------- /random/crypto.go: -------------------------------------------------------------------------------- 1 | package random 2 | 3 | import ( 4 | "crypto/rand" 5 | "math/big" 6 | ) 7 | 8 | type CryptoSeed struct{} 9 | 10 | func (c CryptoSeed) Uint64() uint64 { 11 | // MaxInt gives the current system's maximum integer 12 | // ^ means invert bits in the expression so if: uint(0) == 0000...0000 (exactly 32 or 64 zero bits depending on build target architecture) 13 | // then ^unit(0) == 1111...1111 which gives us the maximum value for the unsigned integer (all ones). 14 | // Then we need to shift integers to the right since the first bit store the sign in an int, 15 | // which gives us ^uint(0) >> 1 == 0111...1111. 16 | const MaxInt = int(^uint(0) >> 1) 17 | nBig, err := rand.Int(rand.Reader, big.NewInt(int64(MaxInt))) 18 | if err != nil { 19 | panic(err) 20 | } 21 | return nBig.Uint64() 22 | } 23 | 24 | // Int63 returns a non-negative pseudo-random 63-bit integer as an int64. 25 | func (c CryptoSeed) Int63() int64 { 26 | const ( 27 | rngMax = 1 << 63 28 | rngMask = rngMax - 1 29 | ) 30 | return int64(c.Uint64() & rngMask) 31 | } 32 | 33 | // Seed should use the provided seed value to initialize the generator to a deterministic state, 34 | // but in CryptoSeed, the value is ignored. 35 | func (c CryptoSeed) Seed(seed int64) {} 36 | -------------------------------------------------------------------------------- /internal/wait/wait_test.go: -------------------------------------------------------------------------------- 1 | package wait_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "go.llib.dev/testcase" 9 | "go.llib.dev/testcase/assert" 10 | "go.llib.dev/testcase/internal/wait" 11 | "go.llib.dev/testcase/pp" 12 | ) 13 | 14 | func TestFor(t *testing.T) { 15 | s := testcase.NewSpec(t) 16 | 17 | s.Test("smoke", func(t *testcase.T) { 18 | done := make(chan struct{}) 19 | defer close(done) 20 | 21 | var waitFor = time.Millisecond 22 | var timeout = adjustDuration(waitFor, 1.4) 23 | 24 | for i := 0; i < 1024; i++ { 25 | assert.Within(t, timeout, func(ctx context.Context) { 26 | wait.For(waitFor) 27 | }) 28 | } 29 | }, testcase.Flaky(3)) 30 | } 31 | 32 | func TestOthers(t *testing.T) { 33 | done := make(chan struct{}) 34 | defer close(done) 35 | 36 | var waitFor = time.Millisecond * 5 37 | var timeout = adjustDuration(waitFor, 1.3) 38 | t.Log("timeout", pp.Format(timeout)) 39 | t.Log("waitFor", pp.Format(waitFor)) 40 | 41 | for i := 0; i < 1024; i++ { 42 | s := time.Now() 43 | wait.Others(waitFor) 44 | d := time.Since(s) 45 | assert.True(t, d <= timeout) 46 | } 47 | } 48 | 49 | func adjustDuration(d time.Duration, m float64) time.Duration { 50 | return time.Duration(float64(d) * m) 51 | } 52 | -------------------------------------------------------------------------------- /Sandbox_test.go: -------------------------------------------------------------------------------- 1 | package testcase_test 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "testing" 7 | 8 | "go.llib.dev/testcase" 9 | "go.llib.dev/testcase/assert" 10 | "go.llib.dev/testcase/internal/doubles" 11 | "go.llib.dev/testcase/random" 12 | "go.llib.dev/testcase/sandbox" 13 | ) 14 | 15 | func ExampleSandbox() { 16 | stb := &doubles.TB{} 17 | outcome := testcase.Sandbox(func() { 18 | // some test helper function calls fatal, which cause runtime.Goexit after marking the test failed. 19 | stb.FailNow() 20 | }) 21 | 22 | fmt.Println("The sandbox run has finished without an issue", outcome.OK) 23 | fmt.Println("runtime.Goexit was called:", outcome.Goexit) 24 | fmt.Println("panic value:", outcome.PanicValue) 25 | } 26 | 27 | func TestSandbox_smoke(t *testing.T) { 28 | var out sandbox.RunOutcome 29 | out = testcase.Sandbox(func() {}) 30 | assert.True(t, out.OK) 31 | assert.Nil(t, out.PanicValue) 32 | 33 | out = testcase.Sandbox(func() { runtime.Goexit() }) 34 | assert.False(t, out.OK) 35 | assert.Nil(t, out.PanicValue) 36 | 37 | expectedPanicValue := random.New(random.CryptoSeed{}).Error() 38 | out = testcase.Sandbox(func() { panic(expectedPanicValue) }) 39 | assert.False(t, out.OK) 40 | assert.Equal[any](t, expectedPanicValue, out.PanicValue) 41 | } 42 | -------------------------------------------------------------------------------- /docs/examples/func_test.go: -------------------------------------------------------------------------------- 1 | package examples_test 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "regexp" 7 | "strings" 8 | "testing" 9 | 10 | "go.llib.dev/testcase" 11 | "go.llib.dev/testcase/assert" 12 | ) 13 | 14 | // config 15 | var Greetings = []string{`Hello`, `Hola`, `Howdy`} 16 | 17 | // package mydomain 18 | 19 | func Say(name string) string { 20 | return fmt.Sprintf(`%s %s!`, Greetings[rand.Intn(len(Greetings))], name) 21 | } 22 | 23 | // package mydomain test 24 | func TestSay(t *testing.T) { 25 | s := testcase.NewSpec(t) 26 | 27 | var ( 28 | name = testcase.Let(s, func(t *testcase.T) string { 29 | return t.Random.String() 30 | }) 31 | nameGet = func(t *testcase.T) string { return name.Get(t) } 32 | subject = func(t *testcase.T) string { 33 | return Say(nameGet(t)) 34 | } 35 | ) 36 | 37 | s.Then(`it will include the name`, func(t *testcase.T) { 38 | t.Must.Contains(subject(t), nameGet(t)) 39 | }) 40 | 41 | s.Then(`it should end the sentence with an exclamation mark`, func(t *testcase.T) { 42 | assert.Must(t).True(strings.HasSuffix(subject(t), `!`)) 43 | }) 44 | 45 | s.Then(`it should use one of the greeting`, func(t *testcase.T) { 46 | t.Must.Contains(Greetings, regexp.MustCompile(`([^\s]+)`).FindString(subject(t))) 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /internal/reflects/IsMutable.go: -------------------------------------------------------------------------------- 1 | package reflects 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | var acceptedConstKind = map[reflect.Kind]struct{}{ 8 | reflect.String: {}, 9 | reflect.Bool: {}, 10 | reflect.Int: {}, 11 | reflect.Int8: {}, 12 | reflect.Int16: {}, 13 | reflect.Int32: {}, 14 | reflect.Int64: {}, 15 | reflect.Uint: {}, 16 | reflect.Uint8: {}, 17 | reflect.Uint16: {}, 18 | reflect.Uint32: {}, 19 | reflect.Uint64: {}, 20 | reflect.Float32: {}, 21 | reflect.Float64: {}, 22 | reflect.Complex64: {}, 23 | reflect.Complex128: {}, 24 | } 25 | 26 | func IsMutable(v any) bool { 27 | if IsNil(v) { 28 | return false 29 | } 30 | return visitIsMutable(reflect.ValueOf(v)) 31 | } 32 | 33 | func visitIsMutable(rv reflect.Value) bool { 34 | if rv.Kind() == reflect.Invalid { 35 | return false 36 | } 37 | if _, ok := acceptedConstKind[rv.Kind()]; ok { 38 | return false 39 | } 40 | if rv.Kind() == reflect.Struct { 41 | fieldNum := rv.NumField() 42 | for i, fNum := 0, fieldNum; i < fNum; i++ { 43 | name := rv.Type().Field(i).Name 44 | field := rv.FieldByName(name) 45 | if visitIsMutable(field) { 46 | return true 47 | } 48 | } 49 | return false 50 | } 51 | return true 52 | } 53 | -------------------------------------------------------------------------------- /internal/reflects/is_test.go: -------------------------------------------------------------------------------- 1 | package reflects_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "go.llib.dev/testcase/assert" 8 | "go.llib.dev/testcase/internal/reflects" 9 | ) 10 | 11 | func TestIsNil(t *testing.T) { 12 | type TestCase struct { 13 | V func() any 14 | IsNil bool 15 | } 16 | 17 | type T struct{} 18 | 19 | for _, tc := range []TestCase{ 20 | { 21 | V: func() any { return nil }, 22 | IsNil: true, 23 | }, 24 | { 25 | V: func() any { 26 | var v *T 27 | return v 28 | }, 29 | IsNil: true, 30 | }, 31 | { 32 | V: func() any { 33 | return &T{} 34 | }, 35 | IsNil: false, 36 | }, 37 | { 38 | V: func() any { 39 | var v map[string]string 40 | return v 41 | }, 42 | IsNil: true, 43 | }, 44 | { 45 | V: func() any { 46 | return map[string]string{} 47 | }, 48 | IsNil: false, 49 | }, 50 | { 51 | V: func() any { 52 | var v []string 53 | return v 54 | }, 55 | IsNil: true, 56 | }, 57 | { 58 | V: func() any { 59 | return []string{} 60 | }, 61 | IsNil: false, 62 | }, 63 | } { 64 | tc := tc 65 | v := tc.V() 66 | t.Run(fmt.Sprintf("%T - %v", v, v), func(t *testing.T) { 67 | assert.Equal(t, tc.IsNil, reflects.IsNil(v)) 68 | }) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /random/internal/fixture/assets/contacts/malenames.txt: -------------------------------------------------------------------------------- 1 | Aaron 2 | Adam 3 | Alan 4 | Albert 5 | Alexander 6 | Andrew 7 | Anthony 8 | Arthur 9 | Austin 10 | Benjamin 11 | Billy 12 | Bobby 13 | Brandon 14 | Brian 15 | Bruce 16 | Bryan 17 | Carl 18 | Charles 19 | Christian 20 | Christopher 21 | Daniel 22 | David 23 | Dennis 24 | Donald 25 | Douglas 26 | Dylan 27 | Edward 28 | Elijah 29 | Eric 30 | Ethan 31 | Eugene 32 | Frank 33 | Gabriel 34 | Gary 35 | George 36 | Gerald 37 | Gregory 38 | Harold 39 | Henry 40 | Jack 41 | Jacob 42 | James 43 | Jason 44 | Jeffrey 45 | Jeremy 46 | Jerry 47 | Jesse 48 | Joe 49 | John 50 | Jonathan 51 | Jordan 52 | Jose 53 | Joseph 54 | Joshua 55 | Juan 56 | Justin 57 | Keith 58 | Kenneth 59 | Kevin 60 | Kyle 61 | Larry 62 | Lawrence 63 | Logan 64 | Louis 65 | Mark 66 | Mason 67 | Matthew 68 | Michael 69 | Nathan 70 | Nicholas 71 | Noah 72 | Patrick 73 | Paul 74 | Peter 75 | Philip 76 | Ralph 77 | Randy 78 | Raymond 79 | Richard 80 | Robert 81 | Roger 82 | Ronald 83 | Roy 84 | Russell 85 | Ryan 86 | Samuel 87 | Scott 88 | Sean 89 | Stephen 90 | Steven 91 | Terry 92 | Thomas 93 | Timothy 94 | Tyler 95 | Vincent 96 | Walter 97 | Wayne 98 | William 99 | Willie 100 | Zachary 101 | -------------------------------------------------------------------------------- /httpspec/utils.go: -------------------------------------------------------------------------------- 1 | package httpspec 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "reflect" 7 | ) 8 | 9 | func toURLValues(i interface{}) url.Values { 10 | if data, ok := i.(url.Values); ok { 11 | return data 12 | } 13 | 14 | rv := reflect.ValueOf(i) 15 | data := url.Values{} 16 | 17 | switch rv.Kind() { 18 | case reflect.Struct: 19 | rt := reflect.TypeOf(i) 20 | for i := 0; i < rv.NumField(); i++ { 21 | field := rv.Field(i) 22 | sf := rt.Field(i) 23 | var key string 24 | if nameInTag, ok := sf.Tag.Lookup(`form`); ok { 25 | key = nameInTag 26 | } else { 27 | key = sf.Name 28 | } 29 | data.Add(key, fmt.Sprint(field.Interface())) 30 | } 31 | 32 | case reflect.Map: 33 | for _, key := range rv.MapKeys() { 34 | mapValue := rv.MapIndex(key) 35 | switch mapValue.Kind() { 36 | case reflect.Slice: 37 | for i := 0; i < mapValue.Len(); i++ { 38 | data.Add(fmt.Sprint(key), fmt.Sprint(mapValue.Index(i).Interface())) 39 | } 40 | 41 | default: 42 | data.Add(fmt.Sprint(key), fmt.Sprint(mapValue.Interface())) 43 | } 44 | } 45 | 46 | case reflect.Ptr: 47 | for k, vs := range toURLValues(rv.Elem().Interface()) { 48 | for _, v := range vs { 49 | data.Add(k, v) 50 | } 51 | } 52 | 53 | } 54 | 55 | return data 56 | } 57 | -------------------------------------------------------------------------------- /pp/PP_test.go: -------------------------------------------------------------------------------- 1 | package pp 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "path/filepath" 7 | "runtime" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestFPP(t *testing.T) { 13 | buf := &bytes.Buffer{} 14 | v1 := time.Date(2022, time.July, 26, 17, 36, 19, 882377000, time.UTC) 15 | v2 := "foo" 16 | n, err := FPP(buf, v1, v2) 17 | if err != nil { 18 | t.Fatal(err.Error()) 19 | } 20 | 21 | exp := "time.Date(2022, time.July, 26, 17, 36, 19, 882377000, time.UTC)\t\"foo\"\n" 22 | if len([]byte(exp)) != n { 23 | t.Fatal("not everything was written out") 24 | } 25 | 26 | act := buf.String() 27 | 28 | assertEqual(t, exp, act) 29 | } 30 | 31 | func TestPP(t *testing.T) { 32 | buf := stubDefaultWriter(t) 33 | 34 | v1 := time.Date(2022, time.July, 26, 17, 36, 19, 882377000, time.UTC) 35 | v2 := "bar" 36 | 37 | _, file, line, _ := runtime.Caller(0) 38 | PP(v1, v2) 39 | 40 | exp := fmt.Sprintf("%s:%d time.Date(2022, time.July, 26, 17, 36, 19, 882377000, time.UTC)\t\"bar\"\n", 41 | filepath.Base(file), line+1) 42 | act := buf.String() 43 | 44 | assertEqual(t, exp, act) 45 | } 46 | 47 | func stubDefaultWriter(tb testing.TB) *bytes.Buffer { 48 | ogw := defaultWriter 49 | tb.Cleanup(func() { defaultWriter = ogw }) 50 | buf := &bytes.Buffer{} 51 | defaultWriter = buf 52 | return buf 53 | } 54 | -------------------------------------------------------------------------------- /random/internal/fixture/assets/contacts/lastnames.txt: -------------------------------------------------------------------------------- 1 | Adams 2 | Allen 3 | Alvarez 4 | Anderson 5 | Bailey 6 | Baker 7 | Bennet 8 | Brooks 9 | Brown 10 | Campbell 11 | Carter 12 | Castillo 13 | Chavez 14 | Clark 15 | Collins 16 | Cook 17 | Cooper 18 | Cox 19 | Cruz 20 | Davis 21 | Diaz 22 | Edwards 23 | Evans 24 | Flores 25 | Foster 26 | Garcia 27 | Gomez 28 | Gonzales 29 | Gonzalez 30 | Gray 31 | Green 32 | Gutierrez 33 | Hall 34 | Harris 35 | Hernandez 36 | Hill 37 | Howard 38 | Hughes 39 | Jackson 40 | James 41 | Jimenez 42 | Johnson 43 | Jones 44 | Kelly 45 | Kim 46 | King 47 | Lee 48 | Lewis 49 | Long 50 | Lopez 51 | Martin 52 | Martinez 53 | Mendoza 54 | Miller 55 | Mitchell 56 | Moore 57 | Morales 58 | Morgan 59 | Morris 60 | Murphy 61 | Myers 62 | Nelson 63 | Nguyen 64 | Ortiz 65 | Parker 66 | Patel 67 | Perez 68 | Peterson 69 | Phillips 70 | Price 71 | Ramirez 72 | Ramos 73 | Reed 74 | Reyes 75 | Richardson 76 | Rivera 77 | Roberts 78 | Robinson 79 | Rodriguez 80 | Rogers 81 | Ross 82 | Ruiz 83 | Sanchez 84 | Sanders 85 | Scott 86 | Smith 87 | Stewart 88 | Taylor 89 | Thomas 90 | Thompson 91 | Torres 92 | Turner 93 | Walker 94 | Ward 95 | Watson 96 | White 97 | Williams 98 | Wilson 99 | Wood 100 | Wright 101 | Young 102 | -------------------------------------------------------------------------------- /clock/internal/notify.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | var handlers = make(map[int]chan<- TimeTravelEvent) 8 | 9 | type TimeTravelEvent struct { 10 | Deep bool 11 | Freeze bool 12 | When time.Time 13 | Prev time.Time 14 | } 15 | 16 | func Notify(c chan<- TimeTravelEvent) func() { 17 | if c == nil { 18 | panic("clock: Notify using nil channel") 19 | } 20 | defer lock()() 21 | var index int 22 | for i := 0; true; i++ { 23 | if _, ok := handlers[i]; !ok { 24 | index = i 25 | break 26 | } 27 | } 28 | handlers[index] = c 29 | return func() { 30 | defer lock()() 31 | delete(handlers, index) 32 | } 33 | } 34 | 35 | func Check() (TimeTravelEvent, bool) { 36 | defer rlock()() 37 | return lookupTimeTravelEvent() 38 | } 39 | 40 | func lookupTimeTravelEvent() (TimeTravelEvent, bool) { 41 | return TimeTravelEvent{ 42 | Deep: chrono.Timeline.Deep, 43 | Freeze: chrono.Timeline.Frozen, 44 | When: chrono.Timeline.When, 45 | Prev: chrono.Timeline.Prev, 46 | }, !chrono.Timeline.IsZero() 47 | } 48 | 49 | func notify() { 50 | defer rlock()() 51 | tt, _ := lookupTimeTravelEvent() 52 | var publish = func(channel chan<- TimeTravelEvent) { 53 | defer recover() 54 | channel <- tt 55 | } 56 | for _, ch := range handlers { 57 | go publish(ch) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /random/internal/fixture/assets/contacts/femalenames.txt: -------------------------------------------------------------------------------- 1 | Abigail 2 | Alexis 3 | Alice 4 | Amanda 5 | Amber 6 | Amy 7 | Andrea 8 | Angela 9 | Ann 10 | Anna 11 | Ashley 12 | Barbara 13 | Betty 14 | Beverly 15 | Brenda 16 | Brittany 17 | Carol 18 | Carolyn 19 | Catherine 20 | Charlotte 21 | Cheryl 22 | Christina 23 | Christine 24 | Cynthia 25 | Danielle 26 | Deborah 27 | Debra 28 | Denise 29 | Diana 30 | Diane 31 | Donna 32 | Doris 33 | Dorothy 34 | Elizabeth 35 | Emily 36 | Emma 37 | Evelyn 38 | Frances 39 | Gloria 40 | Grace 41 | Hannah 42 | Heather 43 | Helen 44 | Isabella 45 | Jacqueline 46 | Janet 47 | Janice 48 | Jean 49 | Jennifer 50 | Jessica 51 | Joan 52 | Joyce 53 | Judith 54 | Judy 55 | Julia 56 | Julie 57 | Karen 58 | Katherine 59 | Kathleen 60 | Kathryn 61 | Kayla 62 | Kelly 63 | Kimberly 64 | Laura 65 | Lauren 66 | Linda 67 | Lisa 68 | Lori 69 | Madison 70 | Margaret 71 | Maria 72 | Marie 73 | Marilyn 74 | Martha 75 | Mary 76 | Megan 77 | Melissa 78 | Michelle 79 | Nancy 80 | Natalie 81 | Nicole 82 | Olivia 83 | Pamela 84 | Patricia 85 | Rachel 86 | Rebecca 87 | Ruth 88 | Samantha 89 | Sandra 90 | Sara 91 | Sarah 92 | Sharon 93 | Shirley 94 | Sophia 95 | Stephanie 96 | Susan 97 | Teresa 98 | Theresa 99 | Victoria 100 | Virginia 101 | -------------------------------------------------------------------------------- /docs/examples/spechelper_sharedResource_test.go: -------------------------------------------------------------------------------- 1 | // package spechelper 2 | package examples_test 3 | 4 | import ( 5 | "context" 6 | "os" 7 | "sync" 8 | "testing" 9 | 10 | "go.llib.dev/testcase" 11 | "go.llib.dev/testcase/assert" 12 | "go.llib.dev/testcase/internal/example/mydomain" 13 | "go.llib.dev/testcase/internal/example/someextres" 14 | ) 15 | 16 | var ( 17 | sharedGlobalStorageInstanceInit sync.Once 18 | sharedGlobalStorageInstance mydomain.Storage // role interface type 19 | ) 20 | 21 | func getSharedGlobalStorageInstance(tb testing.TB) mydomain.Storage { 22 | sharedGlobalStorageInstanceInit.Do(func() { 23 | storage, err := someextres.NewStorage(os.Getenv(`TEST_DATABASE_URL`)) 24 | assert.Must(tb).Nil(err) 25 | sharedGlobalStorageInstance = storage 26 | }) 27 | return sharedGlobalStorageInstance 28 | } 29 | 30 | var Context = testcase.Var[context.Context]{ 31 | ID: `context`, 32 | Init: func(t *testcase.T) context.Context { 33 | return context.Background() 34 | }, 35 | } 36 | 37 | var Storage = testcase.Var[mydomain.Storage]{ 38 | ID: `Storage`, 39 | Init: func(t *testcase.T) mydomain.Storage { 40 | s := getSharedGlobalStorageInstance(t) 41 | tx, err := s.BeginTx(Context.Get(t)) 42 | t.Must.Nil(err) 43 | Context.Set(t, tx) 44 | t.Defer(s.RollbackTx, tx) // teardown 45 | return s 46 | }, 47 | } 48 | -------------------------------------------------------------------------------- /faultinject/Enabled.go: -------------------------------------------------------------------------------- 1 | package faultinject 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "sync" 8 | ) 9 | 10 | func init() { initEnabled() } 11 | 12 | func initEnabled() { 13 | state.Enabled = false 14 | const envKey = "TESTCASE_FAULTINJECT" 15 | v, ok := os.LookupEnv(envKey) 16 | if !ok { 17 | return 18 | } 19 | enabled, err := strconv.ParseBool(v) 20 | if err != nil { 21 | _, _ = fmt.Fprintf(os.Stderr, "%s: %s", envKey, err.Error()) 22 | return 23 | } 24 | state.Enabled = enabled 25 | } 26 | 27 | var state struct { 28 | Mutex sync.RWMutex 29 | Counter int 30 | Enabled bool 31 | Original bool 32 | } 33 | 34 | func Enabled() bool { 35 | state.Mutex.RLock() 36 | defer state.Mutex.RUnlock() 37 | return state.Enabled 38 | } 39 | 40 | func Enable() (Disable func()) { 41 | state.Mutex.Lock() 42 | defer state.Mutex.Unlock() 43 | 44 | if state.Counter == 0 { 45 | state.Original = state.Enabled 46 | } 47 | 48 | state.Counter++ 49 | state.Enabled = true 50 | 51 | return func() { 52 | state.Mutex.Lock() 53 | defer state.Mutex.Unlock() 54 | state.Counter-- 55 | if state.Counter == 0 { 56 | state.Enabled = state.Original 57 | } 58 | } 59 | } 60 | 61 | type testingTB interface { 62 | Cleanup(func()) 63 | } 64 | 65 | func EnableForTest(tb testingTB) { 66 | tb.Cleanup(Enable()) 67 | } 68 | -------------------------------------------------------------------------------- /docs/examples/header/main.go: -------------------------------------------------------------------------------- 1 | package header 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "database/sql/driver" 7 | ) 8 | 9 | // package mystorage 10 | 11 | // sqlDB is the header interface to *sql.DB 12 | type sqlDB interface { 13 | Query(query string, args ...interface{}) (*sql.Rows, error) 14 | QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) 15 | QueryRow(query string, args ...interface{}) *sql.Row 16 | QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row 17 | Exec(query string, args ...interface{}) (sql.Result, error) 18 | ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) 19 | Begin() (*sql.Tx, error) 20 | BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error) 21 | Ping() error 22 | Driver() driver.Driver 23 | Close() error 24 | Conn(ctx context.Context) (*sql.Conn, error) 25 | PingContext(ctx context.Context) error 26 | Prepare(query string) (*sql.Stmt, error) 27 | PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) 28 | // ... and so on 29 | } 30 | 31 | type Supplier struct { 32 | db sqlDB 33 | } 34 | 35 | func (m Supplier) Count() error { 36 | var count int 37 | if err := m.db.QueryRow(`SELECT 1`).Scan(&count); err != nil { 38 | return err 39 | } 40 | 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /seed.go: -------------------------------------------------------------------------------- 1 | package testcase 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "os" 7 | "strconv" 8 | "testing" 9 | "time" 10 | 11 | "go.llib.dev/testcase/random" 12 | 13 | "go.llib.dev/testcase/internal" 14 | "go.llib.dev/testcase/internal/environ" 15 | ) 16 | 17 | func makeSeed() (int64, error) { 18 | rawSeed, injectedRandomSeedIsSet := os.LookupEnv(environ.KeySeed) 19 | if !injectedRandomSeedIsSet { 20 | salt := rand.New(random.CryptoSeed{}).Int63() 21 | base := time.Now().UnixNano() 22 | return base + salt, nil 23 | } 24 | seed, err := strconv.ParseInt(rawSeed, 10, 64) 25 | if err != nil { 26 | return 0, fmt.Errorf("%s has invalid seed integer value: %s", environ.KeySeed, rawSeed) 27 | } 28 | return seed, nil 29 | } 30 | 31 | func seedForSpec(tb testing.TB) (_seed int64) { 32 | helper(tb).Helper() 33 | if isValidTestingTB(tb) { 34 | tb.Cleanup(func() { 35 | tb.Helper() 36 | if tb.Failed() { 37 | // Help developers to know the seed of the failed test execution. 38 | internal.Log(tb, fmt.Sprintf(`%s=%d`, environ.KeySeed, _seed)) 39 | } 40 | }) 41 | } 42 | seed, err := makeSeed() 43 | if err != nil { 44 | tb.Fatal(err.Error()) 45 | } 46 | return seed 47 | } 48 | 49 | func isValidTestingTB(tb testing.TB) bool { 50 | if tb == nil { 51 | return false 52 | } 53 | _, ok := tb.(internal.NullTB) 54 | return !ok 55 | } 56 | -------------------------------------------------------------------------------- /random/charset.go: -------------------------------------------------------------------------------- 1 | package random 2 | 3 | var ( 4 | charsetAlpha string 5 | charsetASCII string 6 | charset string 7 | ) 8 | 9 | const charsetDigit = "0123456789" 10 | 11 | func Charset() string { return charset } 12 | func CharsetAlpha() string { return charsetAlpha } 13 | func CharsetDigit() string { return charsetDigit } 14 | func CharsetASCII() string { return charsetASCII } 15 | 16 | func init() { 17 | initCharset() 18 | initCharsetAlpha() 19 | initCharsetASCII() 20 | } 21 | 22 | func initCharsetAlpha() { 23 | for ch := 'a'; ch <= 'z'; ch++ { 24 | charsetAlpha += string(ch) 25 | } 26 | for ch := 'A'; ch <= 'Z'; ch++ { 27 | charsetAlpha += string(ch) 28 | } 29 | } 30 | 31 | func initCharsetASCII() { 32 | for i := 0; i <= 255; i++ { 33 | charsetASCII += string(rune(i)) 34 | } 35 | } 36 | 37 | func initCharset() { 38 | type CharsetRange struct { 39 | Start int 40 | End int 41 | } 42 | for _, r := range []CharsetRange{ 43 | { 44 | Start: 33, 45 | End: 126, 46 | }, 47 | { 48 | Start: 161, 49 | End: 767, 50 | }, 51 | { 52 | Start: 880, 53 | End: 1159, 54 | }, 55 | { 56 | Start: 1162, 57 | End: 1364, 58 | }, 59 | { 60 | Start: 1567, 61 | End: 1610, 62 | }, 63 | { 64 | Start: 1634, 65 | End: 1747, 66 | }, 67 | } { 68 | for i := r.Start; i <= r.End; i++ { 69 | charset += string(rune(i)) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Race.go: -------------------------------------------------------------------------------- 1 | package testcase 2 | 3 | import ( 4 | "runtime" 5 | "sync" 6 | "sync/atomic" 7 | ) 8 | 9 | // Race is a test helper that allows you to create a race situation easily. 10 | // Race will execute each provided anonymous lambda function in a different goroutine, 11 | // and make sure they are scheduled at the same time. 12 | // 13 | // This is useful when you work on a component that requires thread-safety. 14 | // By using the Race helper, you can write an example use of your component, 15 | // and run the testing suite with `go test -race`. 16 | // The race detector then should be able to notice issues with your implementation. 17 | func Race(fn1, fn2 func(), more ...func()) { 18 | fns := append([]func(){fn1, fn2}, more...) 19 | var ( 20 | start sync.WaitGroup 21 | rdy sync.WaitGroup 22 | wg sync.WaitGroup 23 | ) 24 | start.Add(1) // get ready for the race 25 | wg.Add(len(fns)) 26 | rdy.Add(len(fns)) 27 | var total int32 28 | for _, fn := range fns { 29 | go func(blk func()) { 30 | defer wg.Done() 31 | rdy.Done() // signal that participant is ready 32 | start.Wait() // line up participants 33 | blk() 34 | atomic.AddInt32(&total, 1) 35 | }(fn) 36 | } 37 | runtime.Gosched() 38 | rdy.Wait() // wait until everyone lined up 39 | start.Done() // start the race 40 | wg.Wait() // wait members to finish 41 | if total != int32(len(fns)) { 42 | runtime.Goexit() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /docs/examples/immutableAct_test.go: -------------------------------------------------------------------------------- 1 | package examples_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "go.llib.dev/testcase" 8 | "go.llib.dev/testcase/docs/examples" 9 | ) 10 | 11 | func TestImmutableAct(t *testing.T) { 12 | s := testcase.NewSpec(t) 13 | 14 | myStruct := testcase.Let(s, func(t *testcase.T) *examples.MyStruct { 15 | return &examples.MyStruct{} 16 | }) 17 | 18 | s.Describe(`#Shrug`, func(s *testcase.Spec) { 19 | const shrugEmoji = `¯\_(ツ)_/¯` 20 | var ( 21 | message = testcase.Let(s, func(t *testcase.T) string { return t.Random.String() }) 22 | subject = func(t *testcase.T) string { 23 | return myStruct.Get(t).Shrug(message.Get(t)) 24 | } 25 | ) 26 | 27 | s.When(`message doesn't have shrug in the ending`, func(s *testcase.Spec) { 28 | s.Before(func(t *testcase.T) { 29 | t.Must.Contains(subject(t), shrugEmoji) 30 | }) 31 | 32 | s.Then(`it will append shrug emoji to this`, func(t *testcase.T) { 33 | t.Must.True(strings.HasSuffix(subject(t), shrugEmoji)) 34 | }) 35 | }) 36 | 37 | s.When(`shrug part of the input message`, func(s *testcase.Spec) { 38 | message.Let(s, func(t *testcase.T) string { 39 | return t.Random.String() + shrugEmoji 40 | }) 41 | 42 | s.Then(`it will not append any more shrug emoji to the end of the message`, func(t *testcase.T) { 43 | t.Must.Equal(message.Get(t), subject(t)) 44 | }) 45 | }) 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /clock/examples_test.go: -------------------------------------------------------------------------------- 1 | package clock_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "go.llib.dev/testcase/assert" 8 | "go.llib.dev/testcase/clock" 9 | "go.llib.dev/testcase/clock/timecop" 10 | ) 11 | 12 | func ExampleNow_freeze() { 13 | var tb testing.TB 14 | 15 | type Entity struct { 16 | CreatedAt time.Time 17 | } 18 | 19 | MyFunc := func() Entity { 20 | return Entity{ 21 | CreatedAt: clock.Now(), 22 | } 23 | } 24 | 25 | expected := Entity{ 26 | CreatedAt: clock.Now(), 27 | } 28 | 29 | timecop.Travel(tb, expected.CreatedAt, timecop.Freeze) 30 | 31 | assert.Equal(tb, expected, MyFunc()) 32 | } 33 | 34 | func ExampleNow_withTravelByDuration() { 35 | var tb testing.TB 36 | 37 | _ = clock.Now() // now 38 | timecop.Travel(tb, time.Hour) 39 | _ = clock.Now() // now + 1 hour 40 | } 41 | 42 | func ExampleNow_withTravelByDate() { 43 | var tb testing.TB 44 | 45 | date := time.Date(2022, 01, 01, 12, 0, 0, 0, time.Local) 46 | timecop.Travel(tb, date, timecop.Freeze) // freeze the time until it is read 47 | time.Sleep(time.Second) 48 | _ = clock.Now() // equals with date 49 | } 50 | 51 | func ExampleAfter() { 52 | var tb testing.TB 53 | timecop.SetSpeed(tb, 5) // 5x time speed 54 | <-clock.After(time.Second) // but only wait 1/5 of the time 55 | } 56 | 57 | func ExampleSleep() { 58 | var tb testing.TB 59 | timecop.SetSpeed(tb, 5) // 5x time speed 60 | clock.Sleep(time.Second) // but only sleeps 1/5 of the time 61 | } 62 | -------------------------------------------------------------------------------- /scripts/test-output: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | ( 5 | type go 6 | type example-output 7 | ) 1>/dev/null 8 | 9 | function main() { 10 | testEmptyName 11 | 12 | example-output \ 13 | | extractTestNames \ 14 | | testTestNames 15 | } 16 | 17 | function extractTestNames() { 18 | local line 19 | while read -r line; do 20 | if [[ ${line} =~ RUN\ +([^\ ]+) ]]; then 21 | echo "${BASH_REMATCH[1]}" 22 | fi 23 | done 24 | } 25 | 26 | function testTestNames() { 27 | local cmd name failed 28 | while read -r name; do 29 | cmd="go test -v -run ${name}" 30 | if ! eval "${cmd}" | grep --quiet --fixed-strings --regexp "PASS: ${name}"; then 31 | failed="TRUE" 32 | echo "FAIL: ${cmd}" 33 | else 34 | echo "PASS: ${cmd}" 35 | fi 36 | done 37 | [[ -z ${failed:-} ]] 38 | } 39 | 40 | function testEmptyName() ( 41 | if [[ -n ${WDP:-} ]]; then 42 | cd "${WDP}" 43 | fi 44 | assert() { 45 | local testName=${1} expected=${2} 46 | cmd="go test -v -run ${testName} ./internal/fixtures" 47 | output=$(${cmd}) 48 | if [[ ${output} == *"${expected}"* ]] && [[ ! ${output} =~ no\ tests\ to\ run ]]; then 49 | echo "PASS: ${cmd}" 50 | else 51 | failed="TRUE" 52 | echo "FAIL: ${cmd}" 53 | fi 54 | [[ -z ${failed:-} ]] 55 | } 56 | 57 | assert "TestFixtureOutput/test_output_test.go:15" "foo" 58 | assert "TestFixtureOutput/test_output_test.go:16" "bar" 59 | assert "TestFixtureOutput/test_output_test.go:17" "baz" 60 | ) 61 | 62 | main 63 | -------------------------------------------------------------------------------- /internal/env/env.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import "os" 4 | 5 | type TB interface { 6 | Helper() 7 | Cleanup(func()) 8 | Fatal(...any) 9 | Error(...any) 10 | } 11 | 12 | // SetEnv will set the os environment variable for the current program to a given value, 13 | // and prepares a cleanup function to restore the original state of the environment variable. 14 | // 15 | // Spec using this helper should be flagged with Spec.HasSideEffect or Spec.Sequential. 16 | func SetEnv(tb TB, key, value string) { 17 | tb.Helper() 18 | cleanupEnv(tb, key) 19 | 20 | if err := os.Setenv(key, value); err != nil { 21 | tb.Fatal(err) 22 | } 23 | } 24 | 25 | // UnsetEnv will unset the os environment variable value for the current program, 26 | // and prepares a cleanup function to restore the original state of the environment variable. 27 | // 28 | // Spec using this helper should be flagged with Spec.HasSideEffect or Spec.Sequential. 29 | func UnsetEnv(tb TB, key string) { 30 | tb.Helper() 31 | cleanupEnv(tb, key) 32 | 33 | if err := os.Unsetenv(key); err != nil { 34 | tb.Fatal(err) 35 | } 36 | } 37 | 38 | func cleanupEnv(tb TB, key string) { 39 | tb.Helper() 40 | var restore func() error 41 | if originalValue, ok := os.LookupEnv(key); ok { 42 | restore = func() error { return os.Setenv(key, originalValue) } 43 | } else { 44 | restore = func() error { return os.Unsetenv(key) } 45 | } 46 | tb.Cleanup(func() { 47 | if err := restore(); err != nil { 48 | tb.Error(err) 49 | } 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /clock/spike_test.go: -------------------------------------------------------------------------------- 1 | //go:build spike 2 | 3 | package clock_test 4 | 5 | import ( 6 | "testing" 7 | "time" 8 | 9 | "go.llib.dev/testcase/clock" 10 | "go.llib.dev/testcase/clock/timecop" 11 | ) 12 | 13 | func Test_spike_timeTicker(t *testing.T) { 14 | ticker := time.NewTicker(time.Second / 4) 15 | 16 | done := make(chan struct{}) 17 | defer close(done) 18 | go func() { 19 | for { 20 | select { 21 | case <-ticker.C: 22 | t.Log("ticked") 23 | case <-done: 24 | return 25 | } 26 | } 27 | }() 28 | 29 | t.Log("4 expected on sleep") 30 | time.Sleep(time.Second + time.Microsecond) 31 | 32 | t.Log("now we reset and we expect 8 tick on sleep") 33 | ticker.Reset(time.Second / 8) 34 | time.Sleep(time.Second + time.Microsecond) 35 | 36 | } 37 | 38 | func Test_spike_clockTicker(t *testing.T) { 39 | ticker := clock.NewTicker(time.Second / 4) 40 | 41 | done := make(chan struct{}) 42 | defer close(done) 43 | go func() { 44 | for { 45 | select { 46 | case <-ticker.C: 47 | t.Log("ticked") 48 | case <-done: 49 | return 50 | } 51 | } 52 | }() 53 | 54 | t.Log("4 expected on sleep") 55 | time.Sleep(time.Second + time.Microsecond) 56 | 57 | t.Log("now we reset and we expect 8 tick on sleep") 58 | ticker.Reset(time.Second / 8) 59 | time.Sleep(time.Second + time.Microsecond) 60 | 61 | t.Log("now time sped up, and where 4 would be expected on the following sleep, it should be 8") 62 | timecop.SetSpeed(t, 2) 63 | time.Sleep(time.Second + time.Microsecond) 64 | 65 | } 66 | -------------------------------------------------------------------------------- /deprecated.go: -------------------------------------------------------------------------------- 1 | package testcase 2 | 3 | import "go.llib.dev/testcase/assert" 4 | 5 | // Let is a method to provide backward compatibility with the existing testing suite. 6 | // Due to how Go type parameters work, methods are not allowed to have type parameters, 7 | // thus Let has moved to be a pkg-level function in the package. 8 | // 9 | // Deprecated: use testcase.Let instead testcase#Spec.Let. 10 | func (spec *Spec) Let(varName VarID, blk VarInit[any]) Var[any] { 11 | return let[any](spec, varName, blk) 12 | } 13 | 14 | // LetValue is a method to provide backward compatibility with the existing testing suite. 15 | // Due to how Go type parameters work, methods are not allowed to have type parameters, 16 | // thus LetValue has moved to be a pkg-level function in the package. 17 | // 18 | // Deprecated: use testcase.LetValue instead testcase#Spec.LetValue. 19 | func (spec *Spec) LetValue(varName VarID, value any) Var[any] { 20 | return letValue[any](spec, varName, value) 21 | } 22 | 23 | // VarInitFunc is a backward compatibility type for VarInit. 24 | // 25 | // Deprecated: use VarInit type instead. 26 | type VarInitFunc[V any] VarInit[T] 27 | 28 | // RetryStrategyForEventually 29 | // 30 | // Deprecated: use testcase.WithRetryStrategy instead 31 | func RetryStrategyForEventually(strategy assert.Loop) SpecOption { 32 | return WithRetryStrategy(strategy) 33 | } 34 | 35 | // StubTB 36 | // 37 | // Deprecated: use FakeTB, the naming was off as it was not a stub but a fully fleshed out fake with contracts and all 38 | type StubTB = FakeTB 39 | -------------------------------------------------------------------------------- /sandbox/Run.go: -------------------------------------------------------------------------------- 1 | package sandbox 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "runtime" 7 | "sync" 8 | 9 | "go.llib.dev/testcase/internal/caller" 10 | ) 11 | 12 | func Run(fn func()) (ro RunOutcome) { 13 | var wg sync.WaitGroup 14 | wg.Add(1) 15 | go func() { 16 | defer wg.Done() 17 | defer func() { ro.PanicValue = recover() }() 18 | defer func() { ro.Goexit = stackHasGoexit() }() 19 | defer func() { 20 | if !ro.OK { 21 | ro.Frames = getFrames() 22 | } 23 | }() 24 | fn() 25 | ro.OK = true 26 | }() 27 | wg.Wait() 28 | return 29 | } 30 | 31 | type RunOutcome struct { 32 | OK bool 33 | PanicValue any 34 | Goexit bool 35 | Frames []runtime.Frame 36 | } 37 | 38 | func (ro RunOutcome) Trace() string { 39 | var buf bytes.Buffer 40 | switch { 41 | case ro.Goexit: 42 | _, _ = buf.Write([]byte("runtime.Goexit")) 43 | case !ro.OK: 44 | _, _ = fmt.Fprintf(&buf, "panic: %v", ro.PanicValue) 45 | } 46 | _, _ = buf.Write([]byte("\n")) 47 | for _, frame := range ro.Frames { 48 | _, _ = fmt.Fprintf(&buf, "%s\n\t%s:%d %#v\n", frame.Function, frame.File, frame.Line, frame.PC) 49 | } 50 | return buf.String() 51 | } 52 | 53 | // OnNotOK will execute the argument block when the OK state is false. 54 | func (ro RunOutcome) OnNotOK(blk func()) { 55 | if ro.OK { 56 | return 57 | } 58 | if blk == nil { 59 | return 60 | } 61 | blk() 62 | } 63 | 64 | func stackHasGoexit() bool { 65 | const goexitFuncName = "runtime.Goexit" 66 | return caller.Until(func(frame runtime.Frame) bool { 67 | return frame.Function == goexitFuncName 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /httpspec/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | - [httpspec](#httpspec) 5 | - [Documentation](#documentation) 6 | - [Usage](#usage) 7 | 8 | 9 | 10 | # httpspec 11 | 12 | httpspec allow you to create HTTP API specifications with ease. 13 | 14 | ## [Documentation](https://godoc.org/go.llib.dev/testcase/httpspec) 15 | 16 | The documentation maintained in [GoDoc](https://godoc.org/go.llib.dev/testcase/httpspec), including the [examples](https://godoc.org/go.llib.dev/testcase/httpspec#pkg-examples). 17 | 18 | ## Usage 19 | 20 | ```go 21 | package mypkg 22 | 23 | func TestMyHandlerCreate(t *testing.T) { 24 | s := testcase.NewSpec(t) 25 | 26 | // subject 27 | httpspec.SubjectLet(s, func(t *testcase.T) http.Handler { 28 | return MyHandler{} 29 | }) 30 | 31 | // Arrange 32 | httpspec.ContentTypeIsJSON(s) 33 | httpspec.Method.LetValue(s, http.MethodPost) 34 | httpspec.Path.LetValue(s, `/`) 35 | httpspec.Body.Let(s, func(t *testcase.T) interface{} { 36 | // this will end up as {"foo":"bar"} in the request body 37 | return map[string]string{"foo": "bar"} 38 | }) 39 | 40 | s.Then(`it will...`, func(t *testcase.T) { 41 | // Act 42 | rr := httpspec.SubjectGet(t) 43 | 44 | // Assert 45 | assert.Must(t).Equal( http.StatusOK, rr.Code) 46 | var resp CreateResponse 47 | assert.Must(t).Nil( json.Unmarshal(rr.Body.Bytes(), &resp)) 48 | // ... 49 | }) 50 | } 51 | ``` 52 | -------------------------------------------------------------------------------- /spec_helper_test.go: -------------------------------------------------------------------------------- 1 | package testcase_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "go.llib.dev/testcase/internal/doubles" 8 | "go.llib.dev/testcase/sandbox" 9 | 10 | "go.llib.dev/testcase/assert" 11 | ) 12 | 13 | type CustomTB struct { 14 | testing.TB 15 | isFatalFCalled bool 16 | } 17 | 18 | func (tb *CustomTB) Run(name string, blk func(tb testing.TB)) bool { 19 | switch tb := tb.TB.(type) { 20 | case *testing.T: 21 | return tb.Run(name, func(t *testing.T) { blk(t) }) 22 | case *testing.B: 23 | return tb.Run(name, func(b *testing.B) { blk(b) }) 24 | default: 25 | panic("implement me") 26 | } 27 | } 28 | 29 | func (t *CustomTB) Fatalf(format string, args ...interface{}) { 30 | t.isFatalFCalled = true 31 | return 32 | } 33 | 34 | func unsupported(tb testing.TB) { 35 | tb.Helper() 36 | tb.Skip(`unsupported`) 37 | } 38 | 39 | func isFatalFn(stub *doubles.TB) func(block func()) bool { 40 | return func(block func()) bool { 41 | stub.IsFailed = false 42 | defer func() { stub.IsFailed = false }() 43 | 44 | var finished bool 45 | sandbox.Run(func() { 46 | block() 47 | finished = true 48 | }) 49 | 50 | ltb, ok := stub.LastRunTB() 51 | if !ok { 52 | ltb = stub 53 | } 54 | 55 | return !finished && (ltb.Failed() || stub.Failed()) 56 | } 57 | } 58 | 59 | func willFatalWithMessageFn(stub *doubles.TB) func(tb testing.TB, blk func()) string { 60 | isFatal := isFatalFn(stub) 61 | return func(tb testing.TB, blk func()) string { 62 | stub.Logs = bytes.Buffer{} 63 | assert.Must(tb).True(isFatal(blk)) 64 | return stub.Logs.String() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /faultinject/Enabled_internal_test.go: -------------------------------------------------------------------------------- 1 | package faultinject 2 | 3 | import ( 4 | "testing" 5 | 6 | "go.llib.dev/testcase" 7 | ) 8 | 9 | func TestInitEnabled(t *testing.T) { 10 | t.Cleanup(func() { state.Enabled = false }) 11 | const envKey = "TESTCASE_FAULTINJECT" 12 | 13 | s := testcase.NewSpec(t) 14 | 15 | act := func(t *testcase.T) { 16 | initEnabled() 17 | } 18 | 19 | s.Before(func(t *testcase.T) { // clean ahead 20 | testcase.UnsetEnv(t, envKey) 21 | state.Enabled = false 22 | }) 23 | 24 | s.When("no env var is not set", func(s *testcase.Spec) { 25 | s.Before(func(t *testcase.T) { 26 | testcase.UnsetEnv(t, envKey) 27 | }) 28 | 29 | s.Then("the default strategy is to set state to false", func(t *testcase.T) { 30 | act(t) 31 | 32 | t.Must.False(state.Enabled) 33 | }) 34 | }) 35 | 36 | s.When("env var is set to TRUE/ON", func(s *testcase.Spec) { 37 | s.Before(func(t *testcase.T) { 38 | testcase.SetEnv(t, envKey, t.Random.Pick([]string{ 39 | "TRUE", 40 | "true", 41 | "1", 42 | }).(string)) 43 | }) 44 | 45 | s.Then("state is set to true", func(t *testcase.T) { 46 | act(t) 47 | 48 | t.Must.True(state.Enabled) 49 | }) 50 | }) 51 | 52 | s.When("env var is set to FALSE/OFF", func(s *testcase.Spec) { 53 | s.Before(func(t *testcase.T) { 54 | state.Enabled = true 55 | testcase.SetEnv(t, envKey, t.Random.Pick([]string{ 56 | "FALSE", 57 | "false", 58 | "0", 59 | }).(string)) 60 | }) 61 | 62 | s.Then("state is set to false", func(t *testcase.T) { 63 | act(t) 64 | 65 | t.Must.False(state.Enabled) 66 | }) 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /httpspec/body.go: -------------------------------------------------------------------------------- 1 | package httpspec 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "strconv" 10 | 11 | "go.llib.dev/testcase" 12 | ) 13 | 14 | var Body = testcase.Var[any]{ID: `httpspec:Body`, Init: func(t *testcase.T) any { 15 | return &bytes.Buffer{} 16 | }} 17 | 18 | func asIOReader(t *testcase.T, header http.Header, body any) (bodyValue io.ReadCloser) { 19 | defer func() { 20 | if !isDebugEnabled(t) { 21 | return 22 | } 23 | 24 | var buf bytes.Buffer 25 | _, err := io.Copy(&buf, bodyValue) 26 | if err != nil { 27 | t.Fatalf(`httpspec body debug print encountered an error: %v`, err.Error()) 28 | } 29 | 30 | t.Log(`body:`) 31 | t.Log(buf.String()) 32 | 33 | bodyValue = io.NopCloser(bytes.NewReader(buf.Bytes())) 34 | }() 35 | if body == nil { 36 | body = bytes.NewReader([]byte{}) 37 | } 38 | if r, ok := body.(io.ReadCloser); ok { 39 | return r 40 | } 41 | if r, ok := body.(io.Reader); ok { 42 | return io.NopCloser(r) 43 | } 44 | 45 | var buf bytes.Buffer 46 | switch header.Get(`Content-Type`) { 47 | case `application/json`: 48 | if err := json.NewEncoder(&buf).Encode(body); err != nil { 49 | t.Fatalf(`httpspec request body creation encountered: %v`, err.Error()) 50 | } 51 | 52 | case `application/x-www-form-urlencoded`: 53 | _, _ = fmt.Fprint(&buf, toURLValues(body).Encode()) 54 | 55 | default: 56 | header.Set("Content-Type", "text/plain; charset=UTF-8") 57 | _, _ = fmt.Fprintf(&buf, "%v", body) 58 | 59 | } 60 | 61 | header.Add("Content-Length", strconv.Itoa(buf.Len())) 62 | 63 | return io.NopCloser(&buf) 64 | } 65 | -------------------------------------------------------------------------------- /internal/Log.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "runtime" 8 | "strings" 9 | ) 10 | 11 | func Log(logger interface { 12 | Logf(format string, args ...interface{}) 13 | }, args ...interface{}) { 14 | whiteSpace := strings.Repeat(` `, getWhitespaceCount()) 15 | message := fmt.Sprintln(append([]interface{}{"\n"}, args...)...) 16 | logger.Logf("\r%s%s", whiteSpace, indentMessageLines(message)) 17 | } 18 | 19 | // Aligns the provided message so that list lines after the first line Finish at the same location as the first line. 20 | // Assumes that the first line starts at the correct location (after carriage return, tab, label, spacer and tab). 21 | func indentMessageLines(message string) string { 22 | outBuf := new(bytes.Buffer) 23 | 24 | for i, scanner := 0, bufio.NewScanner(strings.NewReader(message)); scanner.Scan(); i++ { 25 | // no need to align first line because it starts at the correct location (after the label) 26 | if i != 0 { 27 | // append alignLen+1 spaces to align with "{{longestLabel}}:" before adding tab 28 | outBuf.WriteString("\n\t" + ` ` + "\t") 29 | } 30 | outBuf.WriteString(scanner.Text()) 31 | } 32 | 33 | return outBuf.String() 34 | } 35 | 36 | // I'm unable to get the windows width during the test runtime, 37 | // so I just make a guess that will work for 95% of the case. 38 | func getWhitespaceCount() int { 39 | _, file, line, ok := runtime.Caller(1) 40 | if !ok { 41 | return 0 42 | } 43 | 44 | parts := strings.Split(file, "/") 45 | file = parts[len(parts)-1] 46 | length := 3 * len(fmt.Sprintf("%s:%d: ", file, line)) 47 | return length 48 | } 49 | -------------------------------------------------------------------------------- /Spec_bc_test.go: -------------------------------------------------------------------------------- 1 | package testcase 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "go.llib.dev/testcase/assert" 8 | "go.llib.dev/testcase/internal/doubles" 9 | "go.llib.dev/testcase/sandbox" 10 | ) 11 | 12 | func TestSpec_FriendlyVarNotDefined(t *testing.T) { 13 | stub := &doubles.TB{} 14 | s := NewSpec(stub) 15 | willFatalWithMessage := willFatalWithMessageFn(stub) 16 | 17 | v1 := Let[string](s, func(t *T) string { return `hello-world` }) 18 | v2 := Let[string](s, func(t *T) string { return `hello-world` }) 19 | tct := NewTWithSpec(stub, s) 20 | 21 | s.Test(`var1 var found`, func(t *T) { 22 | assert.Must(t).Equal(`hello-world`, v1.Get(t)) 23 | }) 24 | 25 | t.Run(`not existing var will panic with friendly msg`, func(t *testing.T) { 26 | msg := willFatalWithMessage(t, func() { tct.vars.Get(tct, `not-exist`) }) 27 | assert.Must(t).Contains(msg.String(), `Variable "not-exist" is not found`) 28 | assert.Must(t).Contains(msg.String(), `Did you mean?`) 29 | assert.Must(t).Contains(msg.String(), v1.ID) 30 | assert.Must(t).Contains(msg.String(), v2.ID) 31 | }) 32 | } 33 | 34 | func isFatalFn(stub *doubles.TB) func(block func()) bool { 35 | return func(block func()) bool { 36 | stub.IsFailed = false 37 | defer func() { stub.IsFailed = false }() 38 | out := sandbox.Run(block) 39 | return !out.OK && stub.Failed() 40 | } 41 | } 42 | 43 | func willFatalWithMessageFn(stub *doubles.TB) func(tb testing.TB, blk func()) bytes.Buffer { 44 | isFatal := isFatalFn(stub) 45 | return func(tb testing.TB, blk func()) bytes.Buffer { 46 | stub.Logs = bytes.Buffer{} 47 | assert.Must(tb).True(isFatal(blk)) 48 | return stub.Logs 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /internal/fmterror/Message.go: -------------------------------------------------------------------------------- 1 | package fmterror 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "go.llib.dev/testcase/pp" 8 | ) 9 | 10 | type Message struct { 11 | Name string 12 | Cause string 13 | Message []any 14 | Values []Value 15 | } 16 | 17 | type Value struct { 18 | Label string 19 | Value any 20 | } 21 | 22 | type Formatted string 23 | 24 | func (m Message) String() string { 25 | var ( 26 | format string 27 | args []interface{} 28 | ) 29 | if m.Name != "" { 30 | format += "[%s] " 31 | args = append(args, m.Name) 32 | } 33 | if m.Cause != "" { 34 | format += "%s" 35 | args = append(args, m.Cause) 36 | } 37 | if 0 < len(m.Message) { 38 | format += "\n%s" 39 | args = append(args, strings.TrimSpace(fmt.Sprintln(m.Message...))) 40 | } 41 | for _, v := range m.Values { 42 | var value string 43 | if raw, ok := v.Value.(Formatted); ok { 44 | value = string(raw) 45 | } else { 46 | value = pp.Format(v.Value) 47 | } 48 | format += "\n%s:" 49 | if 0 < strings.Count(value, "\n") { 50 | format += "\n\n%s\n" 51 | } else { 52 | format += "\t%s" 53 | } 54 | args = append(args, m.rightAlign(v.Label), value) 55 | } 56 | return fmt.Sprintf(format, args...) 57 | } 58 | 59 | func (m Message) rightAlign(str string) string { 60 | if strLen := len(str); strLen < m.labelLength() { 61 | str = strings.Repeat(" ", m.labelLength()-strLen) + str 62 | } 63 | return str 64 | } 65 | 66 | func (m Message) labelLength() int { 67 | var maxLength int 68 | for _, v := range m.Values { 69 | if length := len(v.Label); maxLength < length { 70 | maxLength = length 71 | } 72 | } 73 | return maxLength 74 | } 75 | -------------------------------------------------------------------------------- /pp/default_test.go: -------------------------------------------------------------------------------- 1 | package pp 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "go.llib.dev/testcase/internal/env" 10 | ) 11 | 12 | func Test_envPP(t *testing.T) { 13 | defer initDefaultWriter() 14 | 15 | t.Run("when not supplied", func(t *testing.T) { 16 | env.UnsetEnv(t, "PP") 17 | buf := stubDefaultWriter(t) 18 | initDefaultWriter() 19 | PP("OK") 20 | assertNotEmpty(t, buf.Bytes()) 21 | }) 22 | 23 | t.Run("when provided but empty", func(t *testing.T) { 24 | env.SetEnv(t, "PP", "") 25 | buf := stubDefaultWriter(t) 26 | initDefaultWriter() 27 | PP("OK") 28 | assertNotEmpty(t, buf.Bytes()) 29 | }) 30 | 31 | t.Run("when provided but not a valid file path", func(t *testing.T) { 32 | env.SetEnv(t, "PP", ".") 33 | buf := stubDefaultWriter(t) 34 | initDefaultWriter() 35 | PP("OK") 36 | assertNotEmpty(t, buf.Bytes()) 37 | }) 38 | 39 | t.Run("when existing file provided", func(t *testing.T) { 40 | f, err := os.CreateTemp(t.TempDir(), "") 41 | assertNoError(t, err) 42 | env.SetEnv(t, "PP", f.Name()) 43 | buf := stubDefaultWriter(t) 44 | initDefaultWriter() 45 | PP("OK") 46 | assertEmpty(t, buf.Bytes()) 47 | bs, err := io.ReadAll(f) 48 | assertNoError(t, err) 49 | assertNotEmpty(t, bs) 50 | }) 51 | 52 | t.Run("when non existing file provided", func(t *testing.T) { 53 | fpath := filepath.Join(t.TempDir(), "test.txt") 54 | env.SetEnv(t, "PP", fpath) 55 | buf := stubDefaultWriter(t) 56 | initDefaultWriter() 57 | PP("OK") 58 | assertEmpty(t, buf.Bytes()) 59 | bs, err := os.ReadFile(fpath) 60 | assertNoError(t, err) 61 | assertNotEmpty(t, bs) 62 | }) 63 | 64 | } 65 | -------------------------------------------------------------------------------- /faultinject/fault.go: -------------------------------------------------------------------------------- 1 | package faultinject 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | // Check is a fault-injection helper method which check if there is an injected fault(s) in the given context. 9 | // It checks for errors injected as context value, or ensures to trigger a CallerFault. 10 | // It is safe to use from production code. 11 | func Check(ctx context.Context, faults ...any) error { 12 | if ctx == nil { 13 | return nil 14 | } 15 | for _, fault := range faults { 16 | if err, ok := ctx.Value(fault).(error); ok { 17 | return err 18 | } 19 | } 20 | if ic, ok := lookupInjectContext(ctx); ok { 21 | if err, ok := ic.check(); ok { 22 | tryWaitForDone(ctx) 23 | return err 24 | } 25 | } 26 | if err := ctx.Err(); err != nil { 27 | return err 28 | } 29 | return nil 30 | } 31 | 32 | // After is function that can be called from a deferred context, 33 | // and will inject fault after the function finished its execution. 34 | // The error pointer should point to the function's named return error variable. 35 | // If the function encountered an actual error, fault injection is skipped. 36 | // It is safe to use from production code. 37 | func After(returnErr *error, ctx context.Context, faults ...any) { 38 | if ctx == nil { 39 | return 40 | } 41 | if *returnErr != nil { 42 | return 43 | } 44 | if err := Check(ctx, faults...); err != nil { 45 | *returnErr = err 46 | } 47 | } 48 | 49 | var WaitForContextDoneTimeout = time.Second / 2 50 | 51 | func tryWaitForDone(ctx context.Context) { 52 | timer := time.NewTimer(WaitForContextDoneTimeout) 53 | defer timer.Stop() 54 | select { 55 | case <-ctx.Done(): 56 | case <-timer.C: 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /random/internal/fixture/assets/emaildomains.txt: -------------------------------------------------------------------------------- 1 | gmail.com 2 | yahoo.com 3 | hotmail.com 4 | aol.com 5 | hotmail.co.uk 6 | hotmail.fr 7 | msn.com 8 | yahoo.fr 9 | wanadoo.fr 10 | orange.fr 11 | comcast.net 12 | yahoo.co.uk 13 | yahoo.com.br 14 | yahoo.co.in 15 | live.com 16 | rediffmail.com 17 | free.fr 18 | gmx.de 19 | web.de 20 | yandex.ru 21 | ymail.com 22 | libero.it 23 | outlook.com 24 | uol.com.br 25 | bol.com.br 26 | mail.ru 27 | cox.net 28 | hotmail.it 29 | sbcglobal.net 30 | sfr.fr 31 | live.fr 32 | verizon.net 33 | live.co.uk 34 | googlemail.com 35 | yahoo.es 36 | ig.com.br 37 | live.nl 38 | bigpond.com 39 | terra.com.br 40 | yahoo.it 41 | neuf.fr 42 | yahoo.de 43 | alice.it 44 | rocketmail.com 45 | att.net 46 | laposte.net 47 | facebook.com 48 | bellsouth.net 49 | yahoo.in 50 | hotmail.es 51 | charter.net 52 | yahoo.ca 53 | yahoo.com.au 54 | rambler.ru 55 | hotmail.de 56 | tiscali.it 57 | shaw.ca 58 | yahoo.co.jp 59 | sky.com 60 | earthlink.net 61 | optonline.net 62 | freenet.de 63 | t-online.de 64 | aliceadsl.fr 65 | virgilio.it 66 | home.nl 67 | qq.com 68 | telenet.be 69 | me.com 70 | yahoo.com.ar 71 | tiscali.co.uk 72 | yahoo.com.mx 73 | voila.fr 74 | gmx.net 75 | mail.com 76 | planet.nl 77 | tin.it 78 | live.it 79 | ntlworld.com 80 | arcor.de 81 | yahoo.co.id 82 | frontiernet.net 83 | hetnet.nl 84 | live.com.au 85 | yahoo.com.sg 86 | zonnet.nl 87 | club-internet.fr 88 | juno.com 89 | optusnet.com.au 90 | blueyonder.co.uk 91 | bluewin.ch 92 | skynet.be 93 | sympatico.ca 94 | windstream.net 95 | mac.com 96 | centurytel.net 97 | chello.nl 98 | live.ca 99 | aim.com 100 | bigpond.net.au 101 | -------------------------------------------------------------------------------- /assert/equal.go: -------------------------------------------------------------------------------- 1 | package assert 2 | 3 | import ( 4 | "fmt" 5 | "math/big" 6 | "net" 7 | "reflect" 8 | "testing" 9 | "time" 10 | 11 | "go.llib.dev/testcase/internal/reflects" 12 | ) 13 | 14 | func eq(tb testing.TB, exp, act any) bool { 15 | tb.Helper() 16 | isEq, err := reflects.DeepEqual(exp, act) 17 | Must(tb).NoError(err) 18 | return isEq 19 | } 20 | 21 | type EqualFunc[T any] interface { 22 | func(v1, v2 T) (bool, error) | 23 | func(v1, v2 T) bool 24 | } 25 | 26 | func RegisterEqual[T any, FN EqualFunc[T]](fn FN) struct{} { 27 | var rfn func(v1, v2 reflect.Value) (bool, error) 28 | switch fn := any(fn).(type) { 29 | case func(v1, v2 T) (bool, error): 30 | rfn = func(v1, v2 reflect.Value) (bool, error) { 31 | return fn(v1.Interface().(T), v2.Interface().(T)) 32 | } 33 | case func(v1, v2 T) bool: 34 | rfn = func(v1, v2 reflect.Value) (bool, error) { 35 | return fn(v1.Interface().(T), v2.Interface().(T)), nil 36 | } 37 | default: 38 | panic(fmt.Sprintf("unrecognised Equality checker function signature")) 39 | } 40 | reflects.RegisterIsEqual(reflect.TypeOf((*T)(nil)).Elem(), rfn) 41 | return struct{}{} 42 | } 43 | 44 | var _ = RegisterEqual[time.Time](func(t1, t2 time.Time) bool { 45 | return t1.Equal(t2) 46 | }) 47 | 48 | var _ = RegisterEqual[net.IP](func(ip1, ip2 net.IP) bool { 49 | return ip1.Equal(ip2) 50 | }) 51 | 52 | var _ = RegisterEqual[big.Int](func(v1, v2 big.Int) bool { 53 | return v1.Cmp(&v2) == v2.Cmp(&v1) 54 | }) 55 | 56 | var _ = RegisterEqual[big.Rat](func(v1, v2 big.Rat) bool { 57 | return v1.Cmp(&v2) == v2.Cmp(&v1) 58 | }) 59 | 60 | var _ = RegisterEqual[big.Float](func(v1, v2 big.Float) bool { 61 | return v1.Cmp(&v2) == v2.Cmp(&v1) 62 | }) 63 | -------------------------------------------------------------------------------- /spike_test.go: -------------------------------------------------------------------------------- 1 | package testcase_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func Spike(tb testing.TB) { 9 | if _, ok := os.LookupEnv(`SPIKE`); !ok { 10 | tb.Skip() 11 | } 12 | } 13 | 14 | // This spike meant to help manually verify the grouping mechanism of *testing.T 15 | // > SPIKE=TRUE go testCase -run TestRunGroup -v 16 | func TestRunGroup(t *testing.T) { 17 | Spike(t) 18 | 19 | t.Run(`single`, func(t *testing.T) { 20 | t.Run(`foo`, func(t *testing.T) { 21 | t.Run(`bar`, func(t *testing.T) { 22 | t.Log(`foo-bar`) 23 | }) 24 | t.Run(`baz`, func(t *testing.T) { 25 | t.Log(`foo-baz`) 26 | }) 27 | }) 28 | }) 29 | t.Run(`split`, func(t *testing.T) { 30 | t.Run(`foo`, func(t *testing.T) { 31 | t.Run(`bar`, func(t *testing.T) { 32 | t.Log(`foo-bar`) 33 | }) 34 | }) 35 | t.Run(`foo`, func(t *testing.T) { 36 | t.Run(`baz`, func(t *testing.T) { 37 | t.Log(`foo-baz`) 38 | }) 39 | }) 40 | }) 41 | t.Run(`single+split with lambda`, func(t *testing.T) { 42 | var eventually []func() 43 | t.Run(`foo`, func(t *testing.T) { 44 | eventually = append(eventually, func() { 45 | t.Run(`bar`, func(t *testing.T) { 46 | t.Log(`foo-bar`) 47 | }) 48 | }) 49 | eventually = append(eventually, func() { 50 | t.Run(`baz`, func(t *testing.T) { 51 | t.Log(`foo-baz`) 52 | }) 53 | }) 54 | 55 | // this will run list of them, because we are still within the `testing#T.Run` scope 56 | for _, e := range eventually { 57 | e() 58 | } 59 | }) 60 | 61 | // this will not run the testCase since foo `testing#T.Run` scope is closed 62 | for _, e := range eventually { 63 | e() 64 | } 65 | 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /faultinject/fihttp/Handler.go: -------------------------------------------------------------------------------- 1 | package fihttp 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net/http" 7 | ) 8 | 9 | type Handler struct { 10 | Next http.Handler 11 | ServiceName string 12 | FaultsMapping FaultsMapping 13 | } 14 | 15 | type FaultsMapping map[string]InjectFn 16 | type InjectFn func(context.Context) context.Context 17 | 18 | func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 19 | ctx := r.Context() 20 | var propagatedFaults []Fault 21 | for _, value := range r.Header.Values(Header) { 22 | if faults, ok := h.parseHeader([]byte(value)); ok { 23 | res := h.mapFaultsToTags(faults) 24 | for _, injectFn := range res.Injects { 25 | ctx = injectFn(ctx) 26 | } 27 | propagatedFaults = append(propagatedFaults, res.Propagates...) 28 | } 29 | } 30 | if 0 < len(propagatedFaults) { 31 | ctx = Propagate(ctx, propagatedFaults...) 32 | } 33 | h.Next.ServeHTTP(w, r.WithContext(ctx)) 34 | } 35 | 36 | func (h Handler) parseHeader(data []byte) ([]Fault, bool) { 37 | var faults []Fault 38 | if err := json.Unmarshal(data, &faults); err == nil { 39 | return faults, true 40 | } 41 | var fault Fault 42 | if err := json.Unmarshal(data, &fault); err == nil { 43 | return []Fault{fault}, true 44 | } 45 | return nil, false 46 | } 47 | 48 | type mappingResults struct { 49 | Injects []InjectFn 50 | Propagates []Fault 51 | } 52 | 53 | func (h *Handler) mapFaultsToTags(faults []Fault) mappingResults { 54 | var mr mappingResults 55 | for _, fault := range faults { 56 | if fault.ServiceName != h.ServiceName { 57 | mr.Propagates = append(mr.Propagates, fault) 58 | continue 59 | } 60 | if inject, ok := h.FaultsMapping[fault.Name]; ok { 61 | mr.Injects = append(mr.Injects, inject) 62 | } 63 | } 64 | return mr 65 | } 66 | -------------------------------------------------------------------------------- /random/Make.go: -------------------------------------------------------------------------------- 1 | package random 2 | 3 | func (r *Random) Make(T any) any { 4 | return r.Factory.Make(r, T) 5 | } 6 | 7 | func Slice[T any](length int, mk func() T, opts ...sliceOption) []T { 8 | var c sliceConfig 9 | c.use(opts) 10 | var vs []T 11 | for i := 0; i < length; i++ { 12 | var v T 13 | if c.Unique { 14 | v = Unique(mk, vs...) 15 | } else { 16 | v = mk() 17 | } 18 | vs = append(vs, v) 19 | } 20 | return vs 21 | } 22 | 23 | func Map[K comparable, V any](length int, mk func() (K, V), opts ...mapOption) map[K]V { 24 | var c mapConfig 25 | c.use(opts) 26 | var ( 27 | vs = make(map[K]V) 28 | collisionRetries = 42 29 | ) 30 | for i := 0; i < length; i++ { 31 | var ( 32 | k K 33 | v V 34 | ) 35 | if c.Unique { 36 | var vals []V 37 | for _, val := range vs { 38 | vals = append(vals, val) 39 | } 40 | Unique(func() V { 41 | k, v = mk() 42 | return v 43 | }, vals...) 44 | } else { 45 | k, v = mk() 46 | } 47 | if _, ok := vs[k]; ok { 48 | if 0 < collisionRetries { 49 | collisionRetries-- 50 | i-- 51 | } 52 | continue 53 | } 54 | vs[k] = v 55 | } 56 | return vs 57 | } 58 | 59 | func KV[K comparable, V any](mkK func() K, mkV func() V) func() (K, V) { 60 | return func() (K, V) { 61 | return mkK(), mkV() 62 | } 63 | } 64 | 65 | type sliceConfig struct { 66 | Unique bool 67 | } 68 | 69 | func (c *sliceConfig) use(opts []sliceOption) { 70 | for _, opt := range opts { 71 | opt.sliceOption(c) 72 | } 73 | } 74 | 75 | type sliceOption interface { 76 | sliceOption(*sliceConfig) 77 | } 78 | 79 | type mapConfig struct { 80 | Unique bool 81 | } 82 | 83 | func (c *mapConfig) use(opts []mapOption) { 84 | for _, opt := range opts { 85 | opt.mapOption(c) 86 | } 87 | } 88 | 89 | type mapOption interface { 90 | mapOption(*mapConfig) 91 | } 92 | -------------------------------------------------------------------------------- /internal/environ/environ.go: -------------------------------------------------------------------------------- 1 | package environ 2 | 3 | import ( 4 | "os" 5 | "slices" 6 | "strings" 7 | 8 | "go.llib.dev/testcase/internal" 9 | ) 10 | 11 | // KeySeed is the environment variable key that will be checked for a pseudo random seed, 12 | // which will be used to randomize the order of executions between test cases. 13 | const KeySeed = `TESTCASE_SEED` 14 | 15 | // KeyOrdering is the environment variable key that will be checked for testCase determine 16 | // what order of execution should be used between test cases in a testing group. 17 | // The default sorting behavior is pseudo random based on an the seed. 18 | // 19 | // Mods: 20 | // - defined: execute testCase in the order which they are being defined 21 | // - random: pseudo random based ordering between tests. 22 | const KeyOrdering = `TESTCASE_ORDERING` 23 | const KeyOrdering2 = `TESTCASE_ORDER` 24 | 25 | func OrderingKeys() []string { 26 | return []string{ 27 | KeyOrdering, 28 | KeyOrdering2, 29 | } 30 | } 31 | 32 | const KeyDebug = "TESTCASE_DEBUG" 33 | 34 | var acceptedKeys = []string{ 35 | KeySeed, 36 | KeyOrdering, 37 | KeyOrdering2, 38 | KeyDebug, 39 | } 40 | 41 | func init() { CheckEnvKeys() } 42 | 43 | func CheckEnvKeys() { 44 | var got bool 45 | for _, envPair := range os.Environ() { 46 | ekv := strings.SplitN(envPair, "=", 2) // best effort to split, but it might not be platform agnostic 47 | if len(ekv) != 2 { 48 | continue 49 | } 50 | key, _ := ekv[0], ekv[1] 51 | 52 | if !strings.HasPrefix(key, "TESTCASE_") { 53 | continue 54 | } 55 | 56 | if !slices.Contains(acceptedKeys, key) { 57 | got = true 58 | internal.Warn("unrecognised testcase variable:", key) 59 | } 60 | } 61 | if got { 62 | internal.Warn("check if you might have a typo.") 63 | internal.Warn("accepted environment variables:", strings.Join(acceptedKeys, ", ")) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /docs/examples/MyStruct_test.go: -------------------------------------------------------------------------------- 1 | package examples_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "go.llib.dev/testcase" 7 | "go.llib.dev/testcase/assert" 8 | "go.llib.dev/testcase/docs/examples" 9 | ) 10 | 11 | var myStruct = testcase.Var[examples.MyStruct]{ 12 | ID: `example MyStruct`, 13 | Init: func(t *testcase.T) examples.MyStruct { 14 | return examples.MyStruct{} 15 | }, 16 | } 17 | 18 | func TestMyStruct(t *testing.T) { 19 | s := testcase.NewSpec(t) 20 | s.NoSideEffect() 21 | 22 | // define shared variables and hooks here 23 | // ... 24 | myStruct.Let(s, nil) 25 | 26 | s.Describe(`Say`, SpecMyStruct_Say) 27 | s.Describe(`Foo`, SpecMyStruct_Foo) 28 | s.Describe(`Bar`, SpecMyStruct_Bar) 29 | s.Describe(`Baz`, SpecMyStruct_Baz) 30 | // other specification sub contexts 31 | } 32 | 33 | func SpecMyStruct_Say(s *testcase.Spec) { 34 | var subject = func(t *testcase.T) string { 35 | return myStruct.Get(t).Say() 36 | } 37 | 38 | s.Then(`it will say a famous quote`, func(t *testcase.T) { 39 | assert.Must(t).Equal(`Hello, World!`, subject(t)) 40 | }) 41 | } 42 | 43 | func SpecMyStruct_Foo(s *testcase.Spec) { 44 | var subject = func(t *testcase.T) string { 45 | return myStruct.Get(t).Foo() 46 | } 47 | 48 | s.Then(`it will return with Foo`, func(t *testcase.T) { 49 | assert.Must(t).Equal(`Foo`, subject(t)) 50 | }) 51 | } 52 | 53 | func SpecMyStruct_Bar(s *testcase.Spec) { 54 | var subject = func(t *testcase.T) string { 55 | return myStruct.Get(t).Bar() 56 | } 57 | 58 | s.Then(`it will return with Bar`, func(t *testcase.T) { 59 | assert.Must(t).Equal(`Bar`, subject(t)) 60 | }) 61 | } 62 | 63 | func SpecMyStruct_Baz(s *testcase.Spec) { 64 | var subject = func(t *testcase.T) string { 65 | return myStruct.Get(t).Baz() 66 | } 67 | 68 | s.Then(`it will return with Baz`, func(t *testcase.T) { 69 | assert.Must(t).Equal(`Baz`, subject(t)) 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /seed_test.go: -------------------------------------------------------------------------------- 1 | package testcase_test 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | 7 | "go.llib.dev/testcase" 8 | "go.llib.dev/testcase/assert" 9 | "go.llib.dev/testcase/internal/doubles" 10 | ) 11 | 12 | func TestSpec_seed_T_Random(t *testing.T) { 13 | t.Run("random values are unique across the testing scenarios", func(t *testing.T) { 14 | testcase.UnsetEnv(t, "TESTCASE_SEED") 15 | 16 | assert.Equal(t, 4, len(runSeedScenarios(t))) 17 | }) 18 | t.Run("random values are unique for each spec instance", func(t *testing.T) { 19 | testcase.UnsetEnv(t, "TESTCASE_SEED") 20 | 21 | assert.NotEqual(t, runSeedScenarios(t), runSeedScenarios(t)) 22 | }) 23 | t.Run("using the TESTCASE_SEED env variable allows us to get back the same random values", func(t *testing.T) { 24 | testcase.SetEnv(t, "TESTCASE_SEED", "8426361600145010042") 25 | 26 | tbWithDeterministicTestNames := func() *doubles.TB { 27 | var offset int 28 | return &doubles.TB{ 29 | StubNameFunc: func() string { 30 | offset++ 31 | return strconv.Itoa(offset) 32 | }, 33 | } 34 | } 35 | 36 | assert.Equal(t, 37 | runSeedScenarios(tbWithDeterministicTestNames()), 38 | runSeedScenarios(tbWithDeterministicTestNames())) 39 | }) 40 | } 41 | 42 | func runSeedScenarios(tb testing.TB) map[string]struct{} { 43 | s := testcase.NewSpec(tb) 44 | s.Sequential() 45 | 46 | var values = make(map[string]struct{}) 47 | 48 | s.Test("", func(t *testcase.T) { 49 | values[t.Random.UUID()] = struct{}{} 50 | }) 51 | 52 | s.Test("", func(t *testcase.T) { 53 | values[t.Random.UUID()] = struct{}{} 54 | }) 55 | 56 | s.Context("", func(s *testcase.Spec) { 57 | s.Test("", func(t *testcase.T) { 58 | values[t.Random.UUID()] = struct{}{} 59 | }) 60 | 61 | s.Test("", func(t *testcase.T) { 62 | values[t.Random.UUID()] = struct{}{} 63 | }) 64 | }) 65 | 66 | s.Finish() 67 | return values 68 | } 69 | -------------------------------------------------------------------------------- /random/Unique.go: -------------------------------------------------------------------------------- 1 | package random 2 | 3 | import ( 4 | "time" 5 | 6 | "go.llib.dev/testcase/internal/proxy" 7 | "go.llib.dev/testcase/internal/reflects" 8 | ) 9 | 10 | // Unique function is a utility that helps with generating distinct values 11 | // from those in a given exclusion list. 12 | // If you need multiple unique values of the same type, 13 | // this helper function can be useful for ensuring they're all different. 14 | // 15 | // rnd := random.New(random.CryptoSeed{}) 16 | // v1 := random.Unique(rnd.Int) 17 | // v2 := random.Unique(rnd.Int, v1) 18 | // v3 := random.Unique(rnd.Int, v1, v2) 19 | func Unique[T any](blk func() T, excludeList ...T) T { 20 | if len(excludeList) == 0 { 21 | return blk() 22 | } 23 | 24 | var ( 25 | retries int 26 | deadline = proxy.TimeNow().Add(5 * time.Second) 27 | ) 28 | for ; proxy.TimeNow().Before(deadline) || retries < 5; retries++ { 29 | var ( 30 | v T = blk() 31 | ok bool = true 32 | ) 33 | for _, excluded := range excludeList { 34 | if eq(v, excluded) { 35 | ok = false 36 | break 37 | } 38 | } 39 | if ok { 40 | return v 41 | } 42 | } 43 | panic("random.Unique failed to find a unique value") 44 | } 45 | 46 | func eq[T any](a, b T) bool { 47 | isEqual, err := reflects.DeepEqual(a, b) 48 | if err != nil { 49 | panic(err.Error()) 50 | } 51 | return isEqual 52 | } 53 | 54 | // UniqueValues is an option that used to express a desire for unique value generation with certain functions. 55 | // For example if random.Slice receives the UniqueValues flag, then the created values will be guaranteed to be unique, 56 | // unless it is not possible within a reasonable attempts using the provided value maker function. 57 | const UniqueValues = flagUniqueValues(0) 58 | 59 | type flagUniqueValues int 60 | 61 | func (flagUniqueValues) sliceOption(c *sliceConfig) { c.Unique = true } 62 | func (flagUniqueValues) mapOption(c *mapConfig) { c.Unique = true } 63 | -------------------------------------------------------------------------------- /httpspec/doubles.go: -------------------------------------------------------------------------------- 1 | package httpspec 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net/http" 7 | "testing" 8 | 9 | "go.llib.dev/testcase" 10 | ) 11 | 12 | type RoundTripperFunc func(r *http.Request) (*http.Response, error) 13 | 14 | func (fn RoundTripperFunc) RoundTrip(request *http.Request) (*http.Response, error) { 15 | return fn(request) 16 | } 17 | 18 | func LetRoundTripperDouble(s *testcase.Spec) testcase.Var[*RoundTripperDouble] { 19 | return testcase.Let(s, func(t *testcase.T) *RoundTripperDouble { 20 | return &RoundTripperDouble{} 21 | }) 22 | } 23 | 24 | type RoundTripperDouble struct { 25 | // RoundTripperFunc is an optional argument in case you want to stub the response 26 | RoundTripperFunc RoundTripperFunc 27 | // ReceivedRequests hold all the received http request. 28 | ReceivedRequests []*http.Request 29 | } 30 | 31 | func (d *RoundTripperDouble) RoundTrip(r *http.Request) (*http.Response, error) { 32 | d.ReceivedRequests = append(d.ReceivedRequests, r.Clone(r.Context())) 33 | if d.RoundTripperFunc != nil { 34 | return d.RoundTripperFunc(r) 35 | } 36 | if err := r.Context().Err(); err != nil { 37 | return nil, err 38 | } 39 | const code = http.StatusOK 40 | return &http.Response{ 41 | Status: http.StatusText(code), 42 | StatusCode: code, 43 | Proto: "HTTP/1.0", 44 | ProtoMajor: 1, 45 | ProtoMinor: 0, 46 | Header: http.Header{}, 47 | Body: io.NopCloser(bytes.NewReader([]byte{})), 48 | ContentLength: 0, 49 | TransferEncoding: nil, 50 | Close: false, 51 | Uncompressed: false, 52 | Trailer: nil, 53 | Request: r, 54 | TLS: nil, 55 | }, nil 56 | } 57 | 58 | func (d *RoundTripperDouble) LastReceivedRequest(tb testing.TB) *http.Request { 59 | if len(d.ReceivedRequests) == 0 { 60 | tb.Fatalf("%T did not received any *http.Request", *d) 61 | } 62 | return d.ReceivedRequests[len(d.ReceivedRequests)-1] 63 | } 64 | -------------------------------------------------------------------------------- /internal/suite.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | type NullTB struct{ testing.TB } 8 | 9 | func (n NullTB) Helper() {} 10 | 11 | func (n NullTB) Cleanup(f func()) { 12 | //TODO implement me 13 | panic("implement me") 14 | } 15 | 16 | func (n NullTB) Error(args ...any) { 17 | //TODO implement me 18 | panic("implement me") 19 | } 20 | 21 | func (n NullTB) Errorf(format string, args ...any) { 22 | //TODO implement me 23 | panic("implement me") 24 | } 25 | 26 | func (n NullTB) Fail() { 27 | //TODO implement me 28 | panic("implement me") 29 | } 30 | 31 | func (n NullTB) FailNow() { 32 | //TODO implement me 33 | panic("implement me") 34 | } 35 | 36 | func (n NullTB) Failed() bool { 37 | //TODO implement me 38 | panic("implement me") 39 | } 40 | 41 | func (n NullTB) Fatal(args ...any) { 42 | //TODO implement me 43 | panic("implement me") 44 | } 45 | 46 | func (n NullTB) Fatalf(format string, args ...any) { 47 | //TODO implement me 48 | panic("implement me") 49 | } 50 | 51 | func (n NullTB) Log(args ...any) { 52 | //TODO implement me 53 | panic("implement me") 54 | } 55 | 56 | func (n NullTB) Logf(format string, args ...any) { 57 | //TODO implement me 58 | panic("implement me") 59 | } 60 | 61 | func (n NullTB) Name() string { 62 | //TODO implement me 63 | panic("implement me") 64 | } 65 | 66 | func (n NullTB) Setenv(key, value string) { 67 | //TODO implement me 68 | panic("implement me") 69 | } 70 | 71 | func (n NullTB) Skip(args ...any) { 72 | //TODO implement me 73 | panic("implement me") 74 | } 75 | 76 | func (n NullTB) SkipNow() { 77 | //TODO implement me 78 | panic("implement me") 79 | } 80 | 81 | func (n NullTB) Skipf(format string, args ...any) { 82 | //TODO implement me 83 | panic("implement me") 84 | } 85 | 86 | func (n NullTB) Skipped() bool { 87 | //TODO implement me 88 | panic("implement me") 89 | } 90 | 91 | func (n NullTB) TempDir() string { 92 | //TODO implement me 93 | panic("implement me") 94 | } 95 | -------------------------------------------------------------------------------- /internal/example/spechelper/extres.go: -------------------------------------------------------------------------------- 1 | package spechelper 2 | 3 | import ( 4 | "os" 5 | 6 | "go.llib.dev/testcase" 7 | "go.llib.dev/testcase/internal/example/memory" 8 | "go.llib.dev/testcase/internal/example/mydomain" 9 | "go.llib.dev/testcase/internal/example/someextres" 10 | ) 11 | 12 | var Storage = testcase.Var[mydomain.Storage]{ 13 | ID: `storage`, 14 | OnLet: func(s *testcase.Spec, v testcase.Var[mydomain.Storage]) { 15 | // spec helper function that is environment aware, and can decide what resource should be used in the testCase runtime. 16 | connstr, ok := os.LookupEnv(`TEST_DB_CONNECTION_URL`) 17 | 18 | if !ok { 19 | s.NoSideEffect() 20 | 21 | v.Let(s, func(t *testcase.T) mydomain.Storage { 22 | storage, err := someextres.NewStorage(connstr) 23 | t.Must.NoError(err) 24 | return storage 25 | }) 26 | return 27 | } 28 | 29 | s.HasSideEffect() 30 | // or 31 | s.Sequential() 32 | 33 | v.Let(s, func(t *testcase.T) mydomain.Storage { 34 | return memory.NewStorage() 35 | }) 36 | }, 37 | } 38 | 39 | var ( 40 | ExampleStorage = testcase.Var[*someextres.Storage]{ 41 | ID: "storage component (external resource supplier)", 42 | Init: func(t *testcase.T) *someextres.Storage { 43 | storage, err := someextres.NewStorage(os.Getenv(`TEST_DATABASE_URL`)) 44 | t.Must.Nil(err) 45 | t.Defer(storage.Close) 46 | return storage 47 | }, 48 | } 49 | ExampleStorageGet = func(t *testcase.T) *someextres.Storage { 50 | // workaround until go type parameter release 51 | return ExampleStorage.Get(t) 52 | } 53 | ExampleMyDomainUseCase = testcase.Var[*mydomain.MyUseCase]{ 54 | ID: "my domain rule (domain interactor)", 55 | Init: func(t *testcase.T) *mydomain.MyUseCase { 56 | return &mydomain.MyUseCase{Storage: ExampleStorageGet(t)} 57 | }, 58 | } 59 | ExampleMyDomainUseCaseGet = func(t *testcase.T) *mydomain.MyUseCase { 60 | // workaround until go type parameter release 61 | return ExampleMyDomainUseCase.Get(t) 62 | } 63 | ) 64 | -------------------------------------------------------------------------------- /faultinject/Enabled_test.go: -------------------------------------------------------------------------------- 1 | package faultinject_test 2 | 3 | import ( 4 | "runtime" 5 | "testing" 6 | 7 | "go.llib.dev/testcase" 8 | "go.llib.dev/testcase/assert" 9 | "go.llib.dev/testcase/faultinject" 10 | ) 11 | 12 | func Test_enabled(t *testing.T) { 13 | assert.False(t, faultinject.Enabled(), "by default, fault injection should be disabled") 14 | } 15 | 16 | func TestEnabled_race(t *testing.T) { 17 | testcase.Race(func() { 18 | _ = faultinject.Enabled() 19 | }, func() { 20 | defer faultinject.Enable()() 21 | }) 22 | } 23 | 24 | func TestEnable(t *testing.T) { 25 | CommonEnableTest(t, func(tb testing.TB) { 26 | tb.Cleanup(faultinject.Enable()) 27 | }) 28 | } 29 | 30 | func TestEnableForTest(t *testing.T) { 31 | CommonEnableTest(t, func(tb testing.TB) { 32 | faultinject.EnableForTest(tb) 33 | }) 34 | } 35 | 36 | func CommonEnableTest(t *testing.T, act func(testing.TB)) { 37 | t.Run("enables fault injection during test than restore the og state", func(t *testing.T) { 38 | t.Run("", func(t *testing.T) { 39 | act(t) 40 | assert.True(t, faultinject.Enabled()) 41 | }) 42 | assert.False(t, faultinject.Enabled(), "after cleanup state is restored") 43 | }) 44 | 45 | t.Run("when nested tests depend on Enabling, then only the last restores the original state", func(t *testing.T) { 46 | t.Run("", func(t *testing.T) { 47 | act(t) 48 | t.Run("", func(t *testing.T) { 49 | act(t) 50 | }) 51 | assert.True(t, faultinject.Enabled(), "should be still state as the current testing scope is still active") 52 | }) 53 | assert.False(t, faultinject.Enabled(), "after cleanup state is restored") 54 | }) 55 | 56 | t.Run("when parallel tests depend on Enabling, then only the last finishing test will restore the original state", func(t *testing.T) { 57 | for i, m := 0, runtime.NumCPU()*4; i < m; i++ { 58 | t.Run("", func(t *testing.T) { 59 | t.Parallel() 60 | act(t) 61 | assert.Should(t).True(faultinject.Enabled()) 62 | }) 63 | } 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /httpspec/example_usageWithDotImport_test.go: -------------------------------------------------------------------------------- 1 | package httpspec_test 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "go.llib.dev/testcase" 8 | 9 | . "go.llib.dev/testcase/httpspec" 10 | ) 11 | 12 | func Example_usageWithDotImport() { 13 | s := testcase.NewSpec(testingT) 14 | 15 | Handler.Let(s, func(t *testcase.T) http.Handler { return MyHandler{} }) 16 | 17 | s.Before(func(t *testcase.T) { 18 | t.Log(`given authentication header is set`) 19 | Header.Get(t).Set(`X-Auth-Token`, `token`) 20 | }) 21 | 22 | s.Describe(`GET / - list of X`, func(s *testcase.Spec) { 23 | Method.LetValue(s, http.MethodGet) 24 | Path.LetValue(s, `/`) 25 | 26 | var onSuccess = func(t *testcase.T) ListResponse { 27 | rr := ServeHTTP(t) 28 | t.Must.Equal(http.StatusOK, rr.Code) 29 | // unmarshal the response from rr.body 30 | return ListResponse{} 31 | } 32 | 33 | s.And(`something is set in the query`, func(s *testcase.Spec) { 34 | s.Before(func(t *testcase.T) { 35 | Query.Get(t).Set(`something`, `value`) 36 | }) 37 | 38 | s.Then(`it will react to it as`, func(t *testcase.T) { 39 | listResponse := onSuccess(t) 40 | // assert 41 | _ = listResponse 42 | }) 43 | }) 44 | 45 | s.Then(`it will return the list of resource`, func(t *testcase.T) { 46 | listResponse := onSuccess(t) 47 | // assert 48 | _ = listResponse 49 | }) 50 | }) 51 | 52 | s.Describe(`GET /{resourceID} - show X`, func(s *testcase.Spec) { 53 | Method.LetValue(s, http.MethodGet) 54 | Path.Let(s, func(t *testcase.T) string { 55 | return fmt.Sprintf(`/%s`, t.Random.String()) 56 | }) 57 | 58 | var onSuccess = func(t *testcase.T) ShowResponse { 59 | rr := ServeHTTP(t) 60 | t.Must.Equal(http.StatusOK, rr.Code) 61 | // unmarshal the response from rr.body 62 | return ShowResponse{} 63 | } 64 | 65 | s.Then(`it will return the resource 'show'' representation`, func(t *testcase.T) { 66 | showResponse := onSuccess(t) 67 | // assert 68 | _ = showResponse 69 | }) 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /pp/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | - [Pretty Print (PP)](#pretty-print-pp) 5 | - [usage](#usage) 6 | - [PP / Format](#pp--format) 7 | - [Diff](#diff) 8 | - [printing into a file](#printing-into-a-file) 9 | 10 | 11 | 12 | # Pretty Print (PP) 13 | 14 | the `pp` package provides you with a set of tools that pretty print any Go value. 15 | 16 | ## usage 17 | 18 | ### PP / Format 19 | 20 | ```go 21 | package main 22 | 23 | import ( 24 | "bytes" 25 | "encoding/json" 26 | "go.llib.dev/testcase/pp" 27 | ) 28 | 29 | type ExampleStruct struct { 30 | A string 31 | B int 32 | } 33 | 34 | func main() { 35 | var buf bytes.Buffer 36 | bs, _ := json.Marshal(ExampleStruct{ 37 | A: "The Answer", 38 | B: 42, 39 | }) 40 | buf.Write(bs) 41 | 42 | pp.PP(buf) 43 | } 44 | ``` 45 | 46 | > output 47 | 48 | ``` 49 | bytes.Buffer{ 50 | buf: []byte(`{"A":"The Answer","B":42}`), 51 | off: 0, 52 | lastRead: 0, 53 | } 54 | ``` 55 | 56 | ### Diff 57 | 58 | ```go 59 | package main 60 | 61 | import ( 62 | "fmt" 63 | "go.llib.dev/testcase/pp" 64 | ) 65 | 66 | type ExampleStruct struct { 67 | A string 68 | B int 69 | } 70 | 71 | func main(t *testing.T) { 72 | fmt.Println(pp.Diff(ExampleStruct{ 73 | A: "The Answer", 74 | B: 42, 75 | }, ExampleStruct{ 76 | A: "The Question", 77 | B: 42, 78 | })) 79 | } 80 | ``` 81 | 82 | > output in GNU diff side-by-side style 83 | 84 | ```go 85 | pp_test.ExampleStruct{ pp_test.ExampleStruct{ 86 | A: "The Answer", | A: "The Question", 87 | B: 42, B: 42, 88 | } 89 | ``` 90 | 91 | ## printing into a file 92 | 93 | If STDOUT is supressed, you can also instruct PP to print into a file by setting the output file path in the `PP` environment variable. 94 | 95 | ```shell 96 | export PP="out.txt" 97 | ``` 98 | -------------------------------------------------------------------------------- /clock/Clock.go: -------------------------------------------------------------------------------- 1 | package clock 2 | 3 | import ( 4 | "time" 5 | 6 | "go.llib.dev/testcase/clock/internal" 7 | ) 8 | 9 | // Now returns the current local time. 10 | // 11 | // During testing, Time returned by Now is affected by time travelling. 12 | func Now() time.Time { 13 | return internal.NowFunc() 14 | } 15 | 16 | // Sleep pauses the current goroutine for at least the duration d. 17 | // A negative or zero duration causes Sleep to return immediately. 18 | // 19 | // During testing, it will react to time travelling events 20 | func Sleep(d time.Duration) { 21 | internal.SleepFunc(d) 22 | } 23 | 24 | // After waits for the duration to elapse and then sends the current time on the returned channel. 25 | // The underlying Timer is not recovered by the garbage collector 26 | // 27 | // During testing, After will react to time travelling. 28 | func After(d time.Duration) <-chan time.Time { 29 | return internal.AfterFunc(d) 30 | } 31 | 32 | // Since returns the time elapsed since start. 33 | // It is shorthand for clock.Now().Sub(t). 34 | // 35 | // During testing, Since will react to time travelling. 36 | func Since(start time.Time) time.Duration { 37 | return internal.SinceFunc(start) 38 | } 39 | 40 | // NewTicker returns a new Ticker containing a channel that will send 41 | // the current time on the channel after each tick. The period of the 42 | // ticks is specified by the duration argument. The ticker will adjust 43 | // the time interval or drop ticks to make up for slow receivers. 44 | // The duration d must be greater than zero; if not, NewTicker will 45 | // panic. Stop the ticker to release associated resources. 46 | // 47 | // During testing, Ticker will react to time travelling. 48 | func NewTicker(d time.Duration) *Ticker { 49 | return internal.NewTickerFunc(d) 50 | } 51 | 52 | // Ticker acts as a proxy between the caller and the ticker implementation. 53 | // During testing, it will be a clock-based ticker that can time travel, 54 | // and outside of testing, it will use the time.Ticker. 55 | type Ticker = internal.Ticker 56 | -------------------------------------------------------------------------------- /internal/reflects/access.go: -------------------------------------------------------------------------------- 1 | package reflects 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | func Accessible(rv reflect.Value) reflect.Value { 8 | if rv, ok := TryToMakeAccessible(rv); ok { 9 | return rv 10 | } 11 | return rv 12 | } 13 | 14 | func TryToMakeAccessible(rv reflect.Value) (reflect.Value, bool) { 15 | if rv.CanInterface() { 16 | return rv, true 17 | } 18 | if rv, ok := ToSettable(rv); ok { 19 | return rv, true 20 | } 21 | if rv.CanUint() { 22 | return reflect.ValueOf(rv.Uint()).Convert(rv.Type()), true 23 | } 24 | if rv.CanInt() { 25 | return reflect.ValueOf(rv.Int()).Convert(rv.Type()), true 26 | } 27 | if rv.CanFloat() { 28 | return reflect.ValueOf(rv.Float()).Convert(rv.Type()), true 29 | } 30 | if rv.CanComplex() { 31 | return reflect.ValueOf(rv.Complex()).Convert(rv.Type()), true 32 | } 33 | switch rv.Kind() { 34 | case reflect.String: 35 | return reflect.ValueOf(rv.String()).Convert(rv.Type()), true 36 | case reflect.Map: 37 | m := reflect.MakeMap(rv.Type()) 38 | for _, key := range rv.MapKeys() { 39 | key, ok := TryToMakeAccessible(key) 40 | if !ok { 41 | continue 42 | } 43 | value, ok := TryToMakeAccessible(rv.MapIndex(key)) 44 | if !ok { 45 | continue 46 | } 47 | m.SetMapIndex(key, value) 48 | } 49 | return m, true 50 | case reflect.Slice: 51 | slice := reflect.MakeSlice(rv.Type(), 0, rv.Len()) 52 | for i, l := 0, rv.Len(); i < l; i++ { 53 | v, ok := TryToMakeAccessible(rv.Index(i)) 54 | if !ok { 55 | continue 56 | } 57 | slice = reflect.Append(slice, v) 58 | } 59 | return slice, true 60 | } 61 | return reflect.Value{}, false 62 | } 63 | 64 | func ToSettable(rv reflect.Value) (_ reflect.Value, ok bool) { 65 | if !rv.IsValid() { 66 | return reflect.Value{}, false 67 | } 68 | if rv.CanSet() { 69 | return rv, true 70 | } 71 | if rv.CanAddr() { 72 | if uv := reflect.NewAt(rv.Type(), rv.Addr().UnsafePointer()).Elem(); uv.CanInterface() { 73 | return uv, true 74 | } 75 | } 76 | return reflect.Value{}, false 77 | } 78 | -------------------------------------------------------------------------------- /contracts/CustomTB.go: -------------------------------------------------------------------------------- 1 | package contracts 2 | 3 | import ( 4 | "testing" 5 | 6 | "go.llib.dev/testcase" 7 | "go.llib.dev/testcase/assert" 8 | ) 9 | 10 | type CustomTB struct { 11 | Subject func(*testcase.T) testcase.TBRunner 12 | } 13 | 14 | func (c CustomTB) Test(t *testing.T) { c.Spec(testcase.NewSpec(t)) } 15 | 16 | func (c CustomTB) Benchmark(b *testing.B) { c.Spec(testcase.NewSpec(b)) } 17 | 18 | func (c CustomTB) Spec(s *testcase.Spec) { 19 | customTB := testcase.Let(s, func(t *testcase.T) testcase.TBRunner { 20 | return c.Subject(t) 21 | }) 22 | 23 | s.Describe(`#Run`, func(s *testcase.Spec) { 24 | var ( 25 | name = testcase.Let(s, func(t *testcase.T) string { 26 | return t.Random.String() 27 | }) 28 | blk = testcase.Var[func(testing.TB)]{ID: `blk`} 29 | subject = func(t *testcase.T) bool { 30 | return customTB.Get(t).Run(name.Get(t), blk.Get(t)) 31 | } 32 | ) 33 | 34 | s.When(`block result in a passing sub test`, func(s *testcase.Spec) { 35 | blk.Let(s, func(t *testcase.T) func(testing.TB) { 36 | return func(testing.TB) {} 37 | }) 38 | 39 | s.Then(`it will report the success`, func(t *testcase.T) { 40 | assert.Must(t).True(subject(t)) 41 | }) 42 | 43 | s.Then(`it will not mark the parent as failed`, func(t *testcase.T) { 44 | subject(t) 45 | 46 | assert.Must(t).True(!customTB.Get(t).Failed()) 47 | }) 48 | }) 49 | 50 | s.When(`block fails out early`, func(s *testcase.Spec) { 51 | blk.Let(s, func(t *testcase.T) func(testing.TB) { 52 | return func(tb testing.TB) { tb.FailNow() } 53 | }) 54 | 55 | s.Then(`it will report the fail`, func(t *testcase.T) { 56 | assert.Must(t).True(!subject(t)) 57 | }) 58 | 59 | s.Then(`it will mark the parent as failed`, func(t *testcase.T) { 60 | subject(t) 61 | 62 | assert.Must(t).True(customTB.Get(t).Failed()) 63 | }) 64 | }) 65 | }) 66 | 67 | s.Context("implements testing.TB", func(s *testcase.Spec) { 68 | testcase.RunSuite(s, TestingTB{ 69 | Subject: func(t *testcase.T) testing.TB { 70 | return c.Subject(t) 71 | }, 72 | }) 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /docs/testing-double/spec_helper_test.go: -------------------------------------------------------------------------------- 1 | package testingdouble_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | ) 7 | 8 | type XY struct { 9 | ID string 10 | V int 11 | } 12 | 13 | // Consumer is the business use-case that depends on a XYStorage role. 14 | type Consumer struct { 15 | Storage XYStorage 16 | } 17 | 18 | func (c Consumer) DoSomething(ctx context.Context) { 19 | // use XYStorage here 20 | } 21 | 22 | // XYStorage is the role interface 23 | type XYStorage interface { 24 | CreateXY(ctx context.Context, ptr *XY) error 25 | FindXYByID(ctx context.Context, ptr *XY, id string) (found bool, err error) 26 | } 27 | 28 | // ./contracts package 29 | 30 | type XYStorageContract struct { 31 | Subject func(tb testing.TB) XYStorage 32 | MakeXY func(tb testing.TB) *XY 33 | MakeCtx func(tb testing.TB) context.Context 34 | } 35 | 36 | func (c XYStorageContract) Test(t *testing.T) { 37 | // test behaviour expectations about the storage methods 38 | t.Run(`when entity created in storage, it should assign ID to the received entity and the entity should be located in the storage`, func(t *testing.T) { 39 | var ( 40 | subject = c.Subject(t) 41 | ctx = c.MakeCtx(t) 42 | entity = c.MakeXY(t) 43 | ) 44 | 45 | if err := subject.CreateXY(ctx, entity); err != nil { 46 | t.Fatal(`XYStorage.Create failed:`, err.Error()) 47 | } 48 | 49 | id := entity.ID 50 | 51 | if id == `` { 52 | t.Fatal(`XY.ID was expected to be populated after CreateXY is called`) 53 | } 54 | 55 | t.Log(`entity should be findable in the storage after Create`) 56 | 57 | var actual XY 58 | 59 | found, err := subject.FindXYByID(ctx, &actual, id) 60 | if err != nil { 61 | t.Fatal(`XYStorage.FindByID failed:`, err.Error()) 62 | } 63 | if !found { 64 | t.Fatal(`it was expected that entity can be found in the storage by id`) 65 | } 66 | 67 | if actual != *entity { 68 | t.Fatal(`it was expected that stored entity is the same as the one being persisted in the storage`) 69 | } 70 | }) 71 | } 72 | 73 | func (c XYStorageContract) Benchmark(b *testing.B) { 74 | // benchmark 75 | b.SkipNow() 76 | } 77 | -------------------------------------------------------------------------------- /httpspec/server_test.go: -------------------------------------------------------------------------------- 1 | package httpspec_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "net/url" 7 | "testing" 8 | 9 | "go.llib.dev/testcase" 10 | "go.llib.dev/testcase/assert" 11 | "go.llib.dev/testcase/httpspec" 12 | ) 13 | 14 | func TestLetServer(t *testing.T) { 15 | s := testcase.NewSpec(t) 16 | s.HasSideEffect() 17 | s.Sequential() 18 | 19 | srv := httpspec.LetServer(s, func(t *testcase.T) http.Handler { 20 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 21 | w.WriteHeader(http.StatusTeapot) 22 | }) 23 | }) 24 | 25 | var leak *httptest.Server 26 | s.Test("", func(t *testcase.T) { 27 | response, err := srv.Get(t).Client().Get(srv.Get(t).URL) 28 | t.Must.NoError(err) 29 | t.Must.Equal(http.StatusTeapot, response.StatusCode) 30 | leak = srv.Get(t) 31 | }) 32 | 33 | s.Finish() 34 | _, err := leak.Client().Get(leak.URL) 35 | assert.NotNil(t, err, "should be closed after the test") 36 | } 37 | 38 | func TestClientDo(t *testing.T) { 39 | s := testcase.NewSpec(t) 40 | 41 | req := httpspec.LetRequest(s, httpspec.RequestVar{ 42 | Path: testcase.Let(s, func(t *testcase.T) string { 43 | return "/" + url.PathEscape(t.Random.String()) 44 | }), 45 | Query: testcase.Let(s, func(t *testcase.T) url.Values { 46 | q := url.Values{} 47 | q.Set("foo", t.Random.String()) 48 | return q 49 | 50 | }), 51 | Header: testcase.Let(s, func(t *testcase.T) http.Header { 52 | h := http.Header{} 53 | h.Set("bar", "baz") 54 | return h 55 | }), 56 | }) 57 | srv := httpspec.LetServer(s, func(t *testcase.T) http.Handler { 58 | return http.HandlerFunc(func(w http.ResponseWriter, actual *http.Request) { 59 | expected := req.Get(t) 60 | t.Should.Equal(expected.URL.Path, actual.URL.Path) 61 | t.Should.Equal(expected.URL.Query(), actual.URL.Query()) 62 | t.Should.Equal(expected.URL.Query(), actual.URL.Query()) 63 | w.WriteHeader(http.StatusTeapot) 64 | }) 65 | }) 66 | 67 | s.Test("", func(t *testcase.T) { 68 | response, err := httpspec.ClientDo(t, srv.Get(t), req.Get(t)) 69 | t.Must.NoError(err) 70 | t.Must.Equal(http.StatusTeapot, response.StatusCode) 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /faultinject/fihttp/RoundTripper_test.go: -------------------------------------------------------------------------------- 1 | package fihttp_test 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "testing" 7 | 8 | "go.llib.dev/testcase/random" 9 | 10 | "go.llib.dev/testcase" 11 | "go.llib.dev/testcase/faultinject" 12 | "go.llib.dev/testcase/faultinject/fihttp" 13 | "go.llib.dev/testcase/httpspec" 14 | ) 15 | 16 | func TestRoundTripper(t *testing.T) { 17 | s := testcase.NewSpec(t) 18 | faultinject.EnableForTest(t) 19 | 20 | var ( 21 | next = httpspec.LetRoundTripperDouble(s) 22 | serviceName = testcase.LetValue(s, "") 23 | ) 24 | newRoundTripper := func(t *testcase.T, next http.RoundTripper) http.RoundTripper { 25 | return &fihttp.RoundTripper{ 26 | Next: next, 27 | ServiceName: serviceName.Get(t), 28 | } 29 | } 30 | subject := testcase.Let(s, func(t *testcase.T) *fihttp.RoundTripper { 31 | return newRoundTripper(t, next.Get(t)).(*fihttp.RoundTripper) 32 | }) 33 | 34 | s.Describe(".RoundTrip", func(s *testcase.Spec) { 35 | var ( 36 | request = httpspec.OutboundRequest.Bind(s) 37 | ) 38 | act := func(t *testcase.T) (*http.Response, error) { 39 | return subject.Get(t).RoundTrip(request.Get(t)) 40 | } 41 | 42 | httpspec.ItBehavesLikeRoundTripperMiddleware(s, newRoundTripper) 43 | 44 | s.When("propagated error is present", func(s *testcase.Spec) { 45 | fault := testcase.Let(s, func(t *testcase.T) fihttp.Fault { 46 | return fihttp.Fault{ 47 | ServiceName: t.Random.StringNC(8, random.CharsetAlpha()), 48 | Name: t.Random.StringNC(8, random.CharsetAlpha()), 49 | } 50 | }) 51 | request.Let(s, func(t *testcase.T) *http.Request { 52 | super := request.Super(t) 53 | return super.WithContext(fihttp.Propagate(super.Context(), fault.Get(t))) 54 | }) 55 | 56 | s.Then("outbound request will have the fault injection header", func(t *testcase.T) { 57 | _, err := act(t) 58 | t.Must.NoError(err) 59 | header := next.Get(t).LastReceivedRequest(t).Header.Get(fihttp.Header) 60 | t.Must.NotEmpty(header) 61 | bytes, err := json.Marshal([]fihttp.Fault{fault.Get(t)}) 62 | t.Must.NoError(err) 63 | t.Must.Contains(header, string(bytes)) 64 | }) 65 | }) 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /assert/deprecated.go: -------------------------------------------------------------------------------- 1 | package assert 2 | 3 | import "testing" 4 | 5 | // RetryStrategy 6 | // 7 | // Deprecated: use Loop instead 8 | type RetryStrategy = Loop 9 | 10 | // RetryStrategyFunc 11 | // 12 | // Deprecated: use LoopFunc instead 13 | type RetryStrategyFunc = LoopFunc 14 | 15 | // Contain is a backward port func to enable migration to assert.Contains 16 | // 17 | // Deprecated: use assert.Contains instead of assert.Contain 18 | func Contain(tb testing.TB, haystack, needle any, msg ...Message) { 19 | tb.Helper() 20 | Contains(tb, haystack, needle, msg...) 21 | } 22 | 23 | // NotContain is a backward port func to enable migration to assert.NotContains 24 | // 25 | // Deprecated: use assert.NotContains instead of assert.NotContain 26 | func NotContain(tb testing.TB, haystack, v any, msg ...Message) { 27 | tb.Helper() 28 | NotContains(tb, haystack, v, msg...) 29 | } 30 | 31 | // Contain is a backward port func to enable migration to assert.Asserter#Contains 32 | // 33 | // Deprecated: use assert.Asserter#Contains instead of assert.Asserter#Contain 34 | func (a Asserter) Contain(haystack, needle any, msg ...Message) { 35 | a.TB.Helper() 36 | a.Contains(haystack, needle, msg...) 37 | } 38 | 39 | // NotContain is a backward port func to enable migration to assert.Asserter#NotContains 40 | // 41 | // Deprecated: use assert.Asserter#NotContains instead of assert.Asserter#NotContain 42 | func (a Asserter) NotContain(haystack, needle any, msg ...Message) { 43 | a.TB.Helper() 44 | a.NotContains(haystack, needle, msg...) 45 | } 46 | 47 | // ContainExactly is a backward port func to enable migration to assert.ContainsExactly 48 | // 49 | // Deprecated: use assert.ContainsExactly instead of assert.ContainExactly 50 | func ContainExactly[T any /* Map or Slice */](tb testing.TB, v, oth T, msg ...Message) { 51 | tb.Helper() 52 | ContainsExactly(tb, v, oth, msg...) 53 | } 54 | 55 | // ContainExactly is a backward port func to enable migration to assert.Asserter#ContainsExactly 56 | // 57 | // Deprecated: use assert.Asserter#ContainsExactly instead of assert.Asserter#ContainExactly 58 | func (a Asserter) ContainExactly(v, oth any /* slice | map */, msg ...Message) { 59 | a.TB.Helper() 60 | a.ContainsExactly(v, oth, msg...) 61 | } 62 | -------------------------------------------------------------------------------- /internal/wait/wait.go: -------------------------------------------------------------------------------- 1 | package wait 2 | 3 | import ( 4 | "runtime" 5 | "slices" 6 | "time" 7 | ) 8 | 9 | func Others(timeout time.Duration) { 10 | const WaitUnit = time.Nanosecond 11 | var ( 12 | goroutNum = runtime.NumGoroutine() 13 | startedAt = time.Now() 14 | ) 15 | for i := 0; i < goroutNum; i++ { // since goroutines don't have guarantee when they will be scheduled 16 | runtime.Gosched() // we explicitly mark that we are okay with other goroutines to be scheduled 17 | elapsed := time.Since(startedAt) 18 | if timeout <= elapsed { // if max wait time is reached 19 | return 20 | } 21 | if elapsed < timeout { // if we withint the max wait time, 22 | time.Sleep(WaitUnit) // then we could just yield CPU too with sleep 23 | } 24 | } 25 | } 26 | 27 | var _ = initScales() 28 | var ( 29 | scales = map[time.Duration]float64{} 30 | gscale float64 31 | minDuration time.Duration 32 | ) 33 | 34 | var steps = []time.Duration{ 35 | time.Nanosecond, 36 | time.Microsecond, 37 | 50 * time.Microsecond, 38 | 100 * time.Microsecond, 39 | time.Millisecond, 40 | } 41 | 42 | func initScales() struct{} { 43 | slices.Sort(steps) 44 | var total float64 45 | for _, d := range steps { 46 | scale := scaleByDuration(d) 47 | scales[d] = scale 48 | total += scale 49 | } 50 | gscale = total / float64(len(steps)) 51 | minDuration = time.Duration(scales[time.Nanosecond]) 52 | return struct{}{} 53 | } 54 | 55 | func scaleByDuration(d time.Duration) float64 { 56 | var ( 57 | total time.Duration 58 | count int = 100 59 | ) 60 | for i := 0; i < count; i++ { 61 | start := time.Now() 62 | time.Sleep(d) 63 | duration := time.Since(start) 64 | total += duration 65 | } 66 | var avg = float64(total) / float64(count) 67 | var scale = float64(d) / avg 68 | return scale 69 | } 70 | 71 | func adjust(d time.Duration) time.Duration { 72 | var s float64 = gscale 73 | for i := len(steps) - 1; 0 <= i; i-- { 74 | unit := steps[i] 75 | if unit < d { 76 | break 77 | } 78 | s = scales[unit] 79 | } 80 | return scale(d, s) 81 | } 82 | 83 | func scale(d time.Duration, s float64) time.Duration { 84 | return time.Duration(float64(d) * s) 85 | } 86 | 87 | func For(duration time.Duration) { 88 | if duration <= minDuration { 89 | return 90 | } 91 | runtime.Gosched() 92 | duration = adjust(duration) 93 | time.Sleep(duration) 94 | } 95 | -------------------------------------------------------------------------------- /Race_test.go: -------------------------------------------------------------------------------- 1 | //go:build !race 2 | // +build !race 3 | 4 | package testcase_test 5 | 6 | // The build tag "race" is defined when building with the -race flag. 7 | 8 | import ( 9 | "fmt" 10 | "sync/atomic" 11 | "testing" 12 | "time" 13 | 14 | "go.llib.dev/testcase/sandbox" 15 | 16 | "go.llib.dev/testcase" 17 | "go.llib.dev/testcase/assert" 18 | "go.llib.dev/testcase/internal/doubles" 19 | ) 20 | 21 | func TestRace(t *testing.T) { 22 | eventually := assert.Retry{Strategy: assert.Waiter{Timeout: time.Second}} 23 | 24 | t.Run(`functions run in race against each other`, func(t *testing.T) { 25 | eventually.Assert(t, func(it testing.TB) { 26 | var counter, total int32 27 | blk := func() { 28 | atomic.AddInt32(&total, 1) 29 | c := counter 30 | time.Sleep(time.Millisecond) 31 | counter = c + 1 // counter++ would not work 32 | } 33 | 34 | testcase.Race(blk, blk, blk, blk) 35 | assert.Equal(it, int32(4), total) 36 | it.Log(`counter:`, counter, `total:`, total) 37 | assert.True(it, counter < total, 38 | assert.Message(fmt.Sprintf(`counter was expected to be less that the total block run during race`))) 39 | }) 40 | }) 41 | 42 | t.Run(`each block runs once`, func(t *testing.T) { 43 | var sum int32 44 | testcase.Race(func() { 45 | atomic.AddInt32(&sum, 1) 46 | }, func() { 47 | atomic.AddInt32(&sum, 10) 48 | }, func() { 49 | atomic.AddInt32(&sum, 100) 50 | }, func() { 51 | atomic.AddInt32(&sum, 1000) 52 | }) 53 | assert.Must(t).Equal(int32(1111), sum) 54 | }) 55 | 56 | t.Run(`goexit propagated back from the lambdas after each lambda finished`, func(t *testing.T) { 57 | var fn1Finished, fn2Finished, afterRaceFinished bool 58 | sandbox.Run(func() { 59 | testcase.Race(func() { 60 | fn1Finished = true 61 | }, func() { 62 | fakeTB := &doubles.TB{} 63 | // this only meant to represent why goroutine exit needs to be propagated. 64 | fakeTB.FailNow() 65 | fn2Finished = true 66 | }) 67 | afterRaceFinished = true 68 | }) 69 | 70 | assert.Must(t).True(fn1Finished, `first race block was expected to finish regardless the second's FailNow call`) 71 | assert.Must(t).True(!fn2Finished, `second race block exited with FailNow, it shouldn't finished`) 72 | assert.Must(t).True(!afterRaceFinished, `after the second block exited, the exit should have propagated to the top one`) 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /internal/reflects/IsMutable_test.go: -------------------------------------------------------------------------------- 1 | package reflects_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "go.llib.dev/testcase/assert" 8 | "go.llib.dev/testcase/internal/reflects" 9 | "go.llib.dev/testcase/random" 10 | ) 11 | 12 | func TestIsMutable(t *testing.T) { 13 | rnd := random.New(random.CryptoSeed{}) 14 | type TestCase struct { 15 | V any 16 | Is bool 17 | } 18 | 19 | var nilPtr *int 20 | 21 | var av any 22 | ptrToNil := &av 23 | 24 | for _, tc := range []TestCase{ 25 | { 26 | V: nil, 27 | Is: false, 28 | }, 29 | { 30 | V: rnd.String(), 31 | Is: false, 32 | }, 33 | { 34 | V: rnd.Bool(), 35 | Is: false, 36 | }, 37 | { 38 | V: rnd.Int(), 39 | Is: false, 40 | }, 41 | { 42 | V: int8(rnd.Int()), 43 | Is: false, 44 | }, 45 | { 46 | V: int16(rnd.Int()), 47 | Is: false, 48 | }, 49 | { 50 | V: int32(rnd.Int()), 51 | Is: false, 52 | }, 53 | { 54 | V: int64(rnd.Int()), 55 | Is: false, 56 | }, 57 | { 58 | V: uint(rnd.Int()), 59 | Is: false, 60 | }, 61 | { 62 | V: uint8(rnd.Int()), 63 | Is: false, 64 | }, 65 | { 66 | V: uint16(rnd.Int()), 67 | Is: false, 68 | }, 69 | { 70 | V: uint32(rnd.Int()), 71 | Is: false, 72 | }, 73 | { 74 | V: uint64(rnd.Int()), 75 | Is: false, 76 | }, 77 | { 78 | V: rnd.Float32(), 79 | Is: false, 80 | }, 81 | { 82 | V: rnd.Float64(), 83 | Is: false, 84 | }, 85 | { 86 | V: complex64(0), 87 | Is: false, 88 | }, 89 | { 90 | V: complex128(0), 91 | Is: false, 92 | }, 93 | { 94 | V: struct{}{}, 95 | Is: false, 96 | }, 97 | { 98 | V: &struct{}{}, 99 | Is: true, 100 | }, 101 | { 102 | V: struct{ X *int }{}, 103 | Is: true, 104 | }, 105 | { 106 | V: struct{ x *int }{}, 107 | Is: true, 108 | }, 109 | { 110 | V: []struct{}{}, 111 | Is: true, 112 | }, 113 | { 114 | V: map[int]struct{}{}, 115 | Is: true, 116 | }, 117 | { 118 | V: make(chan int), 119 | Is: true, 120 | }, 121 | { 122 | V: nilPtr, 123 | Is: false, 124 | }, 125 | { 126 | V: ptrToNil, 127 | Is: true, 128 | }, 129 | } { 130 | tc := tc 131 | t.Run(fmt.Sprintf("%T", tc.V), func(t *testing.T) { 132 | assert.Equal(t, tc.Is, reflects.IsMutable(tc.V)) 133 | }) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /faultinject/fihttp/example_test.go: -------------------------------------------------------------------------------- 1 | package fihttp_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "log" 7 | "net/http" 8 | "strings" 9 | 10 | "go.llib.dev/testcase/faultinject" 11 | "go.llib.dev/testcase/faultinject/fihttp" 12 | ) 13 | 14 | func Example() { 15 | type FaultTag struct{} 16 | 17 | client := &http.Client{ 18 | Transport: fihttp.RoundTripper{ 19 | Next: http.DefaultTransport, 20 | ServiceName: "xy-external-service-name", 21 | }, 22 | } 23 | 24 | myHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 25 | // if clients inject the "mapped-fault-name" then we will detect it here. 26 | if err := r.Context().Value(FaultTag{}).(error); err != nil { 27 | const code = http.StatusInternalServerError 28 | http.Error(w, http.StatusText(code), code) 29 | return 30 | } 31 | 32 | // outbound request will have faults injected which is not meant to our service 33 | outboundRequest, err := http.NewRequestWithContext(r.Context(), http.MethodGet, "http://example.com/", nil) 34 | if err != nil { 35 | const code = http.StatusInternalServerError 36 | http.Error(w, http.StatusText(code), code) 37 | return 38 | } 39 | _, _ = client.Do(outboundRequest) 40 | 41 | w.WriteHeader(http.StatusTeapot) 42 | }) 43 | 44 | myHandlerWithFaultInjectionMiddleware := fihttp.Handler{ 45 | Next: myHandler, 46 | ServiceName: "our-service-name", 47 | FaultsMapping: fihttp.FaultsMapping{ 48 | "mapped-fault-name": func(ctx context.Context) context.Context { 49 | return faultinject.Inject(ctx, FaultTag{}, errors.New("boom")) 50 | }, 51 | }, 52 | } 53 | 54 | if err := http.ListenAndServe(":8080", myHandlerWithFaultInjectionMiddleware); err != nil { 55 | log.Fatal(err.Error()) 56 | } 57 | } 58 | 59 | func ExampleRoundTripper() { 60 | const serviceName = "xy-service" 61 | c := &http.Client{ 62 | Transport: fihttp.RoundTripper{ 63 | Next: http.DefaultTransport, 64 | ServiceName: serviceName, 65 | }, 66 | } 67 | 68 | ctx := context.Background() 69 | 70 | ctx = fihttp.Propagate(ctx, fihttp.Fault{ 71 | ServiceName: serviceName, 72 | Name: "fault-name", 73 | }) 74 | 75 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://localhost:8080", strings.NewReader("")) 76 | if err != nil { 77 | panic(err) 78 | } 79 | 80 | response, err := c.Do(req) 81 | if err != nil { 82 | panic(err) 83 | } 84 | 85 | _ = response 86 | } 87 | -------------------------------------------------------------------------------- /TableTest.go: -------------------------------------------------------------------------------- 1 | package testcase 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | ) 7 | 8 | // TableTest allows you to make table tests, without the need to use a boilerplate. 9 | // It optionally allows to use a Spec instead of a testing.TB, 10 | // and then the table tests will inherit the Spec context. 11 | // It guards against mistakes such as using for+t.Run+t.Parallel without variable shadowing. 12 | // TableTest allows a variety of use, please check examples for further information on that. 13 | func TableTest[TBS anyTBOrSpec, TC func(s *Spec) | func(t *T) | any, Act func(t *T) | func(s *Spec) | func(*T, TC)]( 14 | tbs TBS, 15 | tcs map[ /* description */ string]TC, 16 | act Act, 17 | ) { 18 | s := ToSpec(tbs) 19 | var tests []tableTestTestCase[TC] 20 | for desc, tc := range tcs { 21 | tests = append(tests, tableTestTestCase[TC]{ 22 | Desc: desc, 23 | TC: tc, 24 | }) 25 | } 26 | sort.Slice(tests, func(i, j int) bool { 27 | return tests[i].Desc < tests[j].Desc 28 | }) 29 | runT := func(s *Spec, test tableTestTestCase[TC], act func(t *T, tc TC)) { 30 | switch tc := any(test.TC).(type) { 31 | case func(s *Spec): 32 | s.Context(test.Desc, func(s *Spec) { 33 | tc(s) 34 | s.Test("", func(t *T) { 35 | act(t, test.TC) 36 | }) 37 | }) 38 | case func(t *T): 39 | s.Context(test.Desc, func(s *Spec) { 40 | s.Before(tc) 41 | s.Test("", func(t *T) { 42 | act(t, test.TC) 43 | }) 44 | }) 45 | default: 46 | s.Test(test.Desc, func(t *T) { 47 | act(t, test.TC) 48 | }) 49 | } 50 | } 51 | runS := func(s *Spec, test tableTestTestCase[TC], act func(s *Spec)) { 52 | switch tc := any(test.TC).(type) { 53 | case func(s *Spec): 54 | s.Context(test.Desc, func(s *Spec) { 55 | tc(s) 56 | act(s) 57 | }) 58 | case func(t *T): 59 | s.Context(test.Desc, func(s *Spec) { 60 | s.Before(tc) 61 | act(s) 62 | }) 63 | default: 64 | panic(fmt.Sprintf("unsuported TableTest setup: TC<%T> <-> Act<%T>", test.TC, act)) 65 | } 66 | } 67 | s.Context("", func(s *Spec) { 68 | for _, test := range tests { 69 | test := test // pass by value copy to avoid funny concurrency issues 70 | switch act := any(act).(type) { 71 | case func(s *Spec): 72 | runS(s, test, act) 73 | case func(t *T): 74 | runT(s, test, func(t *T, tc TC) { act(t) }) 75 | case func(*T, TC): 76 | runT(s, test, act) 77 | } 78 | } 79 | }) 80 | } 81 | 82 | type tableTestTestCase[TC any] struct { 83 | Desc string 84 | TC TC 85 | } 86 | -------------------------------------------------------------------------------- /httpspec/let.go: -------------------------------------------------------------------------------- 1 | package httpspec 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "net/url" 9 | 10 | "go.llib.dev/testcase" 11 | "go.llib.dev/testcase/random" 12 | ) 13 | 14 | func LetResponseRecorder(s *testcase.Spec) testcase.Var[*httptest.ResponseRecorder] { 15 | return testcase.Let[*httptest.ResponseRecorder](s, func(t *testcase.T) *httptest.ResponseRecorder { 16 | return httptest.NewRecorder() 17 | }) 18 | } 19 | 20 | func LetRequest(s *testcase.Spec, rv RequestVar) testcase.Var[*http.Request] { 21 | rv = rv.withDefaults(s) 22 | return testcase.Let(s, func(t *testcase.T) *http.Request { 23 | defer func() { t.Must.Nil(recover()) }() 24 | u := url.URL{ 25 | Scheme: rv.Scheme.Get(t), 26 | Host: rv.Host.Get(t), 27 | Path: rv.Path.Get(t), 28 | RawPath: rv.Path.Get(t), 29 | RawQuery: rv.Query.Get(t).Encode(), 30 | } 31 | r, err := http.NewRequestWithContext(rv.Context.Get(t), rv.Method.Get(t), u.String(), asIOReader(t, rv.Header.Get(t), rv.Body.Get(t))) 32 | t.Must.NoError(err) 33 | r.Header = rv.Header.Get(t) 34 | return r 35 | }) 36 | } 37 | 38 | type RequestVar struct { 39 | Context testcase.Var[context.Context] 40 | Scheme testcase.Var[string] 41 | Host testcase.Var[string] 42 | Method testcase.Var[string] 43 | Path testcase.Var[string] 44 | Query testcase.Var[url.Values] 45 | Header testcase.Var[http.Header] 46 | Body testcase.Var[any] 47 | } 48 | 49 | func (rv RequestVar) withDefaults(s *testcase.Spec) RequestVar { 50 | if rv.Context.ID == "" { 51 | rv.Context = testcase.Let(s, func(t *testcase.T) context.Context { 52 | return context.Background() 53 | }) 54 | } 55 | if rv.Scheme.ID == "" { 56 | rv.Scheme = testcase.Let(s, func(t *testcase.T) string { 57 | return t.Random.Pick([]string{"http", "https"}).(string) 58 | }) 59 | } 60 | if rv.Host.ID == "" { 61 | rv.Host = testcase.Let(s, func(t *testcase.T) string { 62 | return fmt.Sprintf("www.%s.com", t.Random.StringNC(5, random.CharsetAlpha())) 63 | }) 64 | } 65 | if rv.Method.ID == "" { 66 | rv.Method = testcase.LetValue(s, http.MethodGet) 67 | } 68 | if rv.Path.ID == "" { 69 | rv.Path = testcase.LetValue(s, "/") 70 | } 71 | if rv.Query.ID == "" { 72 | rv.Query = testcase.Let(s, func(t *testcase.T) url.Values { 73 | return make(url.Values) 74 | }) 75 | } 76 | if rv.Header.ID == "" { 77 | rv.Header = testcase.Let(s, func(t *testcase.T) http.Header { 78 | return make(http.Header) 79 | }) 80 | } 81 | if rv.Body.ID == "" { 82 | rv.Body = testcase.LetValue[any](s, nil) 83 | } 84 | return rv 85 | } 86 | -------------------------------------------------------------------------------- /pkg/synctest/phaser.go: -------------------------------------------------------------------------------- 1 | package synctest 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | ) 7 | 8 | // Phaser is a synchronization primitive for coordinating multiple goroutines. 9 | // I that combines the behavior of a latch, barrier, and phaser. 10 | // Goroutines may register via Wait, be released one at a time via Signal or all at once via Broadcast. 11 | // Ultimately, using Finish can ensure, that all wait is released, to ensure that all waiter is released. 12 | // 13 | // Like sync.Mutex, a zero value Phaser is ready to use right away. 14 | // If you’re using a sync.Mutex to protect shared resources, 15 | // you can pass it as an optional argument to Phaser.Wait. 16 | // This lets the mutex be released while waiting and automatically reacquired upon the end of waiting. 17 | type Phaser struct { 18 | m sync.Mutex 19 | o sync.Once 20 | c *sync.Cond 21 | 22 | len int64 23 | done int32 24 | } 25 | 26 | type phaserLockerUnlocker func() 27 | 28 | func (fn phaserLockerUnlocker) Lock() {} 29 | 30 | func (fn phaserLockerUnlocker) Unlock() { fn() } 31 | 32 | func (p *Phaser) init() { 33 | p.o.Do(func() { p.c = sync.NewCond((*nopLocker)(nil)) }) 34 | } 35 | 36 | func (p *Phaser) Len() int { 37 | return int(atomic.LoadInt64(&p.len)) 38 | } 39 | 40 | func (p *Phaser) Wait(ls ...sync.Locker) { 41 | if atomic.LoadInt32(&p.done) != 0 { 42 | return 43 | } 44 | 45 | p.init() 46 | 47 | var ml = multiLocker(ls) 48 | 49 | p.m.Lock() 50 | 51 | if atomic.LoadInt32(&p.done) != 0 { 52 | p.m.Unlock() 53 | return 54 | } 55 | 56 | p.c.L = phaserLockerUnlocker(func() { 57 | // we increment here, because by this time on locker#Unlock, 58 | // the sync.Cond's runtime_notifyListAdd is already executed, 59 | // and listens to Broadcast 60 | atomic.AddInt64(&p.len, 1) 61 | ml.Unlock() 62 | p.c.L = (*nopLocker)(nil) // restore no operation locker 63 | p.m.Unlock() 64 | }) 65 | // during sync.Cond#Wait, ml#Unlock will be unlocked, 66 | // and we need to re-acquire it after the wait finished 67 | defer ml.Lock() 68 | // during Wait the len is incremented, 69 | // and afterwards we need to drecrement it. 70 | defer atomic.AddInt64(&p.len, -1) 71 | p.c.Wait() 72 | } 73 | 74 | func (p *Phaser) Signal() { 75 | p.init() 76 | p.c.Signal() 77 | } 78 | 79 | func (p *Phaser) Broadcast() { 80 | p.init() 81 | p.c.Broadcast() 82 | } 83 | 84 | // Finish lets all waiting goroutines continue immediately. 85 | // After it’s called, any new calls to Wait will also return right away. 86 | func (p *Phaser) Finish() { 87 | p.m.Lock() 88 | defer p.m.Unlock() 89 | p.init() 90 | atomic.CompareAndSwapInt32(&p.done, 0, 1) 91 | p.c.Broadcast() 92 | } 93 | -------------------------------------------------------------------------------- /sandbox/Run_test.go: -------------------------------------------------------------------------------- 1 | package sandbox_test 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "testing" 7 | 8 | "go.llib.dev/testcase" 9 | "go.llib.dev/testcase/assert" 10 | "go.llib.dev/testcase/sandbox" 11 | ) 12 | 13 | func TestRun(t *testing.T) { 14 | s := testcase.NewSpec(t) 15 | 16 | var ( 17 | fn = testcase.Let[func()](s, nil) 18 | ) 19 | act := func(t *testcase.T) sandbox.RunOutcome { 20 | return sandbox.Run(fn.Get(t)) 21 | } 22 | 23 | s.When("the sandboxed function runs without an issue", func(s *testcase.Spec) { 24 | fn.Let(s, func(t *testcase.T) func() { 25 | return func() {} 26 | }) 27 | 28 | s.Then("runs without an issue", func(t *testcase.T) { 29 | outcome := act(t) 30 | t.Must.True(outcome.OK) 31 | t.Must.Nil(outcome.PanicValue) 32 | t.Must.False(outcome.Goexit) 33 | }) 34 | }) 35 | 36 | s.When("the sandboxed function panics", func(s *testcase.Spec) { 37 | expectedPanicValue := testcase.Let(s, func(t *testcase.T) string { 38 | return t.Random.String() 39 | }) 40 | fn.Let(s, func(t *testcase.T) func() { 41 | return func() { 42 | panic(expectedPanicValue.Get(t)) 43 | } 44 | }) 45 | 46 | s.Then("it reports the panic value", func(t *testcase.T) { 47 | outcome := act(t) 48 | t.Must.False(outcome.OK) 49 | t.Must.False(outcome.Goexit) 50 | t.Must.Equal(any(expectedPanicValue.Get(t)), outcome.PanicValue) 51 | }) 52 | 53 | s.Then("it returns the panic stack trace", func(t *testcase.T) { 54 | outcome := act(t) 55 | t.Must.False(outcome.OK) 56 | t.Must.False(outcome.Goexit) 57 | t.Must.Equal(outcome.Trace(), outcome.Trace()) 58 | t.Must.Contains(outcome.Trace(), fmt.Sprintf("panic: %v", expectedPanicValue.Get(t))) 59 | _, file, _, _ := runtime.Caller(0) 60 | t.Must.Contains(outcome.Trace(), file) 61 | }) 62 | }) 63 | 64 | s.When("the sandboxed function calls runtime.Goexit", func(s *testcase.Spec) { 65 | fn.Let(s, func(t *testcase.T) func() { 66 | return func() { runtime.Goexit() } 67 | }) 68 | 69 | s.Then("it reports the Goexit", func(t *testcase.T) { 70 | outcome := act(t) 71 | t.Must.False(outcome.OK) 72 | t.Must.True(outcome.Goexit) 73 | }) 74 | }) 75 | } 76 | 77 | func TestRunOutcome_OnNotOK(t *testing.T) { 78 | t.Run("happy", func(t *testing.T) { 79 | var ran bool 80 | sandbox.Run(func() {}). 81 | OnNotOK(func() { ran = true }) 82 | assert.False(t, ran) 83 | }) 84 | t.Run("rainy", func(t *testing.T) { 85 | var ran bool 86 | sandbox.Run(func() { panic("boom") }). 87 | OnNotOK(func() { ran = true }) 88 | assert.True(t, ran) 89 | }) 90 | t.Run("rainy plus nil block", func(t *testing.T) { 91 | sandbox.Run(func() { panic("boom") }). 92 | OnNotOK(nil) 93 | }) 94 | } 95 | -------------------------------------------------------------------------------- /clock/timecop/timecop.go: -------------------------------------------------------------------------------- 1 | package timecop 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "go.llib.dev/testcase/clock/internal" 8 | "go.llib.dev/testcase/internal/wait" 9 | ) 10 | 11 | // Travel will initiate a time travel. 12 | // It accepts either a time duration as argument to set the travel's duration, 13 | // or a given target time if we need to travel to a specific point in time. 14 | // It accepts optional travel options such as timecop.Freeze and timecop.DeepFreeze. 15 | func Travel[D time.Duration | time.Time](tb testing.TB, d D, tos ...TravelOption) { 16 | tb.Helper() 17 | guardAgainstParallel(tb) 18 | opt := toOption(tos) 19 | switch d := any(d).(type) { 20 | case time.Duration: 21 | travelByDuration(tb, d, opt) 22 | case time.Time: 23 | travelByTime(tb, d, opt) 24 | } 25 | const WaitTimeout = 3 * time.Second 26 | wait.Others(WaitTimeout) 27 | } 28 | 29 | const BlazingFast = 100 30 | 31 | func SetSpeed(tb testing.TB, multiplier float64) { 32 | tb.Helper() 33 | guardAgainstParallel(tb) 34 | if multiplier <= 0 { 35 | tb.Fatal("Timecop.SetSpeed can't receive zero or negative value") 36 | } 37 | tb.Cleanup(internal.SetSpeed(multiplier)) 38 | } 39 | 40 | // guardAgainstParallel 41 | // is a hack that ensures that there was no testing.T.Parallel() used in the test. 42 | func guardAgainstParallel(tb testing.TB) { 43 | tb.Helper() 44 | const key, value = `TEST_CASE_TIMECOP_IN_USE`, "TRUE" 45 | tb.Setenv(key, value) // will fail on parallel execution 46 | } 47 | 48 | func travelByDuration(tb testing.TB, d time.Duration, opt internal.Option) { 49 | tb.Helper() 50 | travelByTime(tb, internal.Now().Add(d), opt) 51 | } 52 | 53 | func travelByTime(tb testing.TB, target time.Time, opt internal.Option) { 54 | tb.Helper() 55 | tb.Cleanup(internal.SetTime(target, opt)) 56 | } 57 | 58 | // Freeze is a Travel TravelOption, and it instruct travel to freeze the time wherever it lands after the travelling.. 59 | const Freeze = freeze(0) 60 | 61 | type freeze int 62 | 63 | func (freeze) configure(o *internal.Option) { 64 | o.Freeze = true 65 | } 66 | 67 | // DeepFreeze is a Travel TravelOption, and it instruct travel to freeze the time wherever it lands after the travelling. 68 | // It is a stronger level of freezing, and will force tickers and timers to also halt immedietly. 69 | const DeepFreeze = deepFreeze(1) 70 | 71 | type deepFreeze int 72 | 73 | func (deepFreeze) configure(o *internal.Option) { 74 | o.Freeze = true 75 | o.Deep = true 76 | } 77 | 78 | // Unfreeze is a Travel TravelOption, and it instruct travel that after the time travelling, the flow of time should continue. 79 | const Unfreeze = unfreeze(0) 80 | 81 | type unfreeze int 82 | 83 | func (unfreeze) configure(o *internal.Option) { 84 | o.Unfreeze = true 85 | } 86 | -------------------------------------------------------------------------------- /ordering.go: -------------------------------------------------------------------------------- 1 | package testcase 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "os" 7 | "sync" 8 | 9 | "go.llib.dev/testcase/internal" 10 | "go.llib.dev/testcase/internal/environ" 11 | ) 12 | 13 | func newOrderer(seed int64) orderer { 14 | switch mod := getGlobalOrderMod(); mod { 15 | case OrderingAsDefined: 16 | return nullOrderer{} 17 | case OrderingAsRandom, undefinedOrdering: 18 | return randomOrderer{Seed: seed} 19 | default: 20 | panic(fmt.Sprintf(`unknown ordering mod: %s`, mod)) 21 | } 22 | } 23 | 24 | type orderer interface { 25 | Order(tc []func()) 26 | } 27 | 28 | type testOrderingMod string 29 | 30 | const ( 31 | undefinedOrdering testOrderingMod = `` 32 | OrderingAsDefined testOrderingMod = `defined` 33 | OrderingAsRandom testOrderingMod = `random` 34 | ) 35 | 36 | //------------------------------------------------- order as defined -------------------------------------------------// 37 | 38 | type nullOrderer struct{} 39 | 40 | func (o nullOrderer) Order([]func()) {} 41 | 42 | //-------------------------------------------------- order randomly --------------------------------------------------// 43 | 44 | type randomOrderer struct { 45 | Seed int64 46 | } 47 | 48 | func (o randomOrderer) Order(tests []func()) { 49 | o.rand().Shuffle(len(tests), o.swapFunc(tests)) 50 | } 51 | 52 | func (o randomOrderer) rand() *rand.Rand { 53 | return rand.New(rand.NewSource(o.Seed)) 54 | } 55 | 56 | func (o randomOrderer) swapFunc(tests []func()) func(i int, j int) { 57 | return func(i, j int) { 58 | tests[i], tests[j] = tests[j], tests[i] 59 | } 60 | } 61 | 62 | //---------------------------------------------- Global Test ordering Mod ----------------------------------------------// 63 | 64 | var ( 65 | globalOrderMod testOrderingMod 66 | globalOrderModInit sync.Once 67 | _ = internal.RegisterCacheFlush(func() { 68 | globalOrderModInit = sync.Once{} 69 | }) 70 | ) 71 | 72 | func getGlobalOrderMod() testOrderingMod { 73 | globalOrderModInit.Do(func() { globalOrderMod = getOrderingModFromENV() }) 74 | return globalOrderMod 75 | } 76 | 77 | func getOrderingModFromENV() testOrderingMod { 78 | var ( 79 | mod string 80 | ok bool 81 | ) 82 | for _, envKey := range environ.OrderingKeys() { 83 | mod, ok = os.LookupEnv(envKey) 84 | if ok { 85 | break 86 | } 87 | } 88 | if !ok { 89 | return OrderingAsRandom 90 | } 91 | switch testOrderingMod(mod) { 92 | case OrderingAsDefined: 93 | return OrderingAsDefined 94 | case OrderingAsRandom: 95 | return OrderingAsRandom 96 | default: 97 | panic(fmt.Sprintf(`Unknown testcase ordering/arrange mod: %s\n\nSupported values: %s, %s`, mod, OrderingAsDefined, OrderingAsRandom)) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /internal/caller/caller_test.go: -------------------------------------------------------------------------------- 1 | package caller_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "go.llib.dev/testcase/assert" 7 | "go.llib.dev/testcase/internal/caller" 8 | ) 9 | 10 | func TestGetFunc(t *testing.T) { 11 | t.Log("from function's top level") 12 | cfn, ok := caller.GetFunc() 13 | assert.True(t, ok) 14 | assert.Equal(t, "TestGetFunc", cfn.Funcion) 15 | assert.Equal(t, "", cfn.Receiver) 16 | assert.Equal(t, "caller_test", cfn.Package) 17 | 18 | t.Log("from lambda that is part of a function") 19 | func() { 20 | cfn, ok := caller.GetFunc() 21 | assert.True(t, ok) 22 | assert.Equal(t, "TestGetFunc", cfn.Funcion) 23 | assert.Equal(t, "", cfn.Receiver) 24 | assert.Equal(t, "caller_test", cfn.Package) 25 | }() 26 | 27 | getCallFixture := GetCallFixture{} 28 | 29 | t.Log("from the top of a method on a receiver") 30 | getCallFixture.TestMethod(t) 31 | 32 | t.Log("from the top of a method on a pointer receiver") 33 | getCallFixture.TestPointerMethod(t) 34 | 35 | t.Log("from a lambda inside a method on a receiver") 36 | getCallFixture.TestLambdaInMethod(t) 37 | 38 | t.Log("from a callback inside a method on a receiver") 39 | getCallFixture.TestCallbackInMethod(t) 40 | } 41 | 42 | type GetCallFixture struct{} 43 | 44 | func (GetCallFixture) TestMethod(tb testing.TB) { 45 | cfn, ok := caller.GetFunc() 46 | assert.True(tb, ok) 47 | assert.Equal(tb, "TestMethod", cfn.Funcion) 48 | assert.Equal(tb, "GetCallFixture", cfn.Receiver) 49 | assert.Equal(tb, "caller_test", cfn.Package) 50 | } 51 | func (*GetCallFixture) TestPointerMethod(tb testing.TB) { 52 | cfn, ok := caller.GetFunc() 53 | assert.True(tb, ok) 54 | assert.Equal(tb, "TestPointerMethod", cfn.Funcion) 55 | assert.Equal(tb, "*GetCallFixture", cfn.Receiver) 56 | assert.Equal(tb, "caller_test", cfn.Package) 57 | } 58 | 59 | func (f GetCallFixture) TestLambdaInMethod(tb testing.TB) { 60 | var run bool 61 | // TODO: if this method wrapped with two lambda 62 | // then for some reason it yields no results. 63 | // figure out why and then fix it 64 | func() { 65 | cfn, ok := caller.GetFunc() 66 | assert.True(tb, ok) 67 | assert.Equal(tb, "TestLambdaInMethod", cfn.Funcion) 68 | assert.Equal(tb, "GetCallFixture", cfn.Receiver) 69 | assert.Equal(tb, "caller_test", cfn.Package) 70 | run = true 71 | }() 72 | assert.True(tb, run) 73 | } 74 | 75 | func (f GetCallFixture) TestCallbackInMethod(tb testing.TB) { 76 | cfn, ok := callbackFn(func() (caller.Func, bool) { 77 | return caller.GetFunc() 78 | }) 79 | 80 | assert.True(tb, ok) 81 | assert.Equal(tb, "TestCallbackInMethod", cfn.Funcion) 82 | assert.Equal(tb, "GetCallFixture", cfn.Receiver) 83 | assert.Equal(tb, "caller_test", cfn.Package) 84 | } 85 | 86 | func callbackFn(blk func() (caller.Func, bool)) (caller.Func, bool) { 87 | return blk() 88 | } 89 | -------------------------------------------------------------------------------- /docs/interface.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | - [Interface: indirection and/or abstraction](#interface-indirection-andor-abstraction) 5 | 6 | 7 | 8 | # Interface: indirection and/or abstraction 9 | 10 | An interface can be thought of as a **static contract** between two components. 11 | The owner and user of the interface called consumer, 12 | and the code that implements the methods of the interface is called the supplier. 13 | At its basics, an interface represents indirection. 14 | The interface expresses a list of possible interactions/methods with the possible implementation. 15 | 16 | When an interface only has one real implementation supplied, 17 | and don't abstract away implementation details 18 | then we usually talk about a header-interface. 19 | An example of this is when the interface talks about executing Queries. 20 | This indirection's primary goal is to test rainy test cases, which otherwise would be difficult to recreate in a testing suite. 21 | By providing a header interface as a dependency, we can supply a test double that can return with errors. 22 | > [example header-interface](/docs/examples/header/main.go) 23 | 24 | When the interface focuses on a particular goal or role and intentionally exclude any implementation details, 25 | then we usually talk about a role-interface. 26 | An example of this is when a storage interface talks about saving, deleting, updating or finding a domain entity. 27 | > [example role-interface](/docs/examples/role/main.go) 28 | 29 | A common mistake with interfaces is over-used to generate mocks, and good behaviour defined as mock expectation. 30 | These types of mocks eventually increase the manual labour and maintenance work in the project's test coverage. 31 | As the project ages and evolves, these mock usages often not updated correctly, 32 | and they continue to keep mimic a not up to date behaviour. 33 | Refactoring also becomes more difficult, 34 | and often tests with mocks shifts the focus from the expected behaviour 35 | to the implementation details of the interaction with mocks. 36 | 37 | The most common tech debt that is often made 38 | when interactors or suppliers replaced with mocks in a test, 39 | to confirm happy paths in the code flow. 40 | Those tests most likely it will end up with asserting implementation details, 41 | rather than testing the expected behavioural outcome. 42 | 43 | The suggested solution is to use as real as a possible component like the actual production implementation 44 | or a [fake testing double](/docs/testing-double/README.md#fake) that verified with a [role interface contract](/docs/contracts.md) 45 | when the production variant would be a bottleneck from the point of testing feedback loop speed. 46 | -------------------------------------------------------------------------------- /assert/Eventually.go: -------------------------------------------------------------------------------- 1 | package assert 2 | 3 | import ( 4 | "runtime" 5 | "testing" 6 | "time" 7 | 8 | "go.llib.dev/testcase/sandbox" 9 | 10 | "go.llib.dev/testcase/internal/doubles" 11 | ) 12 | 13 | func MakeRetry[T time.Duration | int](durationOrCount T) Retry { 14 | switch v := any(durationOrCount).(type) { 15 | case time.Duration: 16 | return Retry{Strategy: Waiter{Timeout: v}} 17 | case int: 18 | return Retry{Strategy: RetryCount(v)} 19 | default: 20 | panic("impossible usage") 21 | } 22 | } 23 | 24 | // Retry Automatically retries operations whose failure is expected under certain defined conditions. 25 | // This pattern enables fault-tolerance. 26 | // 27 | // A common scenario where using Retry will benefit you is testing concurrent operations. 28 | // Due to the nature of async operations, one might need to wait 29 | // and observe the system with multiple tries before the outcome can be seen. 30 | type Retry struct{ Strategy Loop } 31 | 32 | type Loop interface { 33 | // While implements while style looping. 34 | // 35 | // Depending on the outcome of the condition, 36 | // the loop can decide whether further iterations can be done 37 | // or it should be interupted. 38 | While(do func() (condition bool)) 39 | } 40 | 41 | type LoopFunc func(condition func() bool) 42 | 43 | func (fn LoopFunc) While(condition func() bool) { fn(condition) } 44 | 45 | // Assert will attempt to assert with the assertion function block multiple times until the expectations in the function body met. 46 | // In case expectations are failed, it will retry the assertion block using the RetryStrategy. 47 | // The last failed assertion results would be published to the received testing.TB. 48 | // Calling multiple times the assertion function block content should be a safe and repeatable operation. 49 | func (r Retry) Assert(tb testing.TB, blk func(t testing.TB)) { 50 | tb.Helper() 51 | var lastRecorder *doubles.RecorderTB 52 | 53 | isFailed := tb.Failed() 54 | r.Strategy.While(func() bool { 55 | tb.Helper() 56 | runtime.Gosched() 57 | lastRecorder = &doubles.RecorderTB{TB: tb} 58 | ro := sandbox.Run(func() { 59 | tb.Helper() 60 | blk(lastRecorder) 61 | }) 62 | if !ro.OK && !ro.Goexit { // when panic 63 | tb.Fatal("\n" + ro.Trace()) 64 | } 65 | if lastRecorder.IsFailed { 66 | lastRecorder.CleanupNow() 67 | } 68 | if !isFailed && tb.Failed() { 69 | tb.Log("input testing.TB failed during Eventually.Assert, no more retry will be attempted") 70 | return false // if outer testing.TB failed during the assertion, no retry is expected 71 | } 72 | return lastRecorder.IsFailed 73 | }) 74 | 75 | if lastRecorder != nil { 76 | lastRecorder.Forward() 77 | } 78 | pass(tb) 79 | } 80 | 81 | func RetryCount(times int) Loop { 82 | return LoopFunc(func(condition func() bool) { 83 | for i := 0; i < times+1; i++ { 84 | if ok := condition(); !ok { 85 | return 86 | } 87 | } 88 | }) 89 | } 90 | -------------------------------------------------------------------------------- /internal/fmterror/Message_test.go: -------------------------------------------------------------------------------- 1 | package fmterror_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "go.llib.dev/testcase/assert" 7 | "go.llib.dev/testcase/internal/fmterror" 8 | "go.llib.dev/testcase/pp" 9 | ) 10 | 11 | func TestMessage_String(t *testing.T) { 12 | type TestCase struct { 13 | Message fmterror.Message 14 | Expected string 15 | } 16 | for _, tc := range []TestCase{ 17 | { 18 | Message: fmterror.Message{}, 19 | Expected: "", 20 | }, 21 | { 22 | Message: fmterror.Message{ 23 | Name: "Test", 24 | }, 25 | Expected: "[Test] ", 26 | }, 27 | { 28 | Message: fmterror.Message{ 29 | Name: "Test", 30 | Cause: "This", 31 | }, 32 | Expected: "[Test] This", 33 | }, 34 | { 35 | Message: fmterror.Message{ 36 | Name: "Test", 37 | Cause: "This", 38 | Message: []interface{}{"out", 42}, 39 | }, 40 | Expected: "[Test] This\nout 42", 41 | }, 42 | { 43 | Message: fmterror.Message{ 44 | Name: "Test", 45 | Cause: "This", 46 | Values: []fmterror.Value{ 47 | { 48 | Label: "left-label", 49 | Value: 42, 50 | }, 51 | }, 52 | Message: []interface{}{"out", 42}, 53 | }, 54 | Expected: "[Test] This\nout 42\nleft-label:\t42", 55 | }, 56 | { 57 | Message: fmterror.Message{ 58 | Name: "Test", 59 | Cause: "This", 60 | Values: []fmterror.Value{ 61 | { 62 | Label: "left-label", 63 | Value: 42, 64 | }, 65 | { 66 | Label: "right-label", 67 | Value: 24, 68 | }, 69 | }, 70 | Message: []interface{}{"out", 42}, 71 | }, 72 | Expected: "[Test] This\nout 42\n left-label:\t42\nright-label:\t24", 73 | }, 74 | { 75 | Message: fmterror.Message{ 76 | Values: []fmterror.Value{ 77 | { 78 | Label: ".....", 79 | Value: 42, 80 | }, 81 | { 82 | Label: "...", 83 | Value: 24, 84 | }, 85 | }, 86 | }, 87 | Expected: "\n.....:\t42\n ...:\t24", 88 | }, 89 | { 90 | Message: fmterror.Message{ 91 | Values: []fmterror.Value{ 92 | { 93 | Label: "...", 94 | Value: 42, 95 | }, 96 | { 97 | Label: ".....", 98 | Value: 24, 99 | }, 100 | }, 101 | }, 102 | Expected: "\n ...:\t42\n.....:\t24", 103 | }, 104 | { 105 | Message: fmterror.Message{ 106 | Values: []fmterror.Value{ 107 | { 108 | Label: "foo", 109 | Value: []int{1, 2, 3}, 110 | }, 111 | }, 112 | }, 113 | Expected: "\nfoo:\n\n" + pp.Format([]int{1, 2, 3}) + "\n", 114 | }, 115 | { 116 | Message: fmterror.Message{ 117 | Values: []fmterror.Value{ 118 | { 119 | Label: "foo", 120 | Value: fmterror.Formatted("hello"), 121 | }, 122 | }, 123 | }, 124 | Expected: "\nfoo:\t" + "hello", 125 | }, 126 | } { 127 | tc := tc 128 | t.Run(``, func(t *testing.T) { 129 | actual := tc.Message.String() 130 | assert.Equal(t, tc.Expected, actual) 131 | }) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /docs/what-problem-testcase-solves.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | - [To what problem, this project give a solution?](#to-what-problem-this-project-give-a-solution) 5 | - [What does it provide on top of core `testing` package ?](#what-does-it-provide-on-top-of-core-testing-package-) 6 | 7 | 8 | 9 | # To what problem, this project give a solution? 10 | 11 | ## What does it provide on top of core `testing` package ? 12 | 13 | `testcase` package main goal is to provide test specification style, 14 | where you can incrementally explain the execution context of a test edge case. 15 | Through this you have to make a conscious decision to `test` or `skip` a certain edge case. 16 | 17 | To provide some explanation, imagine the following test specification: 18 | 19 | ```gherkin 20 | Given I have a User the database 21 | When the user is active 22 | Then The user state returned as active 23 | ``` 24 | 25 | Traditionally, for an integration test, first you have to create a user, 26 | and you have to setup the entity to reflect the test edge case context, 27 | and then you can persist it. 28 | After that you can make your assertions. 29 | 30 | In situations where you have a lot of small contextual specification details, 31 | for example if a resource like a database has a certain state, 32 | which affects the behavior of the component we currently test, 33 | we have to create test specification like the following example. 34 | 35 | ```go 36 | func TestMyStruct_MyFunc_noUserHadBeenSavedBefore(t *testing.T) { 37 | s, err := GetStorageFromENV() 38 | assert.Must(t).Nil( err) 39 | defer storage.Close() 40 | 41 | err = MyStruct{Storage: s}.MyFunc() 42 | 43 | assert.Must(t).NotNil(err) 44 | } 45 | 46 | func TestMyStruct_MyFunc_storageHasActiveUser(t *testing.T) { 47 | u := User{} 48 | u.IsActive = true 49 | 50 | s, err := GetStorageFromENV() 51 | assert.Must(t).Nil( err) 52 | defer storage.Close() 53 | assert.Must(t).Nil( s.Save(&u)) 54 | defer s.Delete(&u) 55 | 56 | // assert 57 | err = MyStruct{Storage: s}.MyFunc() 58 | assert.Must(t).Nil( err) 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | } 63 | 64 | func TestMyStruct_MyFunc_storageOnlyHasInactiveUser(t *testing.T) { 65 | u := User{} 66 | u.IsActive = false 67 | 68 | s, err := GetStorageFromENV() 69 | assert.Must(t).Nil( err) 70 | defer storage.Close() 71 | assert.Must(t).Nil( s.Save(&u)) 72 | defer s.Delete(&u) 73 | 74 | 75 | err = MyStruct{Storage: s}.MyFunc() 76 | assert.Must(t).NotNil(err) 77 | } 78 | ``` 79 | 80 | And if we want to tweak the arrange part, 81 | we have to duplicate the rest of the parts multiple time then. 82 | 83 | In `testcase` instead of this approach, 84 | you start to specify with the most common part of the context, 85 | that in terms of execution is constant for a certain context, 86 | and then you start add more and more context in each test sub context. 87 | And when the specification feels to have to many nesting layers, 88 | you try to split the component into smaller logical chunks. 89 | -------------------------------------------------------------------------------- /clock/internal/chronos.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | var chrono struct{ Timeline Timeline } 8 | 9 | func init() { chrono.Timeline.Speed = 1 } 10 | 11 | type Timeline struct { 12 | Altered bool 13 | SetAt time.Time 14 | When time.Time 15 | Prev time.Time 16 | Frozen bool 17 | Deep bool 18 | Speed float64 19 | } 20 | 21 | func (tl Timeline) IsZero() bool { 22 | return tl == Timeline{} 23 | } 24 | 25 | func SetSpeed(s float64) func() { 26 | defer notify() 27 | defer lock()() 28 | frozen := chrono.Timeline.Frozen 29 | td := setTime(getTime(), Option{Freeze: frozen}) 30 | og := chrono.Timeline.Speed 31 | chrono.Timeline.Speed = s 32 | return func() { 33 | defer notify() 34 | defer lock()() 35 | chrono.Timeline.Speed = og 36 | td() 37 | } 38 | } 39 | 40 | type Option struct { 41 | Freeze bool 42 | Unfreeze bool 43 | Deep bool 44 | } 45 | 46 | func SetTime(target time.Time, opt Option) func() { 47 | defer notify() 48 | defer lock()() 49 | td := setTime(target, opt) 50 | return func() { 51 | defer notify() 52 | defer lock()() 53 | td() 54 | } 55 | } 56 | 57 | func setTime(target time.Time, opt Option) func() { 58 | prev := getTime() 59 | og := chrono.Timeline 60 | n := chrono.Timeline 61 | n.Altered = true 62 | n.SetAt = time.Now() 63 | n.Prev = prev 64 | n.When = target 65 | if opt.Freeze { 66 | n.Frozen = true 67 | } 68 | if opt.Deep { 69 | n.Deep = true 70 | } 71 | if opt.Unfreeze { 72 | n.Frozen = false 73 | n.Deep = false 74 | } 75 | chrono.Timeline = n 76 | return func() { chrono.Timeline = og } 77 | } 78 | 79 | func ScaledDuration(d time.Duration) time.Duration { 80 | // for some reason, two read lock at the same time has sometimes a deadlock that is not detecable with the -race conditiona detector 81 | // so don't use this inside other functions which are protected by rlock 82 | defer rlock()() 83 | return scaledDuration(d) 84 | } 85 | 86 | func scaledDuration(d time.Duration) time.Duration { 87 | if !chrono.Timeline.Altered { 88 | return d 89 | } 90 | return time.Duration(float64(d) / chrono.Timeline.Speed) 91 | } 92 | 93 | func RemainingDuration(from time.Time, nonScaledDuration time.Duration) time.Duration { 94 | defer rlock()() 95 | now := getTime() 96 | if now.Before(from) { // time travelling can be a bit weird, let's not wait forever if we went back in time 97 | return 0 98 | } 99 | delta := now.Sub(from) 100 | remainer := scaledDuration(nonScaledDuration) - delta 101 | if remainer < 0 { // if due to the time shift, the it was already expected 102 | return 0 103 | } 104 | return remainer 105 | } 106 | 107 | func Now() time.Time { 108 | defer rlock()() 109 | return getTime().Local() 110 | } 111 | 112 | func getTime() time.Time { 113 | now := time.Now() 114 | if !chrono.Timeline.Altered { 115 | return now 116 | } 117 | setAt := chrono.Timeline.SetAt 118 | if chrono.Timeline.Frozen { 119 | setAt = now 120 | } 121 | delta := now.Sub(setAt) 122 | delta = time.Duration(float64(delta) * chrono.Timeline.Speed) 123 | return chrono.Timeline.When.Add(delta) 124 | } 125 | -------------------------------------------------------------------------------- /Suite.go: -------------------------------------------------------------------------------- 1 | package testcase 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "go.llib.dev/testcase/internal" 8 | ) 9 | 10 | // Suite meant to represent a testing suite. 11 | // A test Suite is a collection of test cases. 12 | // In a test suite, the test cases are organized in a logical order. 13 | // A Suite is a great tool to define interface testing suites (contracts). 14 | type Suite interface { 15 | // Spec defines the tests on the received *Spec object. 16 | Spec(s *Spec) 17 | } 18 | 19 | // OpenSuite is a testcase independent testing suite interface standard. 20 | type OpenSuite interface { 21 | // Test is the function that assert expected behavioral requirements from a supplier implementation. 22 | // These behavioral assumptions made by the Consumer in order to simplify and stabilise its own code complexity. 23 | // Every time a Consumer makes an assumption about the behavior of the role interface supplier, 24 | // it should be clearly defined it with tests under this functionality. 25 | Test(*testing.T) 26 | // Benchmark will help with what to measure. 27 | // When you define a role interface contract, you should clearly know what performance aspects important for your Consumer. 28 | // Those aspects should be expressed in a form of Benchmark, 29 | // so different supplier implementations can be easily A/B tested from this aspect as well. 30 | Benchmark(*testing.B) 31 | } 32 | 33 | // RunSuite is a helper function that makes execution one or many Suite easy. 34 | // By using RunSuite, you don't have to distinguish between testing or benchmark execution mod. 35 | // It supports *testing.T, *testing.B, *testcase.T, *testcase.Spec and CustomTB test runners. 36 | func RunSuite[S Suite, TBS anyTBOrSpec](tb TBS, contracts ...S) { 37 | if tb, ok := any(tb).(testingHelper); ok { 38 | tb.Helper() 39 | } 40 | s := ToSpec(tb) 41 | defer s.Finish() 42 | for _, c := range contracts { 43 | c := c 44 | name := getSuiteName(c) 45 | s.Context(name, c.Spec, Group(name)) 46 | } 47 | } 48 | 49 | func RunOpenSuite[OS OpenSuite, TBS anyTBOrSpec](tb TBS, contracts ...OS) { 50 | if tb, ok := any(tb).(testingHelper); ok { 51 | tb.Helper() 52 | } 53 | s := ToSpec(tb) 54 | defer s.Finish() 55 | for _, c := range contracts { 56 | RunSuite(s, OpenSuiteAdapter{OpenSuite: c}) 57 | } 58 | } 59 | 60 | type OpenSuiteAdapter struct{ OpenSuite } 61 | 62 | func (c OpenSuiteAdapter) Spec(s *Spec) { c.runOpenSuite(s.testingTB, c.OpenSuite) } 63 | 64 | func (c OpenSuiteAdapter) runOpenSuite(tb testing.TB, contract OpenSuite) { 65 | switch tb := tb.(type) { 66 | case *T: 67 | c.runOpenSuite(tb.TB, contract) 68 | case *testing.T: 69 | contract.Test(tb) 70 | case *testing.B: 71 | contract.Benchmark(tb) 72 | case TBRunner: 73 | tb.Run(getSuiteName(contract), func(tb testing.TB) { RunOpenSuite(&tb, contract) }) 74 | default: 75 | panic(fmt.Errorf(`unknown testing.TB: %T`, tb)) 76 | } 77 | } 78 | 79 | func getSuiteName(c interface{}) (name string) { 80 | defer func() { name = escapeName(name) }() 81 | switch c := c.(type) { 82 | case interface{ Name() string }: 83 | return c.Name() 84 | case *Spec: 85 | return c.description 86 | default: 87 | return internal.SymbolicName(c) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /docs/nesting.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | - [testcase nesting guide](#testcase-nesting-guide) 5 | - [Flattening](#flattening) 6 | 7 | 8 | 9 | # testcase nesting guide 10 | 11 | In `testcase` to express certain edge cases, 12 | the framework prefers the usage of nesting. 13 | 14 | By convention every `if` statement should have 2 corresponding testing context to represent possible edge cases. 15 | This is required in order to keep clean track of the code complexity. 16 | If the test coverage became too big or have too many level of nesting, 17 | that is the clear sign that the implementation has too broad scope, 18 | and the required mental model for the given production code code is likely to be big. 19 | 20 | * [example code](/docs/examples/ValidateName.go) 21 | * [example test](/docs/examples/ValidateName_test.go) 22 | 23 | For implementations where you need to test business logic, 24 | `testcase#Spec` is suggested, even if the spec has too many nested layers. 25 | That is only represent the complexity of the component. 26 | 27 | ## Flattening 28 | 29 | When the specification becomes too big, 30 | you can improve readability by flattening the specification 31 | by refactoring out specification sub-context(s) into a function. 32 | 33 | The added benefit for this is that all the common variables present on the top level 34 | can be accessed from each of the sub context. 35 | For e.g you can create variable with a database connection, 36 | that ensure a common way to connect and afterwards cleaning up the connection. 37 | 38 | ```go 39 | package examples_test 40 | 41 | import ( 42 | "testing" 43 | 44 | "github.com/stretchr/testify/require" 45 | 46 | "go.llib.dev/testcase" 47 | "go.llib.dev/testcase/docs/examples" 48 | ) 49 | 50 | func TestMyStruct(t *testing.T) { 51 | s := testcase.NewSpec(t) 52 | s.NoSideEffect() 53 | 54 | myStruct := testcase.Let(s, func(t *testcase.T) examples.MyStruct { 55 | return examples.MyStruct{} 56 | }) 57 | 58 | // define shared variables and hooks here 59 | // ... 60 | 61 | s.Describe(`Say`, func(s *testcase.Spec) { 62 | SpecMyStruct_Say(s, myStruct) 63 | }) 64 | s.Describe(`Foo`, func(s *testcase.Spec) { 65 | SpecMyStruct_Foo(s, myStruct) 66 | }) 67 | // other specification sub contexts 68 | } 69 | 70 | func SpecMyStruct_Say(s *testcase.Spec, myStruct testcase.Var[examples.MyStruct]) { 71 | var subject = func(t *testcase.T) string { 72 | return myStruct.Get(t).Say() 73 | } 74 | 75 | s.Then(`it will say a famous quote`, func(t *testcase.T) { 76 | assert.Must(t).Equal( `Hello, World!`, subject(t)) 77 | }) 78 | } 79 | 80 | func SpecMyStruct_Foo(s *testcase.Spec, myStruct testcase.Var[examples.MyStruct]) { 81 | var subject = func(t *testcase.T) string { 82 | return myStruct.Get(t).Foo() 83 | } 84 | 85 | s.Then(`it will say a famous quote`, func(t *testcase.T) { 86 | assert.Must(t).Equal( `Bar`, subject(t)) 87 | }) 88 | } 89 | ``` 90 | 91 | * [example code](/docs/examples/MyStruct.go) 92 | * [example test](/docs/examples/MyStruct_test.go) 93 | --------------------------------------------------------------------------------