testcase

package module
v0.34.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 8, 2021 License: Apache-2.0 Imports: 15 Imported by: 0

README

Table of Contents

Mentioned in Awesome Go GoDoc Build Status Go Report Card codecov

testcase

The testcase package provides tooling to apply BDD testing conventions.

Guide

testcase is a powerful TDD Tooling that requires discipline and understanding about the fundamentals of testing. If you are looking for a guide that helps streamline your knowledge on the topics, then please consider read the below listed articles.

Official API Documentation

If you already use the framework, and you just want pick an example, you can go directly to the API documentation that is kept in godoc format.

Getting Started / Example

Examples kept in godoc format. Every exported functionality aims to have examples provided in the official documentation.

A Basic example:

package main

import (
	"testing"

	"github.com/adamluzsi/testcase"
)

func TestMessageWrapper(t *testing.T) {
	s := testcase.NewSpec(t)
	s.NoSideEffect()

	var (
		message        = testcase.Var{Name: `message`}
		messageWrapper = s.Let(`message wrapper`, func(t *testcase.T) interface{} {
			return MessageWrapper{Message: message.Get(t).(string)}
		})
	)

	s.Describe(`#LookupMessage`, func(s *testcase.Spec) {
		subject := func(t *testcase.T) (string, bool) {
			return messageWrapper.Get(t).(MessageWrapper).LookupMessage()
		}

		s.When(`message is empty`, func(s *testcase.Spec) {
			message.LetValue(s, ``)

			s.Then(`it will return with "ok" as false`, func(t *testcase.T) {
				_, ok := subject(t)
				require.False(t, ok)
			})
		})

		s.When(`message has content`, func(s *testcase.Spec) {
			message.LetValue(s, fixtures.Random.String())

			s.Then(`it will return with "ok" as true`, func(t *testcase.T) {
				_, ok := subject(t)
				require.True(t, ok)
			})

			s.Then(`message received back`, func(t *testcase.T) {
				msg, _ := subject(t)
				require.Equal(t, message.Get(t), msg)
			})
		})
	})
}

Modules

  • httpspec
    • spec module helps you create HTTP API Specs.
  • fixtures
    • fixtures module helps you create random input values for testing

Summary

DRY

testcase provides a way to express common Arrange, Act sections for the Asserts with DRY principle in mind.

  • First you can define your Act section with a method under test as the subject of your test specification
    • The Act section invokes the method under test with the arranged parameters.
  • Then you can build the context of the Act by Arranging the inputs later with humanly explained reasons
    • The Arrange section initializes objects and sets the value of the data that is passed to the method under test.
  • And lastly you can define the test expected outcome in an Assert section.
    • The Assert section verifies that the action of the method under test behaves as expected.

Then adding an additional test edge case to the testing suite becomes easier, as it will have a concrete place where it must be placed.

And if during the creation of the specification, an edge case turns out to be YAGNI, it can be noted, so visually it will be easier to see what edge case is not specified for the given subject.

The value it gives is that to build test for a certain edge case, the required mental model size to express the context becomes smaller, as you only have to focus on one Arrange at a time, until you fully build the bigger picture.

It also implicitly visualize the required mental model of your production code by the nesting. You can read more on that in the nesting section.

Modularization

On top of the DRY convention, any time you need to Arrange a common scenario about your projects domain event, you can modularize these setup blocks in a helper functions.

This helps the readability of the test, while keeping the need of mocks to the minimum as possible for a given test. As a side effect, integration tests can become low hanging fruit for the project.

e.g.:

package mypkg_test

import (
	"testing"

	"my/project/mypkg"


	"github.com/adamluzsi/testcase"

	. "my/project/testing/pkg"
)

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

	// high level Arrange helpers from my/project/testing/pkg
	SetupSpec(s)
	GivenWeHaveUser(s, `myuser`)
	// .. other givens

	myType := func() *mypkg.MyType { return &mypkg.MyType{} }

	s.Describe(`#MyFunc`, func(s *testcase.Spec) {
		var subject = func(t *testcase.T) { myType().MyFunc(t.I(`myuser`).(*mypkg.User)) } // Act

		s.Then(`edge case description`, func(t *testcase.T) {
			// Assert
			subject(t)
		})
	})
}

Stability

  • The package considered stable.
  • The package use rolling release conventions.
  • No breaking change is planned to the package exported API.
  • The package used for production development.
  • The package API is only extended if the practical use case proves its necessity.

