Skip to content

Commit

Permalink
support Suite interface with testcase.Spec
Browse files Browse the repository at this point in the history
This allows the users to create a suite from a function without boilerplate code.
  • Loading branch information
adamluzsi committed May 19, 2023
1 parent 272c6b1 commit e08faa8
Show file tree
Hide file tree
Showing 6 changed files with 268 additions and 14 deletions.
63 changes: 57 additions & 6 deletions Spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

// NewSpec create new Spec struct that is ready for usage.
func NewSpec(tb testing.TB, opts ...SpecOption) *Spec {
tb = ensureTB(tb)
tb.Helper()
var s *Spec
switch tb := tb.(type) {
Expand All @@ -25,7 +26,6 @@ func NewSpec(tb testing.TB, opts ...SpecOption) *Spec {
s.seed = seedForSpec(tb)
s.orderer = newOrderer(tb, s.seed)
s.sync = true
//tb.Cleanup(s.Finish)
}
applyGlobal(s)
return s
Expand All @@ -35,6 +35,7 @@ func newSpec(tb testing.TB, opts ...SpecOption) *Spec {
tb.Helper()
s := &Spec{
testingTB: tb,
opts: opts,
vars: newVariables(),
immutable: false,
}
Expand Down Expand Up @@ -72,6 +73,8 @@ func (spec *Spec) newSubSpec(desc string, opts ...SpecOption) *Spec {
type Spec struct {
testingTB testing.TB

opts []SpecOption

parent *Spec
children []*Spec

Expand All @@ -80,6 +83,8 @@ type Spec struct {
BeforeAll []hookOnce
}

defs []func(*Spec)

immutable bool
vars *variables
parallel bool
Expand All @@ -91,11 +96,13 @@ type Spec struct {
description string
tags []string
tests []func()
finished bool
orderer orderer
seed int64
isTest bool
sync bool

finished bool
orderer orderer
seed int64
isTest bool
sync bool
isSuite bool
}

type (
Expand All @@ -118,6 +125,12 @@ type (
// 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.
func (spec *Spec) Context(desc string, testContextBlock sBlock, opts ...SpecOption) {
spec.defs = append(spec.defs, func(oth *Spec) {
oth.Context(desc, testContextBlock, opts...)
})
if spec.getIsSuite() {
return
}
spec.testingTB.Helper()
sub := spec.newSubSpec(desc, opts...)
if spec.sync {
Expand Down Expand Up @@ -169,6 +182,12 @@ func (spec *Spec) Context(desc string, testContextBlock sBlock, opts ...SpecOpti
// It should not contain anything that modify the test subject input.
// It should focus only on asserting the result of the subject.
func (spec *Spec) Test(desc string, test tBlock, opts ...SpecOption) {
spec.defs = append(spec.defs, func(oth *Spec) {
oth.Test(desc, test, opts...)
})
if spec.getIsSuite() {
return
}
spec.testingTB.Helper()
s := spec.newSubSpec(desc, opts...)
s.isTest = true
Expand Down Expand Up @@ -571,6 +590,7 @@ func (spec *Spec) getTagSet() map[string]struct{} {

func (spec *Spec) addTest(blk func()) {
spec.testingTB.Helper()

if p, ok := spec.lookupParent(); ok && p.sync {
blk()
} else {
Expand All @@ -591,3 +611,34 @@ func escapeName(s string) string {
}
return s
}

func (spec *Spec) Spec(oth *Spec) {
if !spec.isSuite {
panic("Spec method is only allowed when testcase.Spec made with AsSuite option")
}
oth.testingTB.Helper()
isSuite := oth.isSuite
for _, opt := range spec.opts {
opt.setup(oth)
}
oth.isSuite = isSuite
for _, def := range spec.defs {
def(oth)
}
}

func (spec *Spec) getIsSuite() bool {
for _, s := range spec.specsFromCurrent() {
if s.isSuite {
return true
}
}
return false
}

func ensureTB(tb testing.TB) testing.TB {
if tb == nil {
return internal.SuiteNullTB{}
}
return tb
}
85 changes: 85 additions & 0 deletions Spec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1390,3 +1390,88 @@ func TestSpec_spike(t *testing.T) {
})

}

func TestAsSuite(t *testing.T) {
t.Run("runs only when Spec method is called", func(t *testing.T) {
s := testcase.NewSpec(nil, testcase.AsSuite())
s.Sequential()

var states []string

s.Test("A", func(t *testcase.T) {
states = append(states, "A")
})

s.Context("1", func(s *testcase.Spec) {
s.Test("B", func(t *testcase.T) {
states = append(states, "B")
})

s.Context("2", func(s *testcase.Spec) {
s.Test("C", func(t *testcase.T) {
states = append(states, "C")
})
})
})

// if a Spec is a Suite, then it is not executed by default
assert.Empty(t, states)

// when Spec is called, then it will execute
s.Spec(testcase.NewSpec(t))

// then execution is expected
assert.ContainExactly(t, []string{"A", "B", "C"}, states)
})
t.Run("the only the passed testcase.Spec's testing.TB will be used during failure", func(t *testing.T) {
ogTB := &doubles.TB{}
s := testcase.NewSpec(ogTB, testcase.AsSuite())
s.Test("A", func(t *testcase.T) {
t.Fail()
})

// when Spec is called, then it will execute
dtb := &doubles.TB{}
s.Spec(testcase.NewSpec(dtb))

assert.False(t, ogTB.Failed(), "it was not expected that the testing.TB failed")
assert.True(t, dtb.IsFailed)
})
t.Run("options passed down to the target spec", func(t *testing.T) {
s := testcase.NewSpec(nil, testcase.AsSuite(), testcase.Flaky(42))

var once sync.Once
s.Test("", func(t *testcase.T) {
once.Do(func() { t.Fail() })
})

dtb := &doubles.TB{}
s.Spec(testcase.NewSpec(dtb))

assert.False(t, dtb.IsFailed, "flaky flag should have saved the day")
})
t.Run("mounting a Suite into another Suite should still not execute", func(t *testing.T) {
var ran bool
s1 := testcase.NewSpec(nil, testcase.AsSuite())
s1.Test("", func(t *testcase.T) { ran = true })

s2 := testcase.NewSpec(nil, testcase.AsSuite())
s1.Spec(s2) // s1 merge into s2

assert.False(t, ran)

dtb := &doubles.TB{}
s3 := testcase.NewSpec(dtb)
s2.Spec(s3) // execute

assert.True(t, ran)
})

t.Run("when Spec.Spec is called on non Suite Spec", func(t *testing.T) {
dtb := &doubles.TB{}
s := testcase.NewSpec(dtb)
assert.Panic(t, func() {
s.Spec(testcase.NewSpec(dtb))
})
})
}
8 changes: 8 additions & 0 deletions Suite.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ import (
"github.com/adamluzsi/testcase/internal"
)

// AsSuite will flag the Spec as a Suite.
// Calling AsSuite will delay test until the Spec.Spec function is called
func AsSuite() SpecOption {
return specOptionFunc(func(s *Spec) {
s.isSuite = true
})
}

// Suite meant to represent a testing suite.
// A test Suite is a collection of test cases.
// In a test suite, the test cases are organized in a logical order.
Expand Down
16 changes: 15 additions & 1 deletion examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1499,7 +1499,7 @@ func ExampleT_LogPretty() {
// }
}

func ExampleGlobal_Before() {
func Example_global_Before() {
testcase.Global.Before(func(t *testcase.T) {
t.Log("each Spec configured with this")
})
Expand All @@ -1511,3 +1511,17 @@ func ExampleGlobal_Before() {
// includes configuration from global config
})
}

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

func ExampleAsSuite() {
var tb testing.TB
s := testcase.NewSpec(tb)
s.Context("my example testing suite", exampleSuite().Spec)
}
94 changes: 94 additions & 0 deletions internal/suite.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package internal

import (
"testing"
)

type SuiteNullTB struct{ testing.TB }

func (n SuiteNullTB) Helper() {}

func (n SuiteNullTB) Cleanup(f func()) {
//TODO implement me
panic("implement me")
}

func (n SuiteNullTB) Error(args ...any) {
//TODO implement me
panic("implement me")
}

func (n SuiteNullTB) Errorf(format string, args ...any) {
//TODO implement me
panic("implement me")
}

func (n SuiteNullTB) Fail() {
//TODO implement me
panic("implement me")
}

func (n SuiteNullTB) FailNow() {
//TODO implement me
panic("implement me")
}

func (n SuiteNullTB) Failed() bool {
//TODO implement me
panic("implement me")
}

func (n SuiteNullTB) Fatal(args ...any) {
//TODO implement me
panic("implement me")
}

func (n SuiteNullTB) Fatalf(format string, args ...any) {
//TODO implement me
panic("implement me")
}

func (n SuiteNullTB) Log(args ...any) {
//TODO implement me
panic("implement me")
}

func (n SuiteNullTB) Logf(format string, args ...any) {
//TODO implement me
panic("implement me")
}

func (n SuiteNullTB) Name() string {
//TODO implement me
panic("implement me")
}

func (n SuiteNullTB) Setenv(key, value string) {
//TODO implement me
panic("implement me")
}

func (n SuiteNullTB) Skip(args ...any) {
//TODO implement me
panic("implement me")
}

func (n SuiteNullTB) SkipNow() {
//TODO implement me
panic("implement me")
}

func (n SuiteNullTB) Skipf(format string, args ...any) {
//TODO implement me
panic("implement me")
}

func (n SuiteNullTB) Skipped() bool {
//TODO implement me
panic("implement me")
}

func (n SuiteNullTB) TempDir() string {
//TODO implement me
panic("implement me")
}
16 changes: 9 additions & 7 deletions seed.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@ func makeSeed() (int64, error) {

func seedForSpec(tb testing.TB) (_seed int64) {
tb.Helper()
tb.Cleanup(func() {
tb.Helper()
if tb.Failed() {
// Help developers to know the seed of the failed test execution.
internal.Log(tb, fmt.Sprintf(`%s=%d`, EnvKeySeed, _seed))
}
})
if tb != (internal.SuiteNullTB{}) {
tb.Cleanup(func() {
tb.Helper()
if tb.Failed() {
// Help developers to know the seed of the failed test execution.
internal.Log(tb, fmt.Sprintf(`%s=%d`, EnvKeySeed, _seed))
}
})
}
seed, err := makeSeed()
if err != nil {
tb.Fatal(err.Error())
Expand Down

0 comments on commit e08faa8

Please sign in to comment.