1
0
mirror of synced 2026-02-13 13:06:56 +00:00

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>
This commit is contained in:
Allen Porter
2021-08-23 14:17:41 -07:00
parent def92e14ee
commit d45501a129
22 changed files with 165 additions and 122 deletions

View File

@@ -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