Case Study About testcase Package Origin

Reference Project

Documentation

Overview

Package testcase is an opinionated testing framework.

Repository + README: https://github.com/adamluzsi/testcase

Guide: https://github.com/adamluzsi/testcase/blob/master/docs/README.md

Index

Examples

Constants

View Source
const (
	OrderingAsDefined testOrderingMod = `defined`
	OrderingAsRandom  testOrderingMod = `random`
)
View Source
const EnvKeyOrderMod = `TESTCASE_ORDER_MOD`

EnvKeyOrderMod is the environment variable key that will be checked for testCase determine what order of execution should be used between testCase cases in a testing group. The default sorting behavior is pseudo random based on an the seed.

Mods: - defined: execute testCase in the order which they are being defined - random: pseudo random based ordering between tests.

View Source
const EnvKeyOrderSeed = `TESTCASE_ORDER_SEED`

EnvKeyOrderSeed is the environment variable key that will be checked for a pseudo random seed, which will be used to randomize the order of executions between testCase cases.

Variables

This section is empty.

Functions

func RunContract added in v0.34.0

func RunContract(tb interface{}, contracts ...Contract)

func SetEnv added in v0.25.0

func SetEnv(tb testing.TB, key, value string)

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

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

Example
package main

import (
	"github.com/adamluzsi/testcase"
	"testing"
)

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

func UnsetEnv added in v0.28.0

func UnsetEnv(tb testing.TB, key string)

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

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

Example
package main

import (
	"github.com/adamluzsi/testcase"
	"testing"
)

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

Types

type Contract added in v0.30.0

type Contract interface {
	// Test is the function that assert expected behavioral requirements from a supplier implementation.
	// These behavioral assumptions made by the Consumer in order to simplify and stabilise its own code complexity.
	// Every time a Consumer makes an assumption about the behavior of the role interface supplier,
	// it should be clearly defined it with tests under this functionality.
	Test(*testing.T)
	// Benchmark will help with what to measure.
	// When you define a role interface contract, you should clearly know what performance aspects important for your Consumer.
	// Those aspects should be expressed in a form of Benchmark,
	// so different supplier implementations can be easily A/B tested from this aspect as well.
	Benchmark(*testing.B)
}

Contract meant to represent a Role Interface Contract. A role interface express required behavior from a consumer point of view and a role interface contract describes all the assumption about the behavior of supplier that the consumer actively uses to simply the code.

type CustomTB added in v0.23.0

type CustomTB interface {
	testing.TB

	// Run runs blk as a subtest of CustomTB called group. It runs blk in a separate goroutine
	// and blocks until blk returns or calls t.parallel to become a parallel testCase.
	// Run reports whether blk succeeded (or at least did not fail before calling t.parallel).
	//
	// Run may be called simultaneously from multiple goroutines, but list such calls
	// must return before the outer testCase function for t returns.
	Run(name string, blk func(tb testing.TB)) bool
}

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

import (
	"github.com/adamluzsi/testcase/contracts"
	"testing"
)

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

type Retry added in v0.25.0

type Retry struct{ Strategy RetryStrategy }

Retry Automatically retries operations whose failure is expected under certain defined conditions. This pattern enables fault-tolerance.

A common scenario where using Retry will benefit you is testing concurrent operations. Due to the nature of async operations, one might need to wait and observe the system with multiple tries before the outcome can be seen.

Example (AsContextOption)
package main

import (
	"testing"

	"github.com/adamluzsi/testcase"
)

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

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

	}, testcase.Retry{Strategy: testcase.RetryCount(42)})
}
Output:

func (Retry) Assert added in v0.25.0

func (r Retry) Assert(tb testing.TB, blk func(testing.TB))

Assert will attempt to assert with the assertion function block multiple times until the expectations in the function body met. In case expectations are failed, it will retry the assertion block using the RetryStrategy. The last failed assertion results would be published to the received testing.TB. Calling multiple times the assertion function block content should be a safe and repeatable operation.

Example
package main

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

	"github.com/adamluzsi/testcase"
)

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

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

type RetryStrategy added in v0.25.0

type RetryStrategy interface {
	// While implements the retry strategy looping part.
	// Depending on the outcome of the condition,
	// the RetryStrategy can decide whether further iterations can be done or not
	While(condition func() bool)
}

