Skip to content

Commit a7b5823

Browse files
authored
Merge pull request #4461 from hashicorp/f-e2e-framework
E2E: Initial Framework
2 parents e6e7966 + 134a0c4 commit a7b5823

10 files changed

+665
-0
lines changed

e2e/e2e.go

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package e2e
2+
3+
import (
4+
"testing"
5+
6+
"github.com/hashicorp/nomad/e2e/framework"
7+
)
8+
9+
func RunE2ETests(t *testing.T) {
10+
framework.Run(t)
11+
}

e2e/e2e_test.go

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package e2e
2+
3+
import (
4+
"testing"
5+
6+
_ "github.com/hashicorp/nomad/e2e/example"
7+
)
8+
9+
func TestE2E(t *testing.T) {
10+
RunE2ETests(t)
11+
}

e2e/example/e2e_test.go

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package example
2+
3+
import (
4+
"testing"
5+
6+
"github.com/hashicorp/nomad/e2e/framework"
7+
)
8+
9+
func TestE2E(t *testing.T) {
10+
framework.New().AddSuites(&framework.TestSuite{
11+
Component: "simple",
12+
CanRunLocal: true,
13+
Cases: []framework.TestCase{
14+
new(SimpleExampleTestCase),
15+
},
16+
}).Run(t)
17+
}

e2e/example/example.go

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package example
2+
3+
import (
4+
"github.com/hashicorp/nomad/e2e/framework"
5+
)
6+
7+
func init() {
8+
framework.AddSuites(&framework.TestSuite{
9+
Component: "simple",
10+
CanRunLocal: true,
11+
Cases: []framework.TestCase{
12+
new(SimpleExampleTestCase),
13+
},
14+
})
15+
}
16+
17+
type SimpleExampleTestCase struct {
18+
framework.TC
19+
}
20+
21+
func (tc *SimpleExampleTestCase) TestExample(f *framework.F) {
22+
jobs, _, err := tc.Nomad().Jobs().List(nil)
23+
f.NoError(err)
24+
f.Empty(jobs)
25+
}

e2e/framework/case.go

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package framework
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/hashicorp/nomad/api"
8+
)
9+
10+
// TestSuite defines a set of test cases and under what conditions to run them
11+
type TestSuite struct {
12+
Component string // Name of the component/system/feature tested
13+
14+
CanRunLocal bool // Flags if the cases are safe to run on a local nomad cluster
15+
Cases []TestCase // Cases to run
16+
Constraints Constraints // Environment constraints to follow
17+
Parallel bool // If true, will run test cases in parallel
18+
Slow bool // Slow test suites don't run by default
19+
20+
// API Clients
21+
Consul bool
22+
Vault bool
23+
}
24+
25+
// Constraints that must be satisfied for a TestSuite to run
26+
type Constraints struct {
27+
Provider string // Cloud provider ex. 'aws', 'azure', 'gcp'
28+
OS string // Operating system ex. 'windows', 'linux'
29+
Arch string // CPU architecture ex. 'amd64', 'arm64'
30+
Environment string // Environment name ex. 'simple'
31+
Tags []string // Generic tags that must all exist in the environment
32+
}
33+
34+
func (c Constraints) matches(env Environment) error {
35+
if len(c.Provider) != 0 && c.Provider != env.Provider {
36+
return fmt.Errorf("provider constraint does not match environment")
37+
}
38+
39+
if len(c.OS) != 0 && c.OS != env.OS {
40+
return fmt.Errorf("os constraint does not match environment")
41+
}
42+
43+
if len(c.Arch) != 0 && c.Arch != env.Arch {
44+
return fmt.Errorf("arch constraint does not match environment")
45+
}
46+
47+
if len(c.Environment) != 0 && c.Environment != env.Name {
48+
return fmt.Errorf("environment constraint does not match environment name")
49+
}
50+
51+
for _, t := range c.Tags {
52+
if _, ok := env.Tags[t]; !ok {
53+
return fmt.Errorf("tags constraint failed, tag '%s' is not included in environment", t)
54+
}
55+
}
56+
return nil
57+
}
58+
59+
// TC is the base test case which should be embedded in TestCase implementations.
60+
type TC struct {
61+
t *testing.T
62+
63+
cluster *ClusterInfo
64+
}
65+
66+
// Nomad returns a configured nomad api client
67+
func (tc *TC) Nomad() *api.Client {
68+
return tc.cluster.NomadClient
69+
}
70+
71+
// Name returns the name of the test case which is set to the name of the
72+
// implementing type.
73+
func (tc *TC) Name() string {
74+
return tc.cluster.Name
75+
}
76+
77+
func (tc *TC) setClusterInfo(info *ClusterInfo) {
78+
tc.cluster = info
79+
}

