Use shared envTest for unit tests

Speed up unit tests by using a shared envTest. This requires each
test to use its own namespace to avoid clobbering objects for
other tests. Tests previously took around 8 seconds each, and now
the initial test takes 2 seconds with follow up tests taking less
than a second each.

Also update existing tests that use a fixed namespace to use a
generated namespace.

Share gold file template function with yaml files.

Remove the testClusterMode, and instead rely on MainTest to do
the appropriate test setup and rootArgs flag setup. Move the
rootArg flag setup out of NewTestEnvKubeManager to avoid
side effects.

A follow up change can be to push the individual setups
from NewTestEnvKubeManager() into their respective TestMain since
the harness share little code.

Signed-off-by: Allen Porter <allen@thebends.org>
pull/1743/head
Allen Porter 3 years ago
parent def92e14ee
commit d45501a129

@ -77,6 +77,16 @@ You can run the unit tests by simply doing
make test
```
The e2e test suite uses [kind](https://kind.sigs.k8s.io/) for running kubernetes cluster inside docker containers. You can run the e2e tests by simply doing
```bash
make setup-kind
make e2e
# When done
make cleanup-kind
```
## Acceptance policy
These things will make a PR more likely to be accepted:

@ -27,8 +27,7 @@ func TestCheckPre(t *testing.T) {
serverVersion := strings.TrimPrefix(versions["serverVersion"].GitVersion, "v")
cmd := cmdTestCase{
args: "check --pre",
testClusterMode: ExistingClusterMode,
args: "check --pre",
assert: assertGoldenTemplateFile("testdata/check/check_pre.golden", map[string]string{
"clientVersion": clientVersion,
"serverVersion": serverVersion,

@ -39,7 +39,7 @@ func TestHelmReleaseFromGit(t *testing.T) {
},
}
namespace := "thrfg"
namespace := allocateNamespace("thrfg")
del, err := setupTestNamespace(namespace)
if err != nil {
t.Fatal(err)
@ -48,9 +48,8 @@ func TestHelmReleaseFromGit(t *testing.T) {
for _, tc := range cases {
cmd := cmdTestCase{
args: tc.args + " -n=" + namespace,
assert: assertGoldenFile(tc.goldenFile),
testClusterMode: ExistingClusterMode,
args: tc.args + " -n=" + namespace,
assert: assertGoldenTemplateFile(tc.goldenFile, map[string]string{"ns": namespace}),
}
cmd.runTestCmd(t)
}

@ -31,7 +31,7 @@ func TestImageScanning(t *testing.T) {
},
}
namespace := "tis"
namespace := allocateNamespace("tis")
del, err := setupTestNamespace(namespace)
if err != nil {
t.Fatal(err)
@ -40,9 +40,8 @@ func TestImageScanning(t *testing.T) {
for _, tc := range cases {
cmd := cmdTestCase{
args: tc.args + " -n=" + namespace,
assert: assertGoldenFile(tc.goldenFile),
testClusterMode: ExistingClusterMode,
args: tc.args + " -n=" + namespace,
assert: assertGoldenFile(tc.goldenFile),
}
cmd.runTestCmd(t)
}

@ -39,7 +39,7 @@ func TestKustomizationFromGit(t *testing.T) {
},
}
namespace := "tkfg"
namespace := allocateNamespace("tkfg")
del, err := setupTestNamespace(namespace)
if err != nil {
t.Fatal(err)
@ -48,9 +48,8 @@ func TestKustomizationFromGit(t *testing.T) {
for _, tc := range cases {
cmd := cmdTestCase{
args: tc.args + " -n=" + namespace,
assert: assertGoldenFile(tc.goldenFile),
testClusterMode: ExistingClusterMode,
args: tc.args + " -n=" + namespace,
assert: assertGoldenTemplateFile(tc.goldenFile, map[string]string{"ns": namespace}),
}
cmd.runTestCmd(t)
}

@ -15,12 +15,13 @@ func TestMain(m *testing.M) {
// Ensure tests print consistent timestamps regardless of timezone
os.Setenv("TZ", "UTC")
// Install Flux.
// Creating the test env manager sets rootArgs client flags
_, err := NewTestEnvKubeManager(ExistingClusterMode)
testEnv, err := NewTestEnvKubeManager(ExistingClusterMode)
if err != nil {
panic(fmt.Errorf("error creating kube manager: '%w'", err))
}
rootArgs.kubeconfig = testEnv.kubeConfigPath
// Install Flux.
output, err := executeCommand("install --components-extra=image-reflector-controller,image-automation-controller")
if err != nil {
panic(fmt.Errorf("install falied: %s error:'%w'", output, err))
@ -42,6 +43,8 @@ func TestMain(m *testing.M) {
panic(fmt.Errorf("delete namespace error:'%w'", err))
}
testEnv.Stop()
os.Exit(code)
}

@ -9,6 +9,8 @@ import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"sync/atomic"
"testing"
"text/template"
"time"
@ -22,13 +24,18 @@ import (
"sigs.k8s.io/controller-runtime/pkg/envtest"
)
func readYamlObjects(objectFile string) ([]unstructured.Unstructured, error) {
obj, err := os.ReadFile(objectFile)
if err != nil {
return nil, err
}
var nextNamespaceId int64
// Return a unique namespace with the specified prefix, for tests to create
// objects that won't collide with each other.
func allocateNamespace(prefix string) string {
id := atomic.AddInt64(&nextNamespaceId, 1)
return fmt.Sprintf("%s-%d", prefix, id)
}
func readYamlObjects(rdr io.Reader) ([]unstructured.Unstructured, error) {
objects := []unstructured.Unstructured{}
reader := k8syaml.NewYAMLReader(bufio.NewReader(bytes.NewReader(obj)))
reader := k8syaml.NewYAMLReader(bufio.NewReader(rdr))
for {
doc, err := reader.Read()
if err != nil {
@ -49,15 +56,31 @@ func readYamlObjects(objectFile string) ([]unstructured.Unstructured, error) {
// A KubeManager that can create objects that are subject to a test.
type testEnvKubeManager struct {
client client.WithWatch
testEnv *envtest.Environment
client client.WithWatch
testEnv *envtest.Environment
kubeConfigPath string
}
func (m *testEnvKubeManager) NewClient(kubeconfig string, kubecontext string) (client.WithWatch, error) {
return m.client, nil
func (m *testEnvKubeManager) CreateObjectFile(objectFile string, templateValues map[string]string, t *testing.T) {
buf, err := os.ReadFile(objectFile)
if err != nil {
t.Fatalf("Error reading file '%s': %v", objectFile, err)
}
content, err := executeTemplate(string(buf), templateValues)
if err != nil {
t.Fatalf("Error evaluating template file '%s': '%v'", objectFile, err)
}
clientObjects, err := readYamlObjects(strings.NewReader(content))
if err != nil {
t.Fatalf("Error decoding yaml file '%s': %v", objectFile, err)
}
err = m.CreateObjects(clientObjects, t)
if err != nil {
t.Logf("Error creating test objects: '%v'", err)
}
}
func (m *testEnvKubeManager) CreateObjects(clientObjects []unstructured.Unstructured) error {
func (m *testEnvKubeManager) CreateObjects(clientObjects []unstructured.Unstructured, t *testing.T) error {
for _, obj := range clientObjects {
// First create the object then set its status if present in the
// yaml file. Make a copy first since creating an object may overwrite
@ -110,14 +133,14 @@ func NewTestEnvKubeManager(testClusterMode TestClusterMode) (*testEnvKubeManager
tmpFilename := filepath.Join("/tmp", "kubeconfig-"+time.Nanosecond.String())
ioutil.WriteFile(tmpFilename, kubeConfig, 0644)
rootArgs.kubeconfig = tmpFilename
k8sClient, err := client.NewWithWatch(cfg, client.Options{})
if err != nil {
return nil, err
}
return &testEnvKubeManager{
testEnv: testEnv,
client: k8sClient,
testEnv: testEnv,
client: k8sClient,
kubeConfigPath: tmpFilename,
}, nil
case ExistingClusterMode:
// TEST_KUBECONFIG is mandatory to prevent destroying a current cluster accidentally.
@ -125,7 +148,6 @@ func NewTestEnvKubeManager(testClusterMode TestClusterMode) (*testEnvKubeManager
if testKubeConfig == "" {
return nil, fmt.Errorf("environment variable TEST_KUBECONFIG is required to run tests against an existing cluster")
}
rootArgs.kubeconfig = testKubeConfig
useExistingCluster := true
config, err := clientcmd.BuildConfigFromFlags("", testKubeConfig)
@ -142,8 +164,9 @@ func NewTestEnvKubeManager(testClusterMode TestClusterMode) (*testEnvKubeManager
return nil, err
}
return &testEnvKubeManager{
testEnv: testEnv,
client: k8sClient,
testEnv: testEnv,
client: k8sClient,
kubeConfigPath: testKubeConfig,
}, nil
}
@ -217,7 +240,7 @@ func assertGoldenTemplateFile(goldenFile string, templateValues map[string]strin
}
var expectedOutput string
if len(templateValues) > 0 {
expectedOutput, err = executeGoldenTemplate(string(goldenFileContents), templateValues)
expectedOutput, err = executeTemplate(string(goldenFileContents), templateValues)
if err != nil {
return fmt.Errorf("Error executing golden template file '%s': %s", goldenFile, err)
}
@ -243,8 +266,6 @@ const (
type cmdTestCase struct {
// The command line arguments to test.
args string
// TestClusterMode to bootstrap and testing, default to Fake
testClusterMode TestClusterMode
// Tests use assertFunc to assert on an output, success or failure. This
// can be a function defined by the test or existing function above.
assert assertFunc
@ -253,34 +274,14 @@ type cmdTestCase struct {
}
func (cmd *cmdTestCase) runTestCmd(t *testing.T) {
km, err := NewTestEnvKubeManager(cmd.testClusterMode)
if err != nil {
t.Fatalf("Error creating kube manager: '%v'", err)
}
if km != nil {
defer km.Stop()
}
if cmd.objectFile != "" {
clientObjects, err := readYamlObjects(cmd.objectFile)
if err != nil {
t.Fatalf("Error loading yaml: '%v'", err)
}
err = km.CreateObjects(clientObjects)
if err != nil {
t.Fatalf("Error creating test objects: '%v'", err)
}
}
actual, testErr := executeCommand(cmd.args)
if assertErr := cmd.assert(actual, testErr); assertErr != nil {
t.Error(assertErr)
}
}
func executeGoldenTemplate(goldenValue string, templateValues map[string]string) (string, error) {
tmpl := template.Must(template.New("golden").Parse(goldenValue))
func executeTemplate(content string, templateValues map[string]string) (string, error) {
tmpl := template.Must(template.New("golden").Parse(content))
var out bytes.Buffer
if err := tmpl.Execute(&out, templateValues); err != nil {
return "", err

@ -3,12 +3,32 @@
package main
import (
"fmt"
"os"
"testing"
)
// The test environment is long running process shared between tests, initialized
// by a `TestMain` function depending on how the test is involved and which tests
// are a part of the build.
var testEnv *testEnvKubeManager
func TestMain(m *testing.M) {
// Ensure tests print consistent timestamps regardless of timezone
os.Setenv("TZ", "UTC")
os.Exit(m.Run())
// Creating the test env manager sets rootArgs client flags
km, err := NewTestEnvKubeManager(TestEnvClusterMode)
if err != nil {
panic(fmt.Errorf("error creating kube manager: '%w'", err))
}
testEnv = km
rootArgs.kubeconfig = testEnv.kubeConfigPath
// Run tests
code := m.Run()
km.Stop()
os.Exit(code)
}

@ -1,2 +1,2 @@
► deleting helmreleases thrfg in thrfg namespace
► deleting helmreleases thrfg in {{ .ns }} namespace
✔ helmreleases deleted

@ -1,9 +1,9 @@
► annotating GitRepository thrfg in thrfg namespace
► annotating GitRepository thrfg in {{ .ns }} namespace
✔ GitRepository annotated
◎ waiting for GitRepository reconciliation
✔ GitRepository reconciliation completed
✔ fetched revision 6.0.0/627d5c4bb67b77185f37e31d734b085019ff2951
► annotating HelmRelease thrfg in thrfg namespace
► annotating HelmRelease thrfg in {{ .ns }} namespace
✔ HelmRelease annotated
◎ waiting for HelmRelease reconciliation
✔ HelmRelease reconciliation completed

@ -1,4 +1,4 @@
► resuming helmreleases thrfg in thrfg namespace
► resuming helmreleases thrfg in {{ .ns }} namespace
✔ helmreleases resumed
◎ waiting for HelmRelease reconciliation
✔ HelmRelease reconciliation completed

@ -1,2 +1,2 @@
► suspending helmreleases thrfg in thrfg namespace
► suspending helmreleases thrfg in {{ .ns }} namespace
✔ helmreleases suspended

@ -1,2 +1,2 @@
► deleting kustomizations tkfg in tkfg namespace
► deleting kustomizations tkfg in {{ .ns }} namespace
✔ kustomizations deleted

@ -1,9 +1,9 @@
► annotating GitRepository tkfg in tkfg namespace
► annotating GitRepository tkfg in {{ .ns }} namespace
✔ GitRepository annotated
◎ waiting for GitRepository reconciliation
✔ GitRepository reconciliation completed
✔ fetched revision 6.0.0/627d5c4bb67b77185f37e31d734b085019ff2951
► annotating Kustomization tkfg in tkfg namespace
► annotating Kustomization tkfg in {{ .ns }} namespace
✔ Kustomization annotated
◎ waiting for Kustomization reconciliation
✔ Kustomization reconciliation completed

@ -1,4 +1,4 @@
► resuming kustomizations tkfg in tkfg namespace
► resuming kustomizations tkfg in {{ .ns }} namespace
✔ kustomizations resumed
◎ waiting for Kustomization reconciliation
✔ Kustomization reconciliation completed

@ -1,2 +1,2 @@
► suspending kustomizations tkfg in tkfg namespace
► suspending kustomizations tkfg in {{ .ns }} namespace
✔ kustomizations suspended

@ -1,16 +1,16 @@
Object: deployment/podinfo
Namespace: podinfo
Namespace: {{ .ns }}
Status: Managed by Flux
---
HelmRelease: podinfo
Namespace: podinfo
Namespace: {{ .ns }}
Revision: 6.0.0
Status: Last reconciled at 2021-07-16 15:42:20 +0000 UTC
Message: Release reconciliation succeeded
---
HelmChart: podinfo-podinfo
Namespace: flux-system
Namespace: {{ .fluxns }}
Chart: podinfo
Version: 6.0.0
Revision: 6.0.0
@ -18,7 +18,7 @@ Status: Last reconciled at 2021-07-16 15:32:09 +0000 UTC
Message: Fetched revision: 6.0.0
---
HelmRepository: podinfo
Namespace: flux-system
Namespace: {{ .fluxns }}
URL: https://stefanprodan.github.io/podinfo
Revision: 8411f23d07d3701f0e96e7d9e503b7936d7e1d56
Status: Last reconciled at 2021-07-11 00:25:46 +0000 UTC

@ -2,12 +2,12 @@
apiVersion: v1
kind: Namespace
metadata:
name: flux-system
name: {{ .fluxns }}
---
apiVersion: v1
kind: Namespace
metadata:
name: podinfo
name: {{ .ns }}
---
apiVersion: apps/v1
kind: Deployment
@ -15,11 +15,10 @@ metadata:
labels:
app.kubernetes.io/name: podinfo
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: podinfo
helm.toolkit.fluxcd.io/name: podinfo
helm.toolkit.fluxcd.io/namespace: podinfo
helm.toolkit.fluxcd.io/namespace: {{ .ns }}
name: podinfo
namespace: podinfo
namespace: {{ .ns }}
spec:
replicas: 1
selector:
@ -40,9 +39,9 @@ kind: HelmRelease
metadata:
labels:
kustomize.toolkit.fluxcd.io/name: infrastructure
kustomize.toolkit.fluxcd.io/namespace: flux-system
kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }}
name: podinfo
namespace: podinfo
namespace: {{ .ns }}
spec:
chart:
spec:
@ -50,7 +49,7 @@ spec:
sourceRef:
kind: HelmRepository
name: podinfo
namespace: flux-system
namespace: {{ .fluxns }}
interval: 5m
status:
conditions:
@ -59,7 +58,7 @@ status:
reason: ReconciliationSucceeded
status: "True"
type: Ready
helmChart: flux-system/podinfo-podinfo
helmChart: {{ .fluxns }}/podinfo-podinfo
lastAppliedRevision: 6.0.0
lastAttemptedRevision: 6.0.0
lastAttemptedValuesChecksum: c31db75d05b7515eba2eef47bd71038c74b2e531
@ -68,7 +67,7 @@ apiVersion: source.toolkit.fluxcd.io/v1beta1
kind: HelmChart
metadata:
name: podinfo-podinfo
namespace: flux-system
namespace: {{ .fluxns }}
spec:
chart: podinfo
sourceRef:
@ -98,7 +97,7 @@ metadata:
kustomize.toolkit.fluxcd.io/name: infrastructure
kustomize.toolkit.fluxcd.io/namespace: flux-system
name: podinfo
namespace: flux-system
namespace: {{ .fluxns }}
spec:
interval: 5m
timeout: 1m0s
@ -121,7 +120,7 @@ apiVersion: kustomize.toolkit.fluxcd.io/v1beta1
kind: Kustomization
metadata:
name: infrastructure
namespace: flux-system
namespace: {{ .fluxns }}
spec:
path: ./infrastructure/
sourceRef:
@ -143,9 +142,9 @@ kind: GitRepository
metadata:
labels:
kustomize.toolkit.fluxcd.io/name: flux-system
kustomize.toolkit.fluxcd.io/namespace: flux-system
kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }}
name: flux-system
namespace: flux-system
namespace: {{ .fluxns }}
spec:
gitImplementation: go-git
ref:

@ -1,17 +1,17 @@
Object: HelmRelease/podinfo
Namespace: podinfo
Namespace: {{ .ns }}
Status: Managed by Flux
---
Kustomization: infrastructure
Namespace: flux-system
Namespace: {{ .fluxns }}
Path: ./infrastructure
Revision: main/696f056df216eea4f9401adbee0ff744d4df390f
Status: Last reconciled at 2021-08-01 04:52:56 +0000 UTC
Message: Applied revision: main/696f056df216eea4f9401adbee0ff744d4df390f
---
GitRepository: flux-system
Namespace: flux-system
Namespace: {{ .fluxns }}
URL: ssh://git@github.com/example/repo
Branch: main
Revision: main/696f056df216eea4f9401adbee0ff744d4df390f

@ -2,21 +2,21 @@
apiVersion: v1
kind: Namespace
metadata:
name: flux-system
name: {{ .fluxns }}
---
apiVersion: v1
kind: Namespace
metadata:
name: podinfo
name: {{ .ns }}
---
apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
labels:
kustomize.toolkit.fluxcd.io/name: infrastructure
kustomize.toolkit.fluxcd.io/namespace: flux-system
kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }}
name: podinfo
namespace: podinfo
namespace: {{ .ns }}
spec:
chart:
spec:
@ -24,7 +24,7 @@ spec:
sourceRef:
kind: HelmRepository
name: podinfo
namespace: flux-system
namespace: {{ .fluxns }}
interval: 5m
status:
conditions:
@ -33,7 +33,7 @@ status:
reason: ReconciliationSucceeded
status: "True"
type: Ready
helmChart: flux-system/podinfo-podinfo
helmChart: {{ .fluxns }}/podinfo-podinfo
lastAppliedRevision: 6.0.0
lastAttemptedRevision: 6.0.0
lastAttemptedValuesChecksum: c31db75d05b7515eba2eef47bd71038c74b2e531
@ -42,7 +42,7 @@ apiVersion: kustomize.toolkit.fluxcd.io/v1beta1
kind: Kustomization
metadata:
name: infrastructure
namespace: flux-system
namespace: {{ .fluxns }}
spec:
path: ./infrastructure
sourceRef:
@ -65,9 +65,9 @@ kind: GitRepository
metadata:
labels:
kustomize.toolkit.fluxcd.io/name: flux-system
kustomize.toolkit.fluxcd.io/namespace: flux-system
kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }}
name: flux-system
namespace: flux-system
namespace: {{ .fluxns }}
spec:
gitImplementation: go-git
ref:

@ -8,29 +8,44 @@ import (
func TestTraceNoArgs(t *testing.T) {
cmd := cmdTestCase{
args: "trace",
testClusterMode: TestEnvClusterMode,
assert: assertError("object name is required"),
args: "trace",
assert: assertError("object name is required"),
}
cmd.runTestCmd(t)
}
func TestTraceDeployment(t *testing.T) {
cmd := cmdTestCase{
args: "trace podinfo -n podinfo --kind deployment --api-version=apps/v1",
testClusterMode: TestEnvClusterMode,
assert: assertGoldenFile("testdata/trace/deployment.golden"),
objectFile: "testdata/trace/deployment.yaml",
func TestTrace(t *testing.T) {
cases := []struct {
name string
args string
objectFile string
goldenFile string
}{
{
"Deployment",
"trace podinfo --kind deployment --api-version=apps/v1",
"testdata/trace/deployment.yaml",
"testdata/trace/deployment.golden",
},
{
"HelmRelease",
"trace podinfo --kind HelmRelease --api-version=helm.toolkit.fluxcd.io/v2beta1",
"testdata/trace/helmrelease.yaml",
"testdata/trace/helmrelease.golden",
},
}
cmd.runTestCmd(t)
}
func TestTraceHelmRelease(t *testing.T) {
cmd := cmdTestCase{
args: "trace podinfo -n podinfo --kind HelmRelease --api-version=helm.toolkit.fluxcd.io/v2beta1",
testClusterMode: TestEnvClusterMode,
assert: assertGoldenFile("testdata/trace/helmrelease.golden"),
objectFile: "testdata/trace/helmrelease.yaml",
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
tmpl := map[string]string{
"ns": allocateNamespace("podinfo"),
"fluxns": allocateNamespace("flux-system"),
}
testEnv.CreateObjectFile(tc.objectFile, tmpl, t)
cmd := cmdTestCase{
args: tc.args + " -n=" + tmpl["ns"],
assert: assertGoldenTemplateFile(tc.goldenFile, tmpl),
}
cmd.runTestCmd(t)
})
}
cmd.runTestCmd(t)
}

@ -8,9 +8,8 @@ import (
func TestVersion(t *testing.T) {
cmd := cmdTestCase{
args: "--version",
testClusterMode: TestEnvClusterMode,
assert: assertGoldenValue("flux version 0.0.0-dev.0\n"),
args: "--version",
assert: assertGoldenValue("flux version 0.0.0-dev.0\n"),
}
cmd.runTestCmd(t)
}

Loading…
Cancel
Save