func RetryCount added in v0.26.0

func RetryCount(times int) RetryStrategy
Example
package main

import (
	"github.com/adamluzsi/testcase"
)

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

type RetryStrategyFunc added in v0.26.0

type RetryStrategyFunc func(condition func() bool)

func (RetryStrategyFunc) While added in v0.26.0

func (fn RetryStrategyFunc) While(condition func() bool)

type Spec

type Spec struct {
	// contains filtered or unexported fields
}

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

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

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

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

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

Example
package main

import (
	"github.com/adamluzsi/testcase"
	"github.com/adamluzsi/testcase/fixtures"
	"testing"

	"github.com/stretchr/testify/require"
)

type MessageWrapper struct {
	Message string
}

func (mt MessageWrapper) LookupMessage() (string, bool) {
	if mt.Message == `` {
		return ``, false
	}

	return mt.Message, true
}

func TestMessageWrapper(t *testing.T) {
	s := testcase.NewSpec(t)
	s.NoSideEffect()

	var (
		message        = testcase.Var{Name: `message`}
		messageWrapper = s.Let(`message wrapper`, func(t *testcase.T) interface{} {
			return MessageWrapper{Message: message.Get(t).(string)}
		})
	)

	s.Describe(`#LookupMessage`, func(s *testcase.Spec) {
		subject := func(t *testcase.T) (string, bool) {
			return messageWrapper.Get(t).(MessageWrapper).LookupMessage()
		}

		s.When(`message is empty`, func(s *testcase.Spec) {
			message.LetValue(s, ``)

			s.Then(`it will return with "ok" as false`, func(t *testcase.T) {
				_, ok := subject(t)
				require.False(t, ok)
			})
		})

		s.When(`message has content`, func(s *testcase.Spec) {
			message.LetValue(s, fixtures.Random.String())

			s.Then(`it will return with "ok" as true`, func(t *testcase.T) {
				_, ok := subject(t)
				require.True(t, ok)
			})

			s.Then(`message received back`, func(t *testcase.T) {
				msg, _ := subject(t)
				require.Equal(t, message.Get(t), msg)
			})
		})
	})
}

func main() {
	var t *testing.T
	TestMessageWrapper(t)
}
Output:

Example (MyType)
package main

import (
	"fmt"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/adamluzsi/testcase"
)

type RoleInterface interface {
	Say() string
}

type MyType struct {
	MyResource RoleInterface
}

func (mt *MyType) MyFunc() {}

func (mt *MyType) IsLower(s string) bool {
	return strings.ToLower(s) == s
}

func (mt *MyType) Fallible() (string, error) {
	return "", nil
}

type MyResourceSupplier struct{}

func (MyResourceSupplier) Say() string {
	return `Hello, world!`
}