e2e/framework/context.go

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package framework
2+
3+
import (
4+
"testing"
5+
6+
"github.com/hashicorp/nomad/helper/uuid"
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
// F is the framework context that is passed to each test.
12+
// It is used to access the *testing.T context as well as testify helpers
13+
type F struct {
14+
id string
15+
*require.Assertions
16+
assert *assert.Assertions
17+
t *testing.T
18+
19+
data map[interface{}]interface{}
20+
}
21+
22+
func newF(t *testing.T) *F {
23+
return newFWithID(uuid.Generate()[:8], t)
24+
}
25+
26+
func newFFromParent(f *F, t *testing.T) *F {
27+
child := newF(t)
28+
for k, v := range f.data {
29+
child.Set(k, v)
30+
}
31+
return child
32+
}
33+
34+
func newFWithID(id string, t *testing.T) *F {
35+
ft := &F{
36+
id: id,
37+
t: t,
38+
Assertions: require.New(t),
39+
assert: assert.New(t),
40+
}
41+
42+
return ft
43+
}
44+
45+
// Assert fetches an assert flavor of testify assertions
46+
// https://godoc.org/github.com/stretchr/testify/assert
47+
func (f *F) Assert() *assert.Assertions {
48+
return f.assert
49+
}
50+
51+
// T returns the *testing.T context
52+
func (f *F) T() *testing.T {
53+
return f.t
54+
}
55+
56+
// ID returns the current context ID
57+
func (f *F) ID() string {
58+
return f.id
59+
}
60+
61+
// Set is used to set arbitrary key/values to pass between before/after and test methods
62+
func (f *F) Set(key, val interface{}) {
63+
f.data[key] = val
64+
}
65+
66+
// Value retrives values set by the F.Set method
67+
func (f *F) Value(key interface{}) interface{} {
68+
return f.data[key]
69+
}

e2e/framework/doc.go

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
Package framework implements a model for developing end-to-end test suites. The
3+
model includes a top level Framework which TestSuites can be added to. TestSuites
4+
include conditions under which the suite will run and a list of TestCase
5+
implementations to run. TestCases can be implemented with methods that run
6+
before/after each and all tests.
7+
8+
Writing Tests
9+
10+
Tests follow a similar patterns as go tests. They are functions that must start
11+
with 'Test' and instead of a *testing.T argument, a *framework.F is passed and
12+
they must have a receiver that implements the TestCase interface.
13+
A crude example as follows:
14+
15+
// foo_test.go
16+
type MyTestCase struct {
17+
framework.TC
18+
}
19+
20+
func (tc *MyTestCase) TestMyFoo(f *framework.F) {
21+
f.T().Log("bar")
22+
}
23+
24+
func TestCalledFromGoTest(t *testing.T){
25+
framework.New().AddSuites(&framework.TestSuite{
26+
Component: "foo",
27+
Cases: []framework.TestCase{
28+
new(MyTestCase),
29+
},
30+
}).Run(t)
31+
}
32+
33+
Test cases should embed the TC struct which satisfies the TestCase interface.
34+
Optionally a TestCase can also implement the Name() function which returns
35+
a string to name the test case. By default the name is the name of the struct
36+
type, which in the above example would be "MyTestCase"
37+
38+
Test cases may also optionally implement additional interfaces to define setup
39+
and teardown logic:
40+
41+
BeforeEachTest
42+
AfterEachTest
43+
BeforeAllTests
44+
AfterAllTests
45+
46+
The test case struct allows you to setup and teardown state in the struct that
47+
can be consumed by the tests. For example:
48+
49+
type ComplexNomadTC struct {
50+
framework.TC
51+
jobID string
52+
}
53+
54+
func (tc *ComplexNomadTC) BeforeEach(f *framework.F){
55+
// Do some complex job setup with a unique prefix string
56+
jobID, err := doSomeComplexSetup(tc.Nomad(), f.ID())
57+
f.NoError(err)
58+
f.Set("jobID", jobID)
59+
}
60+
61+
func (tc *ComplexNomadTC) TestSomeScenario(f *framework.F){
62+
jobID := f.Value("jobID").(string)
63+
doTestThingWithJob(f, tc.Nomad(), jobID)
64+
}
65+
66+
func (tc *ComplexNomadTC) TestOtherScenario(f *framework.F){
67+
jobID := f.Value("jobID").(string)
68+
doOtherTestThingWithJob(f, tc.Nomad(), jobID)
69+
}
70+
71+
func (tc *ComplexNomadTC) AfterEach(f *framework.F){
72+
jobID := f.Value("jobID").(string)
73+
_, _, err := tc.Nomad().Jobs().Deregister(jobID, true, nil)
74+
f.NoError(err)
75+
}
76+
77+
As demonstrated in the previous example, TC also exposes functions that return
78+
configured api clients including Nomad, Consul and Vault. If Consul or Vault
79+
are not provisioned their respective getter functions will return nil.
80+
81+
Testify Integration
82+
83+
Test cases expose a T() function to fetch the current *testing.T context.
84+
While this means the author is able to most other testing libraries,
85+
github.com/stretch/testify is recommended and integrated into the framework.
86+
The TC struct also embeds testify assertions that are preconfigured with the
87+
current testing context. Additionally TC comes with a Require() method that
88+
yields a testify Require if that flavor is desired.
89+
90+
func (tc *MyTestCase) TestWithTestify() {
91+
err := someErrFunc()
92+
tc.NoError(err)
93+
// Or tc.Require().NoError(err)
94+
}
95+
96+
Parallelism
97+
98+
The test framework honors go test's parallel feature under certain conditions.
99+
A TestSuite can be created with the Parallel field set to true to enable
100+
parallel execution of the test cases of the suite. Tests within a test case
101+
will be executed sequentially unless f.T().Parallel() is called. Note that if
102+
multiple tests are to be executed in parallel, access to TC is not syncronized.
103+
The *framework.F offers a way to store state between before/after each method if
104+
desired.
105+
106+
func (tc *MyTestCase) BeforeEach(f *framework.F){
107+
jobID, _ := doSomeComplexSetup(tc.Nomad(), f.ID())
108+
f.Set("jobID", jobID)
109+
}
110+
111+
func (tc *MyTestCase) TestParallel(f *framework.F){
112+
f.T().Parallel()
113+
jobID := f.Value("jobID").(string)
114+
}
115+
116+
Since test cases have the potential to work with a shared Nomad cluster in parallel
117+
any resources created or destroyed must be prefixed with a unique identifier for
118+
each test case. The framework.F struct exposes an ID() function that will return a
119+
string that is unique with in a test. Therefore, multiple tests with in the case
120+
can reliably create unique IDs between tests and setup/teardown. The string
121+
returned is 8 alpha numeric characters.
122+
123+
*/
124+
package framework

0 commit comments

Comments
 (0)