func main() {
	var t *testing.T

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

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

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

	spec.Describe(`IsLower`, func(s *testcase.Spec) {
		// it is a convention to me to always make a subject for a certain describe block
		//
		var (
			input   = testcase.Var{Name: `input`}
			subject = func(t *testcase.T) bool {
				return myType(t).IsLower(input.Get(t).(string))
			}
		)

		s.When(`input string has lower case characters`, func(s *testcase.Spec) {
			input.LetValue(s, "list lower case")

			s.Before(func(t *testcase.T) {
				// here you can do setups like cleanup for DB tests
			})

			s.After(func(t *testcase.T) {
				// here you can setup a teardown
			})

			s.Around(func(t *testcase.T) func() {
				// here you can setup things that need teardown
				// such example to me is when I use gomock.Controller and mock setup

				return func() {
					// you can do teardown in this
					// this func will be defered after the testCase cases
				}
			})

			s.And(`the first character is capitalized`, func(s *testcase.Spec) {
				// you can add more nesting for more concrete specifications,
				// in each nested block, you work on a separate variable stack,
				// so even if you overwrite something here,
				// that has no effect outside of this scope
				input.LetValue(s, "First character is uppercase")

				s.Then(`it will report false`, func(t *testcase.T) {
					require.False(t, subject(t),
						fmt.Sprintf(`it was expected that %q will be reported to be not lowercase`, t.I(`input`)))
				})

			})

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

	spec.Describe(`Fallible`, func(s *testcase.Spec) {
		var subject = func(t *testcase.T) (string, error) {
			return myType(t).Fallible()
		}

		var onSuccess = func(t *testcase.T) string {
			someMeaningfulVarName, err := subject(t)
			require.Nil(t, err)
			return someMeaningfulVarName
		}

		s.Then(`it will return an empty string`, func(t *testcase.T) {
			require.Equal(t, "", onSuccess(t))
		})
	})
}
Output:

Example (WhenProjectUseSharedSpecificationHelpers)
package main

import (
	"testing"

	"github.com/adamluzsi/testcase"
	// . "my/project/testing/pkg"
)

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

	GivenWeHaveUser(s, `myuser`) // Order
	// .. other givens

	myType := func() *MyType { return &MyType{} }

	s.Describe(`#MyFunc`, func(s *testcase.Spec) {
		var subject = func(t *testcase.T) { myType().MyFunc() } // Act

		s.Then(`edge case description`, func(t *testcase.T) {
			// Assert
			subject(t)
		})
	})
}

/*
	------------------------------------------------------------------------
	Somewhere else in a project's testing package ("my/project/testing/pkg")
	------------------------------------------------------------------------
*/

func SetupSpec(s *testcase.Spec) {
	s.Let(`storage`, func(t *testcase.T) interface{} {
		// create new storage connection
		// t.Defer(s.Close) after the storage was used in the testCase
		return nil
	})
	s.Let(`user manager`, func(t *testcase.T) interface{} {
		// new user manager with storage
		return nil
	})
}

func GivenWeHaveUser(s *testcase.Spec, userLetVar string) {
	s.Let(userLetVar, func(t *testcase.T) interface{} {
		// use user manager to create random user with fixtures maybe
		return nil
	})
}
Output:

Example (WithBenchmark)
var b *testing.B
s := testcase.NewSpec(b)

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

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

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

func NewSpec

func NewSpec(tb testing.TB) *Spec

NewSpec create new Spec struct that is ready for usage.

func (*Spec) After

func (spec *Spec) After(afterBlock testCaseBlock)

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

Example
package main

import (
	"testing"

	"github.com/adamluzsi/testcase"
)

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

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

func (*Spec) And

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

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

Example
package main

import (
	"testing"

	"github.com/adamluzsi/testcase"
)

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

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

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

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

			})
		})

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

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

			})
		})
	})
}
Output:

func (*Spec) Around

func (spec *Spec) Around(aroundBlock hookBlock)

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

Example
package main

import (
	"testing"

	"github.com/adamluzsi/testcase"
)

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

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

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

func (*Spec) Before

func (spec *Spec) Before(beforeBlock testCaseBlock)

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

Example
package main

import (
	"testing"

	"github.com/adamluzsi/testcase"
)

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

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

func (*Spec) Context

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

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

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

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

Example
package main

import (
	"testing"

	"github.com/adamluzsi/testcase"
)

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

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

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

		})
	})
}
Output:

func (*Spec) Describe

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

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

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

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

Example
var t *testing.T
s := testcase.NewSpec(t)

myType := s.Let(`myType`, func(t *testcase.T) interface{} {
	return &MyType{}
})

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

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

func (*Spec) Finish added in v0.29.0

func (spec *Spec) Finish()

Finish runs all the test in the unfinished testing scopes and mark them finished. Finish can be used when it is important to run the test before the testing#TB.Cleanup.

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

func (*Spec) HasSideEffect added in v0.9.0

func (spec *Spec) HasSideEffect()

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

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

Example
package main

import (
	"testing"

	"github.com/adamluzsi/testcase"
)

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

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

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

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

func (*Spec) Let

func (spec *Spec) Let(varName string, blk letBlock) Var

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

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

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

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

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

Example
package main

import (
	"testing"

	"github.com/adamluzsi/testcase"
)

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

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

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

Example (EagerLoading)
package main

import (
	"testing"

	"github.com/adamluzsi/testcase"
)

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

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

	s.Then(`testCase case`, func(t *testcase.T) {
		t.Log(myTestVar.Get(t).(string))
	})
}
Output:

Example (Mock)
//go:generate mockgen -source example_Spec_Let_mock_test.go -destination example_Spec_Let_mock_mocks_test.go -package testcase_test
package main

import (
	"testing"

	"github.com/golang/mock/gomock"

	"github.com/adamluzsi/testcase"
)

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

	mock := s.Let(`the-mock`, func(t *testcase.T) interface{} {
		ctrl := gomock.NewController(t)
		mock := NewMockInterfaceExample(ctrl)
		t.Defer(ctrl.Finish)
		return mock
	})

	s.When(`some scope where mock should behave in a certain way`, func(s *testcase.Spec) {
		s.Before(func(t *testcase.T) {
			mock.Get(t).(*MockInterfaceExample).
				EXPECT().
				Say().
				Return(`some value but can also be a value from *testcase.variables`)
		})

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

type InterfaceExample interface {
	Say() string
}
Output:

Example (SqlDB)
package main

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

	"github.com/stretchr/testify/require"

	"github.com/adamluzsi/testcase"
)

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

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

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

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

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

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

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

func getDBConnection(t testing.TB) *sql.DB {
	// logic to retrieve cached db connection in the testing environment
	return nil
}
Output:

Example (UsageWithinNestedScope)
var t *testing.T
s := testcase.NewSpec(t)

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

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

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

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

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

		s.Then(`it will report false`, func(t *testcase.T) {
			require.False(t, subject(t))
		})
	})
})
Output:

func (*Spec) LetValue added in v0.2.1

func (spec *Spec) LetValue(varName string, value interface{}) Var

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

Example
package main

import (
	"testing"

	"github.com/adamluzsi/testcase"
)

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

	variable := s.LetValue(`variable Name`, "value")

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

Example (UsageWithinNestedScope)
var t *testing.T
s := testcase.NewSpec(t)

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

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

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

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

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

		s.Then(`it will report false`, func(t *testcase.T) {
			require.False(t, subject(t))
		})
	})
})
Output:

func (*Spec) NoSideEffect added in v0.2.1

func (spec *Spec) NoSideEffect()

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

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

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

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

Example
package main

import (
	"testing"

	"github.com/adamluzsi/testcase"
)

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

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

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

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

func (*Spec) Parallel

func (spec *Spec) Parallel()

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

Example
package main

import (
	"testing"

	"github.com/adamluzsi/testcase"
)

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

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

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

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

Example (ScopedWithContext)
package main

import (
	"testing"

	"github.com/adamluzsi/testcase"
)

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

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

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

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

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

func (*Spec) Sequential added in v0.9.0

func (spec *Spec) Sequential()

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

Example
package main

import (
	"testing"

	"github.com/adamluzsi/testcase"
)

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

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

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

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

Example (FromSpecHelper)
package main

import (
	"os"
	"testing"

	"github.com/adamluzsi/testcase"
)

func main() {
	var t *testing.T
	s := testcase.NewSpec(t)
	Setup(s) // setup specification with spec helper function

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

	var (
		myUseCase = func(t *testcase.T) *MyUseCaseThatHasStorageDependency {
			return &MyUseCaseThatHasStorageDependency{Storage: Storage.Get(t).(MyUseCaseStorageRoleInterface)}
		}
	)

	s.Describe(`#SomeMethod`, func(s *testcase.Spec) {
		var subject = func(t *testcase.T) bool {
			return myUseCase(t).SomeMethod()
		}

		s.Test(`it is expected ...`, func(t *testcase.T) {
			if !subject(t) {
				t.Fatal(`assertion failed`)
			}
		})
	})
}

///////////////////////////////////////// in some package testing / spechelper /////////////////////////////////////////

var Storage = testcase.Var{Name: `storage`}

func Setup(s *testcase.Spec) {
	// spec helper function that is environment aware, and can decide what resource should be used in the testCase runtime.
	env, ok := os.LookupEnv(`TEST_DB_CONNECTION_URL`)

	if ok {
		s.Sequential()
		// or
		s.HasSideEffect()
		Storage.Let(s, func(t *testcase.T) interface{} {
			// open database connection
			_ = env // use env to connect or something
			// setup isolation with tx
			return &ExternalResourceBasedStorage{ /*...*/ }
		})
	} else {
		Storage.Let(s, func(t *testcase.T) interface{} {
			return &InMemoryBasedStorage{}
		})
	}
}

type InMemoryBasedStorage struct{}

type ExternalResourceBasedStorage struct{}

type MyUseCaseThatHasStorageDependency struct {
	Storage MyUseCaseStorageRoleInterface
}

func (d *MyUseCaseThatHasStorageDependency) SomeMethod() bool {
	return false
}

type MyUseCaseStorageRoleInterface interface{}
Output:

Example (ScopedWithContext)
package main

import (
	"testing"

	"github.com/adamluzsi/testcase"
)

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

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

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

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

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

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

func (*Spec) Skip added in v0.14.0

func (spec *Spec) Skip(args ...interface{})

Skip is equivalent to Log followed by SkipNow on T for each test case.

Example
package main

import (
	"testing"

	"github.com/adamluzsi/testcase"
)

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

	s.Context(`sub spec`, func(s *testcase.Spec) {
		s.Skip(`WIP`)

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

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

		s.Context(`skipped as well just like the tests of the parent`, func(s *testcase.Spec) {
			s.Test(`will be skipped`, func(t *testcase.T) {})
		})
	})

	s.Test(`this will still run since it is not part of the scope where Spec#Skip was called`, func(t *testcase.T) {})
}
Output:

func (*Spec) SkipBenchmark added in v0.23.0

func (spec *Spec) SkipBenchmark()

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

Example
package main

import (
	"testing"

	"github.com/adamluzsi/testcase"
)

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

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

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

Example (ScopedWithContext)
package main

import (
	"testing"

	"github.com/adamluzsi/testcase"
)

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

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

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

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

func (*Spec) Tag added in v0.10.0

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

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

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

  • TESTCASE_TAG_INCLUDE to filter down to test with a certain tag
  • TESTCASE_TAG_EXCLUDE to exclude certain test from the overall testing scope.

They can be combined as well.

example usage:

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

import (
	"testing"

	"github.com/adamluzsi/testcase"
)

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

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

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

func (*Spec) Test

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

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

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

Example
package main

import (
	"testing"

	"github.com/adamluzsi/testcase"
)

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

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

func (*Spec) Then

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

Then is an alias for Test

Example
package main

import (
	"testing"

	"github.com/adamluzsi/testcase"
)

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

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

func (*Spec) When

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

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

Example
var t *testing.T
s := testcase.NewSpec(t)

var (
	myType  = func(t *testcase.T) *MyType { return &MyType{} }
	input   = testcase.Var{Name: `input`}
	subject = func(t *testcase.T) bool { return myType(t).IsLower(input.Get(t).(string)) }
)

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

	s.Then(`it will be false`, func(t *testcase.T) {
		require.False(t, subject(t))
	})
})

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

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

type SpecOption added in v0.28.0

type SpecOption interface {
	// contains filtered or unexported methods
}

func Flaky added in v0.23.0

func Flaky(CountOrTimeout interface{}) SpecOption

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

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

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

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

Example (RetryNTimes)
package main

import (
	"github.com/adamluzsi/testcase"
	"testing"
)

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

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

Example (RetryUntilTimeout)
package main

import (
	"github.com/adamluzsi/testcase"
	"testing"
	"time"
)

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

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

func Group added in v0.23.5

func Group(name string) SpecOption

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

Example
package main

import (
	"github.com/adamluzsi/testcase"
	"testing"
)

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

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

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

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

func SkipBenchmark added in v0.20.0

func SkipBenchmark() SpecOption
Example
package main

import (
	"github.com/adamluzsi/testcase"
	"testing"
)

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

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

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

type T

type T struct {
	testing.TB
	// contains filtered or unexported fields
}

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

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

func (*T) Cleanup added in v0.19.0

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

func (*T) Defer added in v0.2.1

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

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

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

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

e.g.:

  • mock initialization with mock controller, where the mock controller #Finish function must be executed after each testCase suite.
  • sql.DB / sql.Tx
  • basically anything that has the io.Closer interface
Example
package main

import (
	"database/sql"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/adamluzsi/testcase"
)

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

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

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

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

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

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

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

Example (WithArgs)
package main

import (
	"testing"

	"github.com/adamluzsi/testcase"
)

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

	const something = `the ExampleDeferTeardownWithArgs value`

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

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

		entity.DoSomething()
	})
}

type ExampleDeferTeardownWithArgs struct{}

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

func (*ExampleDeferTeardownWithArgs) DoSomething() {}
Output:

func (*T) HasTag added in v0.10.2

func (t *T) HasTag(tag string) bool
Example
package main

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

	"github.com/stretchr/testify/require"

	"github.com/adamluzsi/testcase"
)

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

	s.Let(`db`, func(t *testcase.T) interface{} {
		db, err := sql.Open(`driverName`, `dataSourceName`)
		require.Nil(t, err)

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

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

func (*T) I

func (t *T) I(varName string) interface{}

I will return a testcase variable. it is suggested to use interface casting right after to it, so you can work with concrete types. If there is no such value, then it will panic with a "friendly" message.

func (*T) Let added in v0.2.0

func (t *T) Let(varName string, value interface{})

Let will allow you to define/override a spec runtime bounded variable. The idiom is that if you cannot express the variable declaration with spec level let, or if you need to override in a sub scope a let's content using the previous variable state, or a result of a multi return variable needs to be stored at spec runtime level you can utilize this Let function to achieve this.

Typical use-case to this when you want to have a spec.Context, with different values or states, but you don't want to rebuild from scratch at each layer.

Example
package main

import (
	"context"
	"testing"

	"github.com/adamluzsi/testcase"
)

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

	ctx := s.Let(`ctx`, func(t *testcase.T) interface{} {
		return context.Background()
	})

	s.When(`let can be manipulated during runtime hooks by simply calling *T#Let`, func(s *testcase.Spec) {
		s.Before(func(t *testcase.T) {
			newContext := context.WithValue(ctx.Get(t).(context.Context), `certain`, `value`)

			// here for example we update the testCase variable ctx to have a certain value to fulfil the subcontext goal
			t.Let(ctx.Name, newContext)
			// or with variable setter
			ctx.Set(t, newContext)
		})

		s.Then(`ctx here has the value that was assigned in the before hook`, func(t *testcase.T) {
			_ = ctx.Get(t).(context.Context)
		})
	})

	s.Then(`your ctx is in the original state without any modifications`, func(t *testcase.T) {
		_ = ctx.Get(t).(context.Context)
	})
}
Output:

type Var added in v0.16.0

type Var struct {
	// Name is the testCase spec variable group from where the cached value can be accessed later on.
	// Name is Mandatory when you create a variable, else the empty string will be used as the variable group.
	Name string
	// Init is an optional constructor definition that will be used when Var is bonded to a *Spec without constructor function passed to the Let function.
	// The goal of this field to initialize a variable that can be reused across different testing suites by bounding the Var to a given testing suite.
	//
	// Please use #Get if you wish to access a testCase runtime across cached variable value.
	// The value returned by this is not subject to any #Before and #Around hook that might mutate the variable value during the testCase runtime.
	// Init function doesn't cache the value in the testCase runtime spec but literally just meant to initialize a value for the Var in a given testCase case.
	// Please use it with caution.
	Init letBlock /* [T] */
}

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

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

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

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

Example
var t *testing.T
s := testcase.NewSpec(t)

var (
	resource = testcase.Var{Name: `resource`}
	myType   = s.Let(`myType`, func(t *testcase.T) interface{} {
		return &MyType{MyResource: resource.Get(t).(RoleInterface)}
	})
)

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

	s.When(`resource is xy`, func(s *testcase.Spec) {
		resource.Let(s, func(t *testcase.T) interface{} {
			return MyResourceSupplier{}
		})

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

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

Example (Spechelper)
package main

import (
	"fmt"
	"github.com/adamluzsi/testcase"
	"github.com/adamluzsi/testcase/internal/example/mydomain"
	"github.com/adamluzsi/testcase/internal/example/storages"
	"github.com/stretchr/testify/require"
	"net/http"
	"net/http/httptest"
	"os"
	"testing"
)

// package spechelper

var (
	ExampleStorage = testcase.Var{
		Name: "storage component (external resource supplier)",
		Init: func(t *testcase.T) interface{} {
			storage, err := storages.New(os.Getenv(`TEST_DATABASE_URL`))
			require.Nil(t, err)
			t.Defer(storage.Close)
			return storage
		},
	}
	ExampleStorageGet = func(t *testcase.T) *storages.Storage {
		// workaround until go type parameter release
		return ExampleStorage.Get(t).(*storages.Storage)
	}
	ExampleMyDomainUseCase = testcase.Var{
		Name: "my domain rule (domain interactor)",
		Init: func(t *testcase.T) interface{} {
			return &mydomain.MyUseCaseInteractor{Storage: ExampleStorageGet(t)}
		},
	}
	ExampleMyDomainUseCaseGet = func(t *testcase.T) *mydomain.MyUseCaseInteractor {
		// workaround until go type parameter release
		return ExampleMyDomainUseCase.Get(t).(*mydomain.MyUseCaseInteractor)
	}
)

// package httpapi // external interface

func NewAPI(interactor *mydomain.MyUseCaseInteractor) *http.ServeMux {
	mux := http.NewServeMux()
	mux.HandleFunc(`/foo`, func(w http.ResponseWriter, r *http.Request) {
		reply, err := interactor.Foo(r.Context())
		if err != nil {
			code := http.StatusInternalServerError
			http.Error(w, http.StatusText(code), code)
			return
		}
		_, _ = fmt.Fprint(w, reply)
	})
	return mux
}

// package httpapi_test

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

	api := s.Let(`api`, func(t *testcase.T) interface{} {
		return NewAPI(ExampleMyDomainUseCaseGet(t))
	})
	apiGet := func(t *testcase.T) *http.ServeMux { return api.Get(t).(*http.ServeMux) }

	s.Describe(`GET /foo`, func(s *testcase.Spec) {
		subject := func(t *testcase.T) *httptest.ResponseRecorder {
			w := httptest.NewRecorder()
			r := httptest.NewRequest(http.MethodGet, `/`, nil)
			apiGet(t).ServeHTTP(w, r)
			return w
		}

		s.Then(`it will reply with baz`, func(t *testcase.T) {
			require.Contains(t, `baz`, subject(t).Body.String())
		})
	})
}
Output:

func (Var) EagerLoading added in v0.16.0

func (v Var) EagerLoading(s *Spec) Var

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

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

Example
package main

import (
	"github.com/adamluzsi/testcase"
	"testing"
)

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

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

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

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

func (Var) Get added in v0.16.0

func (v Var) Get(t *T) (T interface{})

Get returns the current cached value of the given Variable When Go2 released, it will replace type casting

Example
package main

import (
	"github.com/adamluzsi/testcase"
	"testing"
)

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

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

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

func (Var) Let added in v0.16.0

func (v Var) Let(s *Spec, blk letBlock) Var

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

Example
package main

import (
	"github.com/adamluzsi/testcase"
	"testing"
)

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

	value := testcase.Var{
		Name: `the variable group`,
		Init: func(t *testcase.T) interface{} {
			return 42
		},
	}

	value.Let(s, nil)

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

Example (EagerLoading)
package main

import (
	"github.com/adamluzsi/testcase"
	"testing"
)

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

	value := testcase.Var{Name: `value`}

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

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

Example (ValueDefinedAtTestingContextScope)
package main

import (
	"github.com/adamluzsi/testcase"
	"testing"
)

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

	value := testcase.Var{Name: `the variable group`}

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

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

func (Var) LetValue added in v0.16.0

func (v Var) LetValue(s *Spec, value interface{}) Var

LetValue set the value of the variable to a given block

Example
package main

import (
	"github.com/adamluzsi/testcase"
	"testing"
)

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

	value := testcase.Var{Name: `the variable group`}

	value.LetValue(s, 42)

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

Example (EagerLoading)
package main

import (
	"github.com/adamluzsi/testcase"
	"testing"
)

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

	value := testcase.Var{Name: `value`}
	value.LetValue(s, 42).EagerLoading(s)

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

func (Var) Set added in v0.16.0

func (v Var) Set(t *T, value interface{})

Set sets a value to a given variable during testCase runtime

Example
package main

import (
	"github.com/adamluzsi/testcase"
	"testing"
)

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

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

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

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

type Waiter added in v0.15.0

type Waiter struct {
	WaitDuration time.Duration
	WaitTimeout  time.Duration
}

Waiter is a component that waits for a time, event, or opportunity.

func (Waiter) Wait added in v0.15.0

func (w Waiter) Wait()

Wait will attempt to wait a bit and leave breathing space for other goroutines to steal processing time. It will also attempt to schedule other goroutines.

Example
package main

import (
	"time"

	"github.com/adamluzsi/testcase"
)

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

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

func (Waiter) While added in v0.25.0

func (w Waiter) While(condition func() bool)

While will wait until a condition met, or until the wait timeout. By default, if the timeout is not defined, it just attempts to execute the condition once. Calling multiple times the condition function should be a safe operation.

Example
package main

import (
	"math/rand"
	"time"

	"github.com/adamluzsi/testcase"
)

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

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

Directories

Path Synopsis
docs

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL