Repository: tetratelabs/getenvoy
Pull request #167 (2021-04-07 06:22:56)

Finishes migration to testing/testify
Per #130, this finishes the port of tests to testing/testify.

The only thing pulling in ginkgo/gomega now is the istio code. This may be obviated after #128 

```
$  go mod why github.com/onsi/ginkgo
# github.com/onsi/ginkgo
github.com/tetratelabs/getenvoy/pkg/binary/envoy/controlplane
github.com/tetratelabs/getenvoy/pkg/binary/envoy/controlplane.test
istio.io/istio/pilot/pkg/bootstrap
istio.io/istio/pkg/util
k8s.io/apimachinery/pkg/util/strategicpatch
k8s.io/kube-openapi/pkg/util/proto
k8s.io/kube-openapi/pkg/util/proto.test
github.com/onsi/ginkgo

$ go mod why github.com/onsi/gomega 
# github.com/onsi/gomega
github.com/tetratelabs/getenvoy/pkg/binary/envoy/controlplane
istio.io/istio/pkg/config/mesh
istio.io/istio/pkg/config/mesh.test
github.com/onsi/gomega
```

codefromthecrypt: kicked test as e2e failed due to known issue with old macOS+rust script
mathetake: let me review tomorrow..

Patch:
diff --git a/go.mod b/go.mod
index a0b5f5e..79ed46a 100644
--- a/go.mod
+++ b/go.mod
@@ -22,8 +22,8 @@ require (
 	github.com/mattn/go-shellwords v1.0.10
 	github.com/mholt/archiver v3.1.1+incompatible
 	github.com/mitchellh/go-homedir v1.1.0
-	github.com/onsi/ginkgo v1.12.1
-	github.com/onsi/gomega v1.10.0
+	github.com/onsi/ginkgo v1.12.1 // indirect
+	github.com/onsi/gomega v1.10.0 // indirect
 	github.com/opencontainers/image-spec v1.0.1
 	github.com/opencontainers/selinux v1.8.0 // indirect
 	github.com/otiai10/copy v1.2.0
diff --git a/pkg/util/cmd/run.go b/pkg/util/cmd/run.go
index ee054fc..d55c300 100644
--- a/pkg/util/cmd/run.go
+++ b/pkg/util/cmd/run.go
@@ -55,9 +55,9 @@ func (fn ComposableRunE) Then(nextFn Run) ComposableRunE {
 // Using this function it is possible to compose more sophisticated behavior, e.g. one
 // where a handler function is called on multiple commands.
 func CallParentPersistentPreRunE() ComposableRunE {
-	return ComposableRunE(func(cmd *cobra.Command, args []string) error {
+	return func(cmd *cobra.Command, args []string) error {
 		return persistentPreRunE(cmd.Parent(), args)
-	})
+	}
 }
 
 // persistentPreRunE walks command tree up and calls PersistentPreRunE/PersistentPreRun


Test patch:
diff --git a/pkg/extension/init/descriptor_test.go b/pkg/extension/init/descriptor_test.go
index fd045ac..568e148 100644
--- a/pkg/extension/init/descriptor_test.go
+++ b/pkg/extension/init/descriptor_test.go
@@ -12,60 +12,51 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package init_test
+package init
 
 import (
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/ginkgo/extensions/table"
-	. "github.com/onsi/gomega"
+	"testing"
+
+	"github.com/stretchr/testify/require"
 
-	. "github.com/tetratelabs/getenvoy/pkg/extension/init"
 	"github.com/tetratelabs/getenvoy/pkg/extension/workspace/config/extension"
 )
 
-var _ = Describe("GenerateExtensionName()", func() {
-	type testCase struct {
+func TestGenerateExtensionName(t *testing.T) {
+	tests := []struct {
 		category  extension.Category
 		outputDir string
 		expected  string
-	}
-	DescribeTable("should generate a valid extension name",
-		func(given testCase) {
-			actual := GenerateExtensionName(given.category, given.outputDir)
-			Expect(actual).To(Equal(given.expected))
-		},
-		Entry("http", testCase{
+	}{
+		{
 			category:  extension.EnvoyHTTPFilter,
 			outputDir: `My-Ext3nsi0n.com`,
 			expected:  `mycompany.filters.http.my_ext3nsi0n_com`,
-		}),
-		Entry("network", testCase{
+		},
+		{
 			category:  extension.EnvoyNetworkFilter,
 			outputDir: `My-Ext3nsi0n.com`,
 			expected:  `mycompany.filters.network.my_ext3nsi0n_com`,
-		}),
-		Entry("access_logger", testCase{
+		},
+		{
 			category:  extension.EnvoyAccessLogger,
 			outputDir: `My-Ext3nsi0n.com`,
 			expected:  `mycompany.access_loggers.my_ext3nsi0n_com`,
-		}),
-	)
-
-	DescribeTable("should panic on unknown categories",
-		func(given testCase) {
-			Expect(func() {
-				_ = GenerateExtensionName(given.category, given.outputDir)
-			}).ToNot(Panic())
 		},
-		func() []TableEntry {
-			testCases := make([]TableEntry, len(extension.Categories))
-			for i, category := range extension.Categories {
-				testCases[i] = Entry(category.String(), testCase{
-					category:  category,
-					outputDir: `my-extension`,
-				})
-			}
-			return testCases
-		}()...,
-	)
-})
+	}
+
+	for _, test := range tests {
+		test := test // pin! see https://github.com/kyoh86/scopelint for why
+
+		t.Run(test.category.String(), func(t *testing.T) {
+			actual := GenerateExtensionName(test.category, test.outputDir)
+			require.Equal(t, test.expected, actual)
+		})
+	}
+}
+
+func TestGenerateExtensionNamePanicsOnUnknownCategory(t *testing.T) {
+	require.PanicsWithError(t, `unknown extension category "my-category"`, func() {
+		_ = GenerateExtensionName(`my-category`, `outputDir`)
+	})
+}
diff --git a/pkg/extension/init/init_suite_test.go b/pkg/extension/init/init_suite_test.go
deleted file mode 100644
index dfcc9e9..0000000
--- a/pkg/extension/init/init_suite_test.go
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright 2020 Tetrate
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package init_test
-
-import (
-	"testing"
-
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/gomega"
-)
-
-func TestInit(t *testing.T) {
-	RegisterFailHandler(Fail)
-	RunSpecs(t, "Init Suite")
-}
diff --git a/pkg/extension/init/init_test.go b/pkg/extension/init/init_test.go
index dbcf71b..71a48fd 100644
--- a/pkg/extension/init/init_test.go
+++ b/pkg/extension/init/init_test.go
@@ -15,36 +15,22 @@
 package init
 
 import (
+	"fmt"
 	"io/ioutil"
-	"os"
 	"path/filepath"
+	"testing"
 
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/ginkgo/extensions/table"
-	. "github.com/onsi/gomega"
+	"github.com/stretchr/testify/require"
 
 	"github.com/tetratelabs/getenvoy/pkg/extension/workspace/config/extension"
+	"github.com/tetratelabs/getenvoy/pkg/test/morerequire"
 )
 
-var _ = Describe("interpolate()", func() {
-	type testCase struct {
-		extension   *extension.Descriptor
-		fileName    string
-		fileContent string
-		expected    string
+func TestInterpolateExtensionName(t *testing.T) {
+	e := extension.Descriptor{
+		Name: "my_company.my_extension",
 	}
-	DescribeTable("should interpolate extension name",
-		func(given testCase) {
-			actual, err := interpolate(given.extension)(given.fileName, []byte(given.fileContent))
-			Expect(err).NotTo(HaveOccurred())
-			Expect(string(actual)).To(Equal(given.expected))
-		},
-		Entry("src/factory.rs", testCase{
-			extension: &extension.Descriptor{
-				Name: "my_company.my_extension",
-			},
-			fileName: "src/factory.rs",
-			fileContent: `
+	actual, err := interpolate(&e)("src/factory.rs", []byte(`
 impl<'a> ExtensionFactory for SampleHttpFilterFactory<'a> {
     type Extension = SampleHttpFilter<'a>;
 
@@ -54,8 +40,10 @@ impl<'a> ExtensionFactory for SampleHttpFilterFactory<'a> {
     /// (also known as "group_name").
 	const NAME: &'static str = "{{ .Extension.Name }}";
 }
-`,
-			expected: `
+`))
+
+	require.NoError(t, err)
+	require.Equal(t, fmt.Sprintf(`
 impl<'a> ExtensionFactory for SampleHttpFilterFactory<'a> {
     type Extension = SampleHttpFilter<'a>;
 
@@ -63,50 +51,20 @@ impl<'a> ExtensionFactory for SampleHttpFilterFactory<'a> {
     ///
     /// This name appears in "Envoy" configuration as a value of "root_id" field
     /// (also known as "group_name").
-	const NAME: &'static str = "my_company.my_extension";
+	const NAME: &'static str = "%s";
+}
+`, e.Name), string(actual))
 }
-`,
-		}),
-	)
-})
-
-var _ = Describe("Scaffold()", func() {
-	var tmpDir string
-
-	BeforeEach(func() {
-		dir, err := ioutil.TempDir("", "")
-		Expect(err).NotTo(HaveOccurred())
-		tmpDir = dir
-	})
-
-	AfterEach(func() {
-		if tmpDir != "" {
-			Expect(os.RemoveAll(tmpDir)).To(Succeed())
-		}
-	})
 
-	type testCase struct {
+func TestScaffold(t *testing.T) {
+	tests := []struct {
+		name      string
 		extension *extension.Descriptor
 		file      string
 		expected  string
-	}
-	DescribeTable("should interpolate extension name",
-		func(given testCase) {
-			opts := &ScaffoldOpts{
-				Extension:    given.extension,
-				TemplateName: "default",
-				OutputDir:    tmpDir,
-			}
-
-			err := Scaffold(opts)
-			Expect(err).NotTo(HaveOccurred())
-
-			actual, err := ioutil.ReadFile(filepath.Join(opts.OutputDir, given.file))
-			Expect(err).NotTo(HaveOccurred())
-
-			Expect(string(actual)).To(ContainSubstring(given.expected))
-		},
-		Entry("rust/filters/http", testCase{
+	}{
+		{
+			name: "rust/filters/http",
 			extension: &extension.Descriptor{
 				Name:     "my_company.my_extension",
 				Category: extension.EnvoyHTTPFilter,
@@ -114,8 +72,9 @@ var _ = Describe("Scaffold()", func() {
 			},
 			file:     "src/factory.rs",
 			expected: `"my_company.my_extension"`,
-		}),
-		Entry("rust/filters/network", testCase{
+		},
+		{
+			name: "rust/filters/network",
 			extension: &extension.Descriptor{
 				Name:     "my_company.my_extension",
 				Category: extension.EnvoyNetworkFilter,
@@ -123,8 +82,9 @@ var _ = Describe("Scaffold()", func() {
 			},
 			file:     "src/factory.rs",
 			expected: `"my_company.my_extension"`,
-		}),
-		Entry("rust/access_logger", testCase{
+		},
+		{
+			name: "rust/access_logger",
 			extension: &extension.Descriptor{
 				Name:     "my_company.my_extension",
 				Category: extension.EnvoyAccessLogger,
@@ -132,6 +92,28 @@ var _ = Describe("Scaffold()", func() {
 			},
 			file:     "src/logger.rs",
 			expected: `"my_company.my_extension"`,
-		}),
-	)
-})
+		},
+	}
+
+	for _, test := range tests {
+		test := test // pin! see https://github.com/kyoh86/scopelint for why
+
+		t.Run(test.name, func(t *testing.T) {
+			outputDir, revertOutputDir := morerequire.RequireNewTempDir(t)
+			defer revertOutputDir()
+
+			opts := &ScaffoldOpts{
+				Extension:    test.extension,
+				TemplateName: "default",
+				OutputDir:    outputDir,
+			}
+
+			err := Scaffold(opts)
+			require.NoError(t, err)
+
+			actual, err := ioutil.ReadFile(filepath.Join(outputDir, test.file))
+			require.NoError(t, err)
+			require.Contains(t, string(actual), test.expected)
+		})
+	}
+}
diff --git a/pkg/extension/workspace/config/extension/extension_suite_test.go b/pkg/extension/workspace/config/extension/extension_suite_test.go
deleted file mode 100644
index a55ca0a..0000000
--- a/pkg/extension/workspace/config/extension/extension_suite_test.go
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright 2020 Tetrate
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package extension_test
-
-import (
-	"testing"
-
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/gomega"
-)
-
-func TestExtension(t *testing.T) {
-	RegisterFailHandler(Fail)
-	RunSpecs(t, "Extension Suite")
-}
diff --git a/pkg/extension/workspace/config/extension/extension_test.go b/pkg/extension/workspace/config/extension/extension_test.go
index 988014e..1b654eb 100644
--- a/pkg/extension/workspace/config/extension/extension_test.go
+++ b/pkg/extension/workspace/config/extension/extension_test.go
@@ -16,80 +16,52 @@ package extension_test
 
 import (
 	"fmt"
+	"testing"
 
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/ginkgo/extensions/table"
-	. "github.com/onsi/gomega"
+	"github.com/stretchr/testify/require"
 
 	"github.com/tetratelabs/getenvoy/pkg/extension/workspace/config"
 	. "github.com/tetratelabs/getenvoy/pkg/extension/workspace/config/extension"
 )
 
-var _ = Describe("Extension", func() {
-	Describe("Unmarshal()", func() {
-		Describe("in case of invalid input", func() {
-			type testCase struct {
-				input       string
-				expectedErr string
-			}
-			DescribeTable("should fail with a proper error",
-				func(given testCase) {
-					var descriptor Descriptor
-					err := config.Unmarshal([]byte(given.input), &descriptor)
-					Expect(err).To(MatchError(given.expectedErr))
-				},
-				Entry("invalid programming language", testCase{
-					input: `#
+func TestDescriptorValidate(t *testing.T) {
+	input := `#
 # Envoy Wasm extension created with getenvoy toolkit.
 #
 kind: Extension
 
 name: mycompany.filters.http.custom_metrics
 
-language: invalid
 category: envoy.filters.http
-`,
-					expectedErr: `error unmarshaling JSON: "invalid" is not a valid programming language`,
-				}),
-				Entry("invalid extension category", testCase{
-					input: `#
-# Envoy Wasm extension created with getenvoy toolkit.
-#
-kind: Extension
-
-name: mycompany.filters.http.custom_metrics
-
 language: rust
-category: invalid
-`,
-					expectedErr: `error unmarshaling JSON: "invalid" is not a valid extension category`,
-				}),
-			)
-		})
-	})
-
-	//nolint:lll
-	Describe("Validate()", func() {
-		Describe("in case of invalid input", func() {
-			type testCase struct {
-				input       string
-				expectedErr string
-			}
-			DescribeTable("should fail with a proper error",
-				func(given testCase) {
-					var descriptor Descriptor
-					err := config.Unmarshal([]byte(given.input), &descriptor)
-					Expect(err).ToNot(HaveOccurred())
-
-					err = descriptor.Validate()
-					Expect(err).To(MatchError(given.expectedErr))
-				},
-				Entry("empty", testCase{
-					input:       ``,
-					expectedErr: `4 errors occurred: extension name cannot be empty; extension category cannot be empty; programming language cannot be empty; runtime description is not valid: Envoy version cannot be empty`,
-				}),
-				Entry("invalid Envoy version", testCase{
-					input: `#
+
+# Runtime the extension is being developed against.
+runtime:
+  envoy:
+    version: standard:1.17.0
+`
+	var descriptor Descriptor
+	err := config.Unmarshal([]byte(input), &descriptor)
+	require.NoError(t, err)
+
+	err = descriptor.Validate()
+	require.NoError(t, err)
+}
+
+func TestDescriptorValidateError(t *testing.T) {
+	tests := []struct {
+		name        string
+		input       string
+		expectedErr string
+	}{
+		{
+			name:        "empty",
+			input:       ``,
+			expectedErr: `4 errors occurred: extension name cannot be empty; extension category cannot be empty; programming language cannot be empty; runtime description is not valid: Envoy version cannot be empty`,
+		},
+		{
+			name: "invalid Envoy version",
+			input: `#
 # Envoy Wasm extension created with getenvoy toolkit.
 #
 kind: Extension
@@ -104,10 +76,11 @@ runtime:
   envoy:
     version: invalid value
 `,
-					expectedErr: `runtime description is not valid: Envoy version is not valid: "invalid value" is not a valid GetEnvoy reference. Expected format: <flavor>:<version>[/<platform>]`,
-				}),
-				Entry("missing extension name", testCase{
-					input: `#
+			expectedErr: `runtime description is not valid: Envoy version is not valid: "invalid value" is not a valid GetEnvoy reference. Expected format: <flavor>:<version>[/<platform>]`,
+		},
+		{
+			name: "missing extension name",
+			input: `#
 # Envoy Wasm extension created with getenvoy toolkit.
 #
 kind: Extension
@@ -120,10 +93,10 @@ runtime:
   envoy:
     version: standard:1.17.0
 `,
-					expectedErr: `extension name cannot be empty`,
-				}),
-				Entry("invalid extension name", testCase{
-					input: `#
+			expectedErr: `extension name cannot be empty`,
+		}, {
+			name: "invalid extension name",
+			input: `#
 # Envoy Wasm extension created with getenvoy toolkit.
 #
 kind: Extension
@@ -138,108 +111,87 @@ runtime:
   envoy:
     version: standard:1.17.0
 `,
-					expectedErr: `"?!@#$%" is not a valid extension name. Extension name must match the format "^[a-z0-9_]+(\\.[a-z0-9_]+)*$". E.g., 'mycompany.filters.http.custom_metrics'`,
-				}),
-			)
+			expectedErr: `"?!@#$%" is not a valid extension name. Extension name must match the format "^[a-z0-9_]+(\\.[a-z0-9_]+)*$". E.g., 'mycompany.filters.http.custom_metrics'`,
+		},
+	}
+
+	for _, test := range tests {
+		test := test // pin! see https://github.com/kyoh86/scopelint for why
+
+		t.Run(test.name, func(t *testing.T) {
+			var descriptor Descriptor
+			err := config.Unmarshal([]byte(test.input), &descriptor)
+			require.NoError(t, err)
+
+			err = descriptor.Validate()
+			require.EqualError(t, err, test.expectedErr)
 		})
-		Describe("in case of valid input", func() {
-			type testCase struct {
-				input string
-			}
-			DescribeTable("should not return any error",
-				func(given testCase) {
-					var descriptor Descriptor
-					err := config.Unmarshal([]byte(given.input), &descriptor)
-					Expect(err).ToNot(HaveOccurred())
-
-					err = descriptor.Validate()
-					Expect(err).ToNot(HaveOccurred())
-
-					actual, err := config.Marshal(&descriptor)
-					Expect(err).ToNot(HaveOccurred())
-					Expect(actual).To(MatchYAML(given.input))
-				},
-				Entry("invalid Envoy version", testCase{
-					input: `#
-# Envoy Wasm extension created with getenvoy toolkit.
-#
-kind: Extension
+	}
+}
+
+func TestValidateExtensionName(t *testing.T) {
+	tests := []struct {
+		name  string
+		input string
+	}{
+		{"Envoy-like name", "mycompany.filters.http.custom_metrics"},
+		{"no segments", "myextension"},
+		{"numbers", "911.i18n.v2"},
+		{"'_'", "_._"},
+	}
 
-name: mycompany.filters.http.custom_metrics
+	for _, test := range tests {
+		test := test // pin! see https://github.com/kyoh86/scopelint for why
 
-category: envoy.filters.http
-language: rust
+		t.Run(test.name, func(t *testing.T) {
+			err := ValidateExtensionName(test.input)
+			require.NoError(t, err)
+		})
+	}
+}
+
+func TestValidateExtensionNameError(t *testing.T) {
+	tests := []struct {
+		name  string
+		input string
+	}{
+		{"trailing '.'", "myextension."},
+		{"upper-case", "MYEXTENSION"},
+		{"'-'", "-.-"},
+		{"non alpha-num characters", `!@#$%^&*()-+<>?~:;"'\[]{}`},
+	}
 
-# Runtime the extension is being developed against.
-runtime:
-  envoy:
-    version: standard:1.17.0
-`,
-				}),
-			)
+	for _, test := range tests {
+		test := test // pin! see https://github.com/kyoh86/scopelint for why
+
+		t.Run(test.name, func(t *testing.T) {
+			err := ValidateExtensionName(test.input)
+			expectedErr := fmt.Sprintf(`%q is not a valid extension name. Extension name must match the format "^[a-z0-9_]+(\\.[a-z0-9_]+)*$". E.g., 'mycompany.filters.http.custom_metrics'`, test.input) //nolint:lll
+			require.EqualError(t, err, expectedErr, test.input)
 		})
-	})
-})
-
-var _ = Describe("ValidateExtensionName()", func() {
-	DescribeTable("should accept valid names",
-		func(given string) {
-			err := ValidateExtensionName(given)
-			Expect(err).ToNot(HaveOccurred())
-		},
-		Entry("Envoy-like name", "mycompany.filters.http.custom_metrics"),
-		Entry("no segments", "myextension"),
-		Entry("numbers", "911.i18n.v2"),
-		Entry("'_'", "_._"),
-	)
-	//nolint:lll
-	DescribeTable("should reject invalid names",
-		func(given string) {
-			err := ValidateExtensionName(given)
-			Expect(err).To(MatchError(fmt.Sprintf(`%q is not a valid extension name. Extension name must match the format "^[a-z0-9_]+(\\.[a-z0-9_]+)*$". E.g., 'mycompany.filters.http.custom_metrics'`, given)))
-		},
-		Entry("trailing '.'", "myextension."),
-		Entry("upper-case", "MYEXTENSION"),
-		Entry("'-'", "-.-"),
-		Entry("non alpha-num characters", `!@#$%^&*()-+<>?~:;"'\[]{}`),
-	)
-})
-
-var _ = Describe("SanitizeExtensionName()", func() {
-	type testCase struct {
-		input    []string
-		expected string
 	}
-	DescribeTable("should replace unsafe characters",
-		func(given testCase) {
-			actual := SanitizeExtensionName(given.input...)
-			Expect(actual).To(Equal(given.expected))
-			Expect(ValidateExtensionName(actual)).To(Succeed())
-		},
-		Entry("upper-case, non-alpha-num and empty", testCase{
-			input:    []string{`My-C0mpany.com`, ``, `e!x@t#`},
-			expected: `my_c0mpany_com.e_x_t_`,
-		}),
-	)
-})
-
-var _ = Describe("SanitizeExtensionNameSegment()", func() {
-	type testCase struct {
+}
+
+func TestSanitizeExtensionName(t *testing.T) {
+	actual := SanitizeExtensionName(`My-C0mpany.com`, ``, `e!x@t#`)
+	require.Equal(t, `my_c0mpany_com.e_x_t_`, actual)
+}
+
+func TestSanitizeExtensionNameSegment(t *testing.T) {
+	tests := []struct {
 		input    string
 		expected string
+	}{
+		{`My-C0mpany.com`, `my_c0mpany_com`},
+		{`!@#$%^&*()-+<>?~:;"'\[]{}`, `_________________________`},
 	}
-	DescribeTable("should replace unsafe characters",
-		func(given testCase) {
-			actual := SanitizeExtensionNameSegment(given.input)
-			Expect(actual).To(Equal(given.expected))
-		},
-		Entry("upper-case", testCase{
-			input:    `My-C0mpany.com`,
-			expected: `my_c0mpany_com`,
-		}),
-		Entry("non alpha-num characters", testCase{
-			input:    `!@#$%^&*()-+<>?~:;"'\[]{}`,
-			expected: `_________________________`,
-		}),
-	)
-})
+
+	for _, test := range tests {
+		test := test // pin! see https://github.com/kyoh86/scopelint for why
+
+		t.Run(test.input, func(t *testing.T) {
+			actual := SanitizeExtensionNameSegment(test.input)
+			require.Equal(t, test.expected, actual)
+		})
+	}
+}
diff --git a/pkg/extension/workspace/config/toolchain/builtin/builtin_suite_test.go b/pkg/extension/workspace/config/toolchain/builtin/builtin_suite_test.go
deleted file mode 100644
index deb45f0..0000000
--- a/pkg/extension/workspace/config/toolchain/builtin/builtin_suite_test.go
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright 2020 Tetrate
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package builtin_test
-
-import (
-	"testing"
-
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/gomega"
-)
-
-func TestBuiltin(t *testing.T) {
-	RegisterFailHandler(Fail)
-	RunSpecs(t, "Builtin Suite")
-}
diff --git a/pkg/extension/workspace/config/toolchain/builtin/helpers_test.go b/pkg/extension/workspace/config/toolchain/builtin/helpers_test.go
index 6f5044d..9d340a0 100644
--- a/pkg/extension/workspace/config/toolchain/builtin/helpers_test.go
+++ b/pkg/extension/workspace/config/toolchain/builtin/helpers_test.go
@@ -15,51 +15,39 @@
 package builtin_test
 
 import (
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/ginkgo/extensions/table"
-	. "github.com/onsi/gomega"
+	"testing"
+
+	"github.com/stretchr/testify/require"
 
 	"github.com/tetratelabs/getenvoy/pkg/extension/workspace/config"
 	. "github.com/tetratelabs/getenvoy/pkg/extension/workspace/config/toolchain/builtin"
 )
 
-var _ = Describe("ToolchainConfig", func() {
-	Describe("Validate()", func() {
-		Describe("in case of valid input", func() {
-			type testCase struct {
-				input string
-			}
-			DescribeTable("should not return any error",
-				func(given testCase) {
-					var toolchain ToolchainConfig
-					err := config.Unmarshal([]byte(given.input), &toolchain)
-					Expect(err).NotTo(HaveOccurred())
-
-					err = toolchain.Validate()
-					Expect(err).ToNot(HaveOccurred())
-
-					actual, err := config.Marshal(toolchain)
-					Expect(err).NotTo(HaveOccurred())
-
-					Expect(actual).To(MatchYAML(given.input))
-				},
-				Entry("default build container", testCase{
-					input: `
+func TestToolchainConfigValidate(t *testing.T) {
+	tests := []struct {
+		name  string
+		input string
+	}{
+		{
+			name: "default build container",
+			input: `
                     kind: BuiltinToolchain
                     container:
                       image: default/image
 `,
-				}),
-				Entry("empty build config", testCase{
-					input: `
+		},
+		{
+			name: "empty build config",
+			input: `
                     kind: BuiltinToolchain
                     container:
                       image: default/image
                     build: {}
 `,
-				}),
-				Entry("'build' config with container", testCase{
-					input: `
+		},
+		{
+			name: "'build' config with container",
+			input: `
                     kind: BuiltinToolchain
                     container:
                       image: default/image
@@ -67,9 +55,10 @@ var _ = Describe("ToolchainConfig", func() {
                       container:
                         image: build/image
 `,
-				}),
-				Entry("'build' config with *.wasm file output path", testCase{
-					input: `
+		},
+		{
+			name: "'build' config with *.wasm file output path",
+			input: `
                     kind: BuiltinToolchain
                     container:
                       image: default/image
@@ -79,9 +68,10 @@ var _ = Describe("ToolchainConfig", func() {
                       output:
                         wasmFile: output/extension.wasm
 `,
-				}),
-				Entry("'test' config with container", testCase{
-					input: `
+		},
+		{
+			name: "'test' config with container",
+			input: `
                     kind: BuiltinToolchain
                     container:
                       image: default/image
@@ -89,9 +79,10 @@ var _ = Describe("ToolchainConfig", func() {
                       container:
                         image: test/image
 `,
-				}),
-				Entry("'clean' config with container", testCase{
-					input: `
+		},
+		{
+			name: "'clean' config with container",
+			input: `
                     kind: BuiltinToolchain
                     container:
                       image: default/image
@@ -99,61 +90,72 @@ var _ = Describe("ToolchainConfig", func() {
                       container:
                         image: clean/image
 `,
-				}),
-			)
-		})
+		},
+	}
+
+	for _, test := range tests {
+		test := test // pin! see https://github.com/kyoh86/scopelint for why
 
-		Describe("in case of invalid input", func() {
-			type testCase struct {
-				input       string
-				expectedErr string
-			}
-			DescribeTable("should fail with a proper error",
-				func(given testCase) {
-					var toolchain ToolchainConfig
-					err := config.Unmarshal([]byte(given.input), &toolchain)
-					Expect(err).NotTo(HaveOccurred())
+		t.Run(test.name, func(t *testing.T) {
+			var toolchainConfig ToolchainConfig
+			err := config.Unmarshal([]byte(test.input), &toolchainConfig)
+			require.NoError(t, err)
+
+			err = toolchainConfig.Validate()
+			require.NoError(t, err)
+		})
+	}
+}
 
-					err = toolchain.Validate()
-					Expect(err).To(MatchError(given.expectedErr))
-				},
-				Entry("blank", testCase{
-					input:       ``,
-					expectedErr: `configuration of the default build container cannot be empty`,
-				}),
-				Entry("no default build container", testCase{
-					input: `
+func TestToolchainConfigValidateError(t *testing.T) {
+	tests := []struct {
+		name        string
+		input       string
+		expectedErr string
+	}{
+		{
+			name:        "blank",
+			input:       ``,
+			expectedErr: `configuration of the default build container cannot be empty`,
+		},
+		{
+			name: "no default build container",
+			input: `
                     kind: BuiltinToolchain
 `,
-					expectedErr: `configuration of the default build container cannot be empty`,
-				}),
-				Entry("default build container: no image name", testCase{
-					input: `
+			expectedErr: `configuration of the default build container cannot be empty`,
+		},
+		{
+			name: "default build container: no image name",
+			input: `
                     kind: BuiltinToolchain
                     container: {}
 `,
-					expectedErr: `configuration of the default build container is not valid: image name cannot be empty`,
-				}),
-				Entry("default build container: invalid image name", testCase{
-					input: `
+			expectedErr: `configuration of the default build container is not valid: image name cannot be empty`,
+		},
+		{
+			name: "default build container: invalid image name",
+			input: `
                     kind: BuiltinToolchain
                     container:
                       image: ???
 `,
-					expectedErr: `configuration of the default build container is not valid: "???" is not a valid image name: invalid reference format`,
-				}),
-				Entry("build tool: no image name", testCase{
-					input: `
+			expectedErr: `configuration of the default build container is not valid: "???" is not a valid image name: invalid reference format`,
+		},
+		{
+			name: "build tool: no image name",
+			input: `
                     kind: BuiltinToolchain
                     container:
                       image: default/image
                     build:
                       container: {}
 `,
-					expectedErr: `'build' tool config is not valid: container configuration is not valid: image name cannot be empty`,
-				}),
-				Entry("build tool: no image name", testCase{
-					input: `
+			expectedErr: `'build' tool config is not valid: container configuration is not valid: image name cannot be empty`,
+		},
+		{
+			name: "build tool: no image name",
+			input: `
                     kind: BuiltinToolchain
                     container:
                       image: default/image
@@ -161,10 +163,11 @@ var _ = Describe("ToolchainConfig", func() {
                       container:
                         image: ???
 `,
-					expectedErr: `'build' tool config is not valid: container configuration is not valid: "???" is not a valid image name: invalid reference format`,
-				}),
-				Entry("build tool: no *.wasm file output path", testCase{
-					input: `
+			expectedErr: `'build' tool config is not valid: container configuration is not valid: "???" is not a valid image name: invalid reference format`,
+		},
+		{
+			name: "build tool: no *.wasm file output path",
+			input: `
                     kind: BuiltinToolchain
                     container:
                       image: default/image
@@ -173,10 +176,11 @@ var _ = Describe("ToolchainConfig", func() {
                         image: build/image
                       output: {}
 `,
-					expectedErr: `'build' tool config is not valid: output configuration is not valid: *.wasm file output path cannot be empty`,
-				}),
-				Entry("build tool: *.wasm file absolute output path", testCase{
-					input: `
+			expectedErr: `'build' tool config is not valid: output configuration is not valid: *.wasm file output path cannot be empty`,
+		},
+		{
+			name: "build tool: *.wasm file absolute output path",
+			input: `
                     kind: BuiltinToolchain
                     container:
                       image: default/image
@@ -186,20 +190,22 @@ var _ = Describe("ToolchainConfig", func() {
                       output:
                         wasmFile: /absolute/path/to/extension.wasm
 `,
-					expectedErr: `'build' tool config is not valid: output configuration is not valid: *.wasm file output path must be relative to the workspace root`,
-				}),
-				Entry("test tool: no image name", testCase{
-					input: `
+			expectedErr: `'build' tool config is not valid: output configuration is not valid: *.wasm file output path must be relative to the workspace root`,
+		},
+		{
+			name: "test tool: no image name",
+			input: `
                     kind: BuiltinToolchain
                     container:
                       image: default/image
                     test:
                       container: {}
 `,
-					expectedErr: `'test' tool config is not valid: container configuration is not valid: image name cannot be empty`,
-				}),
-				Entry("test tool: no image name", testCase{
-					input: `
+			expectedErr: `'test' tool config is not valid: container configuration is not valid: image name cannot be empty`,
+		},
+		{
+			name: "test tool: no image name",
+			input: `
                     kind: BuiltinToolchain
                     container:
                       image: default/image
@@ -207,20 +213,22 @@ var _ = Describe("ToolchainConfig", func() {
                       container:
                         image: ???
 `,
-					expectedErr: `'test' tool config is not valid: container configuration is not valid: "???" is not a valid image name: invalid reference format`,
-				}),
-				Entry("clean tool: no image name", testCase{
-					input: `
+			expectedErr: `'test' tool config is not valid: container configuration is not valid: "???" is not a valid image name: invalid reference format`,
+		},
+		{
+			name: "clean tool: no image name",
+			input: `
                     kind: BuiltinToolchain
                     container:
                       image: default/image
                     clean:
                       container: {}
 `,
-					expectedErr: `'clean' tool config is not valid: container configuration is not valid: image name cannot be empty`,
-				}),
-				Entry("clean tool: no image name", testCase{
-					input: `
+			expectedErr: `'clean' tool config is not valid: container configuration is not valid: image name cannot be empty`,
+		},
+		{
+			name: "clean tool: no image name",
+			input: `
                     kind: BuiltinToolchain
                     container:
                       image: default/image
@@ -228,54 +236,63 @@ var _ = Describe("ToolchainConfig", func() {
                       container:
                         image: ???
 `,
-					expectedErr: `'clean' tool config is not valid: container configuration is not valid: "???" is not a valid image name: invalid reference format`,
-				}),
-			)
-		})
-	})
+			expectedErr: `'clean' tool config is not valid: container configuration is not valid: "???" is not a valid image name: invalid reference format`,
+		},
+	}
+
+	for _, test := range tests {
+		test := test // pin! see https://github.com/kyoh86/scopelint for why
 
-	Describe("GetBuildOutputWasmFile()", func() {
-		type testCase struct {
-			input    string
-			expected string
-		}
-		DescribeTable("should return proper output path",
-			func(given testCase) {
-				var toolchain ToolchainConfig
-				err := config.Unmarshal([]byte(given.input), &toolchain)
-				Expect(err).NotTo(HaveOccurred())
+		t.Run(test.name, func(t *testing.T) {
+			var toolchainConfig ToolchainConfig
+			err := config.Unmarshal([]byte(test.input), &toolchainConfig)
+			require.NoError(t, err)
+
+			err = toolchainConfig.Validate()
+			require.EqualError(t, err, test.expectedErr)
+		})
+	}
+}
 
-				Expect(toolchain.GetBuildOutputWasmFile()).To(Equal(given.expected))
-			},
-			Entry("build: no config", testCase{
-				input: `
+func TestToolchainConfigGetBuildOutputWasmFile(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    string
+		expected string
+	}{
+		{
+			name: "build: no config",
+			input: `
                 kind: BuiltinToolchain
                 container:
                   image: default/image
 `,
-				expected: `extension.wasm`,
-			}),
-			Entry("build: empty config", testCase{
-				input: `
+			expected: `extension.wasm`,
+		},
+		{
+			name: "build: empty config",
+			input: `
                 kind: BuiltinToolchain
                 container:
                   image: default/image
                 build: {}
 `,
-				expected: `extension.wasm`,
-			}),
-			Entry("build: empty output config", testCase{
-				input: `
+			expected: `extension.wasm`,
+		},
+		{
+			name: "build: empty output config",
+			input: `
                 kind: BuiltinToolchain
                 container:
                   image: default/image
                 build:
                   output: {}
 `,
-				expected: `extension.wasm`,
-			}),
-			Entry("build: empty *.wasm file output path", testCase{
-				input: `
+			expected: `extension.wasm`,
+		},
+		{
+			name: "build: empty *.wasm file output path",
+			input: `
                 kind: BuiltinToolchain
                 container:
                   image: default/image
@@ -283,10 +300,11 @@ var _ = Describe("ToolchainConfig", func() {
                   output:
                     wasmFile:
 `,
-				expected: `extension.wasm`,
-			}),
-			Entry("build: non-empty *.wasm file output path", testCase{
-				input: `
+			expected: `extension.wasm`,
+		},
+		{
+			name: "build: non-empty *.wasm file output path",
+			input: `
                 kind: BuiltinToolchain
                 container:
                   image: default/image
@@ -294,8 +312,20 @@ var _ = Describe("ToolchainConfig", func() {
                   output:
                     wasmFile: output/extension.wasm
 `,
-				expected: `output/extension.wasm`,
-			}),
-		)
-	})
-})
+			expected: `output/extension.wasm`,
+		},
+	}
+
+	for _, test := range tests {
+		test := test // pin! see https://github.com/kyoh86/scopelint for why
+
+		t.Run(test.name, func(t *testing.T) {
+			var toolchainConfig ToolchainConfig
+			err := config.Unmarshal([]byte(test.input), &toolchainConfig)
+			require.NoError(t, err)
+
+			actual := toolchainConfig.GetBuildOutputWasmFile()
+			require.Equal(t, test.expected, actual)
+		})
+	}
+}
diff --git a/pkg/extension/workspace/example/envoy/template/expand_test.go b/pkg/extension/workspace/example/envoy/template/expand_test.go
index 1b2a2f7..9b97988 100644
--- a/pkg/extension/workspace/example/envoy/template/expand_test.go
+++ b/pkg/extension/workspace/example/envoy/template/expand_test.go
@@ -15,118 +15,141 @@
 package template_test
 
 import (
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/ginkgo/extensions/table"
-	. "github.com/onsi/gomega"
+	"testing"
+
+	"github.com/stretchr/testify/require"
 
 	"github.com/tetratelabs/getenvoy/pkg/extension/manager"
 	"github.com/tetratelabs/getenvoy/pkg/extension/workspace/config/extension"
 	. "github.com/tetratelabs/getenvoy/pkg/extension/workspace/example/envoy/template"
 )
 
-var _ = Describe("Expand()", func() {
-	Describe("in case of invalid input", func() {
-		type testCase struct {
-			input       string
-			expectedErr string
-		}
-		//nolint:lll
-		DescribeTable("should fail with a proper error",
-			func(given testCase) {
-				ctx := &ExpandContext{
-					DefaultExtension:       manager.NewLocalExtension(extension.NewExtensionDescriptor(), "/path/to/extension.wasm"),
-					DefaultExtensionConfig: ``,
-				}
+func TestExpand(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    string
+		expected string
+	}{
+		{
+			name:     "default value: admin",
+			input:    `{{ .GetEnvoy.DefaultValue "admin" }}`,
+			expected: `{"accessLogPath":"/dev/null","address":{"socketAddress":{"address":"127.0.0.1","portValue":9901}}}`,
+		},
+		{
+			name:     "default value: admin.access_log_path",
+			input:    `{{ .GetEnvoy.DefaultValue "admin.access_log_path" }}`,
+			expected: `"/dev/null"`,
+		},
+		{
+			name:     "default value: admin.address",
+			input:    `{{ .GetEnvoy.DefaultValue "admin.address" }}`,
+			expected: `{"socketAddress":{"address":"127.0.0.1","portValue":9901}}`,
+		},
+		{
+			name:     "default value: admin.address.socket.address",
+			input:    `{{ .GetEnvoy.DefaultValue "admin.address.socket.address" }}`,
+			expected: `"127.0.0.1"`,
+		},
+		{
+			name:     "default value: admin.address.socket.port",
+			input:    `{{ .GetEnvoy.DefaultValue "admin.address.socket.port" }}`,
+			expected: `9901`,
+		},
+		{
+			name:     "extension: name",
+			input:    `{{ .GetEnvoy.Extension.Name }}`,
+			expected: `"mycompany.filters.http.custom_metrics"`,
+		},
+		{
+			name:     "extension: code",
+			input:    `{{ .GetEnvoy.Extension.Code }}`,
+			expected: `{"local":{"filename":"/path/to/extension.wasm"}}`,
+		},
+		{
+			name:     "extension: config",
+			input:    `{{ .GetEnvoy.Extension.Config }}`,
+			expected: `{"@type":"type.googleapis.com/google.protobuf.StringValue","value":"{\"key\":\"value\"}"}`,
+		},
+		{
+			name:     "access to proto message",
+			input:    `{{ (.GetEnvoy.Extension.Code).Message.GetLocal.GetFilename }}`,
+			expected: `/path/to/extension.wasm`,
+		},
+	}
+
+	for _, test := range tests {
+		test := test // pin! see https://github.com/kyoh86/scopelint for why
+
+		t.Run(test.name, func(t *testing.T) {
+			ctx := &ExpandContext{
+				DefaultExtension: manager.NewLocalExtension(
+					&extension.Descriptor{
+						Name:     "mycompany.filters.http.custom_metrics",
+						Category: extension.EnvoyHTTPFilter,
+						Language: extension.LanguageRust,
+					},
+					"/path/to/extension.wasm",
+				),
+				DefaultExtensionConfig: `{"key":"value"}`,
+			}
+
+			actual, err := Expand([]byte(test.input), ctx)
+			require.NoError(t, err)
+			require.Equal(t, test.expected, string(actual))
+		})
+	}
+}
+
+func TestExpandValidates(t *testing.T) {
+	tests := []struct {
+		name        string
+		input       string
+		expectedErr string
+	}{
+		{
+			name:        "invalid Golang template",
+			input:       `{{{ "hello" }}`,
+			expectedErr: `failed to parse Envoy config template: template: :1: unexpected "{" in command`,
+		},
+		{
+			name:        "missing leading '.'",
+			input:       `{{ GetEnvoy.DefaultValue "admin" }}`,
+			expectedErr: `failed to parse Envoy config template: template: :1: function "GetEnvoy" not defined`,
+		},
+		{
+			name:        "unknown property name",
+			input:       `{{ .GetEnvoy.DefaultValue "???" }}`,
+			expectedErr: `failed to render Envoy config template: template: :1:12: executing "" at <.GetEnvoy.DefaultValue>: error calling DefaultValue: unknown property "???"`,
+		},
+		{
+			name:        "external Wasm module: name",
+			input:       `{{ .GetEnvoy.Extension.Name "org/name:version" }}`,
+			expectedErr: `failed to render Envoy config template: template: :1:12: executing "" at <.GetEnvoy.Extension.Name>: error calling Name: unable to resolve Wasm module [org/name:version]: not supported yet`,
+		},
+		{
+			name:        "external Wasm module: code",
+			input:       `{{ .GetEnvoy.Extension.Code "org/name:version" }}`,
+			expectedErr: `failed to render Envoy config template: template: :1:12: executing "" at <.GetEnvoy.Extension.Code>: error calling Code: unable to resolve Wasm module [org/name:version]: not supported yet`,
+		},
+		{
+			name:        "external Wasm module: config",
+			input:       `{{ .GetEnvoy.Extension.Config "another-wasm-module" }}`,
+			expectedErr: `failed to render Envoy config template: template: :1:12: executing "" at <.GetEnvoy.Extension.Config>: error calling Config: unable to resolve a named config [another-wasm-module]: not supported yet`,
+		},
+	}
 
-				_, err := Expand([]byte(given.input), ctx)
-				Expect(err).To(MatchError(given.expectedErr))
-			},
-			Entry("invalid Golang template", testCase{
-				input:       `{{{ "hello" }}`,
-				expectedErr: `failed to parse Envoy config template: template: :1: unexpected "{" in command`,
-			}),
-			Entry("missing leading '.'", testCase{
-				input:       `{{ GetEnvoy.DefaultValue "admin" }}`,
-				expectedErr: `failed to parse Envoy config template: template: :1: function "GetEnvoy" not defined`,
-			}),
-			Entry("unknown property name", testCase{
-				input:       `{{ .GetEnvoy.DefaultValue "???" }}`,
-				expectedErr: `failed to render Envoy config template: template: :1:12: executing "" at <.GetEnvoy.DefaultValue>: error calling DefaultValue: unknown property "???"`,
-			}),
-			Entry("external Wasm module: name", testCase{
-				input:       `{{ .GetEnvoy.Extension.Name "org/name:version" }}`,
-				expectedErr: `failed to render Envoy config template: template: :1:12: executing "" at <.GetEnvoy.Extension.Name>: error calling Name: unable to resolve Wasm module [org/name:version]: not supported yet`,
-			}),
-			Entry("external Wasm module: code", testCase{
-				input:       `{{ .GetEnvoy.Extension.Code "org/name:version" }}`,
-				expectedErr: `failed to render Envoy config template: template: :1:12: executing "" at <.GetEnvoy.Extension.Code>: error calling Code: unable to resolve Wasm module [org/name:version]: not supported yet`,
-			}),
-			Entry("external Wasm module: config", testCase{
-				input:       `{{ .GetEnvoy.Extension.Config "another-wasm-module" }}`,
-				expectedErr: `failed to render Envoy config template: template: :1:12: executing "" at <.GetEnvoy.Extension.Config>: error calling Config: unable to resolve a named config [another-wasm-module]: not supported yet`,
-			}),
-		)
-	})
+	for _, test := range tests {
+		test := test // pin! see https://github.com/kyoh86/scopelint for why
 
-	Describe("in case of valid input", func() {
-		type testCase struct {
-			input    string
-			expected string
-		}
-		DescribeTable("should fail with a proper error",
-			func(given testCase) {
-				ctx := &ExpandContext{
-					DefaultExtension: manager.NewLocalExtension(
-						&extension.Descriptor{
-							Name:     "mycompany.filters.http.custom_metrics",
-							Category: extension.EnvoyHTTPFilter,
-							Language: extension.LanguageRust,
-						},
-						"/path/to/extension.wasm",
-					),
-					DefaultExtensionConfig: `{"key":"value"}`,
-				}
+		t.Run(test.name, func(t *testing.T) {
+			ctx := &ExpandContext{
+				DefaultExtension:       manager.NewLocalExtension(extension.NewExtensionDescriptor(), "/path/to/extension.wasm"),
+				DefaultExtensionConfig: ``,
+			}
 
-				actual, err := Expand([]byte(given.input), ctx)
-				Expect(err).ToNot(HaveOccurred())
-				Expect(string(actual)).To(Equal(given.expected))
-			},
-			Entry("default value: admin", testCase{
-				input:    `{{ .GetEnvoy.DefaultValue "admin" }}`,
-				expected: `{"accessLogPath":"/dev/null","address":{"socketAddress":{"address":"127.0.0.1","portValue":9901}}}`,
-			}),
-			Entry("default value: admin.access_log_path", testCase{
-				input:    `{{ .GetEnvoy.DefaultValue "admin.access_log_path" }}`,
-				expected: `"/dev/null"`,
-			}),
-			Entry("default value: admin.address", testCase{
-				input:    `{{ .GetEnvoy.DefaultValue "admin.address" }}`,
-				expected: `{"socketAddress":{"address":"127.0.0.1","portValue":9901}}`,
-			}),
-			Entry("default value: admin.address.socket.address", testCase{
-				input:    `{{ .GetEnvoy.DefaultValue "admin.address.socket.address" }}`,
-				expected: `"127.0.0.1"`,
-			}),
-			Entry("default value: admin.address.socket.port", testCase{
-				input:    `{{ .GetEnvoy.DefaultValue "admin.address.socket.port" }}`,
-				expected: `9901`,
-			}),
-			Entry("extension: name", testCase{
-				input:    `{{ .GetEnvoy.Extension.Name }}`,
-				expected: `"mycompany.filters.http.custom_metrics"`,
-			}),
-			Entry("extension: code", testCase{
-				input:    `{{ .GetEnvoy.Extension.Code }}`,
-				expected: `{"local":{"filename":"/path/to/extension.wasm"}}`,
-			}),
-			Entry("extension: config", testCase{
-				input:    `{{ .GetEnvoy.Extension.Config }}`,
-				expected: `{"@type":"type.googleapis.com/google.protobuf.StringValue","value":"{\"key\":\"value\"}"}`,
-			}),
-			Entry("access to proto message", testCase{
-				input:    `{{ (.GetEnvoy.Extension.Code).Message.GetLocal.GetFilename }}`,
-				expected: `/path/to/extension.wasm`,
-			}),
-		)
-	})
-})
+			actual, err := Expand([]byte(test.input), ctx)
+			require.Nil(t, actual)
+			require.EqualError(t, err, test.expectedErr)
+		})
+	}
+}
diff --git a/pkg/extension/workspace/example/envoy/template/template_suite_test.go b/pkg/extension/workspace/example/envoy/template/template_suite_test.go
deleted file mode 100644
index 3c85602..0000000
--- a/pkg/extension/workspace/example/envoy/template/template_suite_test.go
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright 2020 Tetrate
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package template_test
-
-import (
-	"testing"
-
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/gomega"
-)
-
-func TestTemplate(t *testing.T) {
-	RegisterFailHandler(Fail)
-	RunSpecs(t, "Template Suite")
-}
diff --git a/pkg/extension/workspace/example/envoy/util/load_test.go b/pkg/extension/workspace/example/envoy/util/load_test.go
index 76149b2..190ce6e 100644
--- a/pkg/extension/workspace/example/envoy/util/load_test.go
+++ b/pkg/extension/workspace/example/envoy/util/load_test.go
@@ -17,19 +17,17 @@ package util
 import (
 	"reflect"
 	"strings"
+	"testing"
 
 	envoylistener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
 	"github.com/golang/protobuf/proto"
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/ginkgo/extensions/table"
-	. "github.com/onsi/gomega"
+	"github.com/stretchr/testify/require"
 
 	"github.com/tetratelabs/getenvoy/pkg/extension/workspace/model"
 )
 
-var _ = Describe("Load()", func() {
-	It("should not fail on unknown Any types", func() {
-		config := &model.File{Source: "/path/to/envoy.yaml", Content: []byte(`
+func TestLoadAllowsUnknownAnyTypes(t *testing.T) {
+	config := &model.File{Source: "/path/to/envoy.yaml", Content: []byte(`
         unknown_field: unknown_value
         filter_chains:
         - filters:
@@ -40,42 +38,49 @@ var _ = Describe("Load()", func() {
               c: 1000
 `)}
 
-		var listener envoylistener.Listener
-		err := Load(config, &listener)
-		Expect(err).ToNot(HaveOccurred())
-	})
+	var listener envoylistener.Listener
+	err := Load(config, &listener)
+	require.NoError(t, err)
+}
 
-	//nolint:lll
-	It("should fail if input is not a valid YAML", func() {
-		config := &model.File{Source: "/path/to/envoy.yaml", Content: []byte(`
+func TestLoadValidatesYAML(t *testing.T) {
+	config := &model.File{Source: "/path/to/envoy.yaml", Content: []byte(`
         code: {{ .GetEnvoy.Extension.Code }}
 `)}
-		expectedErr := `failed to convert into JSON Envoy config coming from "/path/to/envoy.yaml": yaml: invalid map key: map[interface {}]interface {}{".GetEnvoy.Extension.Code":interface {}(nil)}`
 
-		var listener envoylistener.Listener
-		err := Load(config, &listener)
-		Expect(err).To(MatchError(expectedErr))
-	})
-})
+	var listener envoylistener.Listener
+	err := Load(config, &listener)
+
+	expectedErr := `failed to convert into JSON Envoy config coming from "/path/to/envoy.yaml": yaml: invalid map key: map[interface {}]interface {}{".GetEnvoy.Extension.Code":interface {}(nil)}`
+	require.EqualError(t, err, expectedErr)
+}
+
+func TestNewFakeAnyResolver(t *testing.T) {
+	tests := []struct {
+		name    string
+		typeURL string
+	}{
+		{"unknown type", "type.googleapis.com/unknown.Type"},
+		{"known type", "type.googleapis.com/envoy.config.bootstrap.v3.Bootstrap"},
+	}
+
+	for _, test := range tests {
+		test := test // pin! see https://github.com/kyoh86/scopelint for why
 
-var _ = Describe("newFakeAnyResolver()", func() {
-	DescribeTable("should be able to resolve any type URL",
-		func(given string) {
+		t.Run(test.name, func(t *testing.T) {
 			resolver := newFakeAnyResolver()
 
-			actual, err := resolver.Resolve(given)
-			Expect(err).ToNot(HaveOccurred())
+			actual, err := resolver.Resolve(test.typeURL)
+			require.NoError(t, err)
 
 			props := proto.GetProperties(reflect.ValueOf(actual).Elem().Type())
 			fields := MessageFields(props.Prop).Filter(func(field *proto.Properties) bool {
 				return !strings.HasPrefix(field.Name, "XXX_")
 			})
-			Expect(fields).To(HaveLen(0))
-		},
-		Entry("unknown type", "type.googleapis.com/unknown.Type"),
-		Entry("known type", "type.googleapis.com/envoy.config.bootstrap.v3.Bootstrap"),
-	)
-})
+			require.Empty(t, fields)
+		})
+	}
+}
 
 type MessageFields []*proto.Properties
 
diff --git a/pkg/extension/workspace/example/envoy/util/util_suite_test.go b/pkg/extension/workspace/example/envoy/util/util_suite_test.go
deleted file mode 100644
index 844f40d..0000000
--- a/pkg/extension/workspace/example/envoy/util/util_suite_test.go
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright 2020 Tetrate
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package util_test
-
-import (
-	"testing"
-
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/gomega"
-)
-
-func TestUtil(t *testing.T) {
-	RegisterFailHandler(Fail)
-	RunSpecs(t, "Util Suite")
-}
diff --git a/pkg/extension/workspace/example/runtime/configdir/configdir_suite_test.go b/pkg/extension/workspace/example/runtime/configdir/configdir_suite_test.go
deleted file mode 100644
index b25a499..0000000
--- a/pkg/extension/workspace/example/runtime/configdir/configdir_suite_test.go
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright 2020 Tetrate
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package configdir_test
-
-import (
-	"testing"
-
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/gomega"
-)
-
-func TestGetenvoy(t *testing.T) {
-	RegisterFailHandler(Fail)
-	RunSpecs(t, "configdir Suite")
-}
diff --git a/pkg/extension/workspace/example/runtime/configdir/configdir_test.go b/pkg/extension/workspace/example/runtime/configdir/configdir_test.go
index e6d49df..57a4f80 100644
--- a/pkg/extension/workspace/example/runtime/configdir/configdir_test.go
+++ b/pkg/extension/workspace/example/runtime/configdir/configdir_test.go
@@ -19,11 +19,10 @@ import (
 	"io/ioutil"
 	"os"
 	"path/filepath"
+	"testing"
 
 	envoybootstrap "github.com/envoyproxy/go-control-plane/envoy/config/bootstrap/v3"
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/ginkgo/extensions/table"
-	. "github.com/onsi/gomega"
+	"github.com/stretchr/testify/require"
 
 	workspaces "github.com/tetratelabs/getenvoy/pkg/extension/workspace"
 	"github.com/tetratelabs/getenvoy/pkg/extension/workspace/example/runtime"
@@ -31,161 +30,173 @@ import (
 	"github.com/tetratelabs/getenvoy/pkg/extension/workspace/model"
 )
 
-var _ = Describe("NewConfigDir()", func() {
-
-	runContext := func(workspace model.Workspace, example model.Example) *runtime.RunContext {
-		_, f := example.GetExtensionConfig()
-		return &runtime.RunContext{
-			Opts: runtime.RunOpts{
-				Workspace: workspace,
-				Example: runtime.ExampleOpts{
-					Name:    "default",
-					Example: example,
-				},
-				Extension: runtime.ExtensionOpts{
-					WasmFile: `/path/to/extension.wasm`,
-					Config:   *f,
-				},
+func TestNewConfigDir(t *testing.T) {
+	tests := []struct {
+		name             string
+		workspaceDir     string
+		isEnvoyTemplate  func(string) bool
+		requireBootstrap func(t *testing.T, bootstrap *envoybootstrap.Bootstrap)
+	}{
+		{
+			name:         "envoy.tmpl.yaml",
+			workspaceDir: "testdata/workspace1",
+			isEnvoyTemplate: func(name string) bool {
+				return name == "envoy.tmpl.yaml" //nolint:goconst
 			},
-		}
-	}
-
-	expectValidBootstrap := func(bootstrap *envoybootstrap.Bootstrap) {
-		Expect(bootstrap).ToNot(BeNil())
-		Expect(bootstrap.GetAdmin().GetAccessLogPath()).To(Equal("/dev/null"))
-		Expect(bootstrap.GetAdmin().GetAddress().GetSocketAddress().GetAddress()).To(Equal("127.0.0.1"))
-		Expect(bootstrap.GetAdmin().GetAddress().GetSocketAddress().GetPortValue()).To(Equal(uint32(9901)))
-	}
-
-	expectInvalidBootstrap := func(bootstrap *envoybootstrap.Bootstrap) {
-		Expect(bootstrap).To(BeNil())
+			requireBootstrap: expectValidBootstrap,
+		},
+		{
+			name:         "envoy.tmpl.yaml + lds.tmpl.yaml + cds.yaml",
+			workspaceDir: "testdata/workspace2",
+			isEnvoyTemplate: func(name string) bool {
+				return name == "envoy.tmpl.yaml" || name == "lds.tmpl.yaml" || name == "cds.yaml"
+			},
+			requireBootstrap: expectValidBootstrap,
+		},
+		{
+			name:         "envoy.tmpl.yaml: not a valid YAML",
+			workspaceDir: "testdata/workspace3",
+			isEnvoyTemplate: func(name string) bool {
+				return false
+			},
+			requireBootstrap: expectInvalidBootstrap,
+		},
+		{
+			name:         "envoy.tmpl.yaml: invalid paths to `lds` and `cds` files",
+			workspaceDir: "testdata/workspace4",
+			isEnvoyTemplate: func(name string) bool {
+				return name == "envoy.tmpl.yaml"
+			},
+			requireBootstrap: expectValidBootstrap,
+		},
+		{
+			name:         "envoy.tmpl.yaml: .txt configuration",
+			workspaceDir: "testdata/workspace8",
+			isEnvoyTemplate: func(name string) bool {
+				return name == "envoy.tmpl.yaml"
+			},
+			requireBootstrap: expectValidBootstrap,
+		},
 	}
 
-	Describe("in case of valid input", func() {
-		type testCase struct {
-			workspaceDir    string
-			isEnvoyTemplate func(string) bool
-			expectBootstrap func(bootstrap *envoybootstrap.Bootstrap)
-		}
-		DescribeTable("should create a proper config directory",
-			func(given testCase) {
-				workspace, err := workspaces.GetWorkspaceAt(given.workspaceDir)
-				Expect(err).ToNot(HaveOccurred())
-
-				example, err := workspace.GetExample("default")
-				Expect(err).ToNot(HaveOccurred())
-
-				ctx := runContext(workspace, example)
-
-				By("creating a config dir")
-				configDir, err := NewConfigDir(ctx)
-				Expect(err).ToNot(HaveOccurred())
-				defer func() {
-					By("removing config dir")
-					Expect(configDir.Close()).To(Succeed())
-
-					By("verifying config dir has been removed")
-					_, err := os.Stat(configDir.GetDir())
-					Expect(err).To(HaveOccurred())
-					Expect(os.IsNotExist(err)).To(BeTrue())
-				}()
-
-				By("verifying the config dir")
-				Expect(configDir.GetBootstrapFile()).ToNot(BeEmpty())
-				given.expectBootstrap(configDir.GetBootstrap())
-
-				By("verifying contents of the config dir")
-				for _, fileName := range ctx.Opts.Example.GetFiles().GetNames() {
-					expected, err := ioutil.ReadFile(filepath.Join(given.workspaceDir, "expected/getenvoy_extension_run", fileName))
-					Expect(err).ToNot(HaveOccurred())
-					actual, err := ioutil.ReadFile(filepath.Join(configDir.GetDir(), fileName))
-					Expect(err).ToNot(HaveOccurred())
-					if given.isEnvoyTemplate(fileName) {
-						Expect(string(actual)).Should(MatchYAML(string(expected)))
-					} else {
-						Expect(string(actual)).To(Equal(string(expected)))
-					}
+	for _, test := range tests {
+		test := test // pin! see https://github.com/kyoh86/scopelint for why
+
+		t.Run(test.name, func(t *testing.T) {
+			workspace, err := workspaces.GetWorkspaceAt(test.workspaceDir)
+			require.NoError(t, err)
+
+			example, err := workspace.GetExample("default")
+			require.NoError(t, err)
+
+			ctx := runContext(workspace, example)
+
+			configDir, err := NewConfigDir(ctx)
+			require.NoError(t, err)
+
+			defer func() {
+				err := configDir.Close()
+				require.NoError(t, err)
+
+				// verifying config dir has been removed
+				_, err = os.Stat(configDir.GetDir())
+				require.Error(t, err)
+				require.True(t, os.IsNotExist(err))
+			}()
+
+			// verify the config dir
+			require.FileExists(t, configDir.GetBootstrapFile())
+			test.requireBootstrap(t, configDir.GetBootstrap())
+
+			// verify contents of the config dir
+			for _, fileName := range ctx.Opts.Example.GetFiles().GetNames() {
+				expected, err := ioutil.ReadFile(filepath.Join(test.workspaceDir, "expected/getenvoy_extension_run", fileName))
+				require.NoError(t, err)
+				actual, err := ioutil.ReadFile(filepath.Join(configDir.GetDir(), fileName))
+				require.NoError(t, err)
+				if test.isEnvoyTemplate(fileName) {
+					require.YAMLEq(t, string(expected), string(actual))
+				} else {
+					require.Equal(t, string(expected), string(actual))
 				}
-			},
-			Entry("envoy.tmpl.yaml", testCase{
-				workspaceDir: "testdata/workspace1",
-				isEnvoyTemplate: func(name string) bool {
-					return name == "envoy.tmpl.yaml" //nolint:goconst
-				},
-				expectBootstrap: expectValidBootstrap,
-			}),
-			Entry("envoy.tmpl.yaml + lds.tmpl.yaml + cds.yaml", testCase{
-				workspaceDir: "testdata/workspace2",
-				isEnvoyTemplate: func(name string) bool {
-					return name == "envoy.tmpl.yaml" || name == "lds.tmpl.yaml" || name == "cds.yaml"
-				},
-				expectBootstrap: expectValidBootstrap,
-			}),
-			Entry("envoy.tmpl.yaml: not a valid YAML", testCase{
-				workspaceDir: "testdata/workspace3",
-				isEnvoyTemplate: func(name string) bool {
-					return false
-				},
-				expectBootstrap: expectInvalidBootstrap,
-			}),
-			Entry("envoy.tmpl.yaml: invalid paths to `lds` and `cds` files", testCase{
-				workspaceDir: "testdata/workspace4",
-				isEnvoyTemplate: func(name string) bool {
-					return name == "envoy.tmpl.yaml"
-				},
-				expectBootstrap: expectValidBootstrap,
-			}),
-			Entry("envoy.tmpl.yaml: .txt configuration", testCase{
-				workspaceDir: "testdata/workspace8",
-				isEnvoyTemplate: func(name string) bool {
-					return name == "envoy.tmpl.yaml"
-				},
-				expectBootstrap: expectValidBootstrap,
-			}),
-		)
-	})
-
-	Describe("in case of invalid input", func() {
-		abs := func(path string) string {
-			path, err := filepath.Abs(path)
-			if err != nil {
-				panic(err)
 			}
-			return path
+		})
+	}
+}
+
+func runContext(workspace model.Workspace, example model.Example) *runtime.RunContext {
+	_, f := example.GetExtensionConfig()
+	return &runtime.RunContext{
+		Opts: runtime.RunOpts{
+			Workspace: workspace,
+			Example: runtime.ExampleOpts{
+				Name:    "default",
+				Example: example,
+			},
+			Extension: runtime.ExtensionOpts{
+				WasmFile: `/path/to/extension.wasm`,
+				Config:   *f,
+			},
+		},
+	}
+}
+
+func expectValidBootstrap(t *testing.T, bootstrap *envoybootstrap.Bootstrap) {
+	require.NotNil(t, bootstrap)
+	require.Equal(t, "/dev/null", bootstrap.GetAdmin().GetAccessLogPath())
+	require.Equal(t, "127.0.0.1", bootstrap.GetAdmin().GetAddress().GetSocketAddress().GetAddress())
+	require.Equal(t, uint32(9901), bootstrap.GetAdmin().GetAddress().GetSocketAddress().GetPortValue())
+}
+
+func expectInvalidBootstrap(t *testing.T, bootstrap *envoybootstrap.Bootstrap) {
+	require.Nil(t, bootstrap)
+}
+
+func TestNewConfigDirValidates(t *testing.T) {
+	abs := func(path string) string {
+		path, err := filepath.Abs(path)
+		if err != nil {
+			panic(err)
 		}
+		return path
+	}
+	tests := []struct {
+		name         string
+		workspaceDir string
+		expectedErr  string
+	}{
+		{
+			name:         "envoy.tmpl.yaml: invalid placeholder",
+			workspaceDir: "testdata/workspace5",
+			expectedErr:  fmt.Sprintf(`failed to process Envoy config template coming from %q: failed to render Envoy config template: template: :4:19: executing "" at <.GetEnvoy.DefaultValue>: error calling DefaultValue: unknown property "???"`, abs("testdata/workspace5/.getenvoy/extension/examples/default/envoy.tmpl.yaml")),
+		},
+		{
+			name:         "envoy.tmpl.yaml + lds.tmpl.yaml + cds.yaml: invalid placeholder in lds.tmpl.yaml",
+			workspaceDir: "testdata/workspace6",
+			expectedErr:  fmt.Sprintf(`failed to process Envoy config template coming from %q: failed to render Envoy config template: template: :22:34: executing "" at <.GetEnvoy.Extension.Code>: error calling Code: unable to resolve Wasm module [???]: not supported yet`, abs("testdata/workspace6/.getenvoy/extension/examples/default/lds.tmpl.yaml")),
+		},
+		{
+			name:         "envoy.tmpl.yaml + lds.tmpl.yaml + cds.tmpl.yaml: invalid placeholder in cds.tmpl.yaml",
+			workspaceDir: "testdata/workspace7",
+			expectedErr:  fmt.Sprintf(`failed to process Envoy config template coming from %q: failed to render Envoy config template: template: :1:18: executing "" at <.GetEnvoy.Extension.Config>: error calling Config: unable to resolve a named config [???]: not supported yet`, abs("testdata/workspace7/.getenvoy/extension/examples/default/cds.tmpl.yaml")),
+		},
+	}
 
-		type testCase struct {
-			workspaceDir string
-			expectedErr  string
-		}
-		//nolint:lll
-		DescribeTable("should fail with a proper error",
-			func(given testCase) {
-				workspace, err := workspaces.GetWorkspaceAt(given.workspaceDir)
-				Expect(err).ToNot(HaveOccurred())
+	for _, test := range tests {
+		test := test // pin! see https://github.com/kyoh86/scopelint for why
 
-				example, err := workspace.GetExample("default")
-				Expect(err).ToNot(HaveOccurred())
+		t.Run(test.name, func(t *testing.T) {
+			workspace, err := workspaces.GetWorkspaceAt(test.workspaceDir)
+			require.NoError(t, err)
 
-				ctx := runContext(workspace, example)
+			example, err := workspace.GetExample("default")
+			require.NoError(t, err)
 
-				By("creating a config dir")
-				configDir, err := NewConfigDir(ctx)
-				Expect(configDir).To(BeNil())
-				Expect(err).To(MatchError(given.expectedErr))
-			},
-			Entry("envoy.tmpl.yaml: invalid placeholder", testCase{
-				workspaceDir: "testdata/workspace5",
-				expectedErr:  fmt.Sprintf(`failed to process Envoy config template coming from %q: failed to render Envoy config template: template: :4:19: executing "" at <.GetEnvoy.DefaultValue>: error calling DefaultValue: unknown property "???"`, abs("testdata/workspace5/.getenvoy/extension/examples/default/envoy.tmpl.yaml")),
-			}),
-			Entry("envoy.tmpl.yaml + lds.tmpl.yaml + cds.yaml: invalid placeholder in lds.tmpl.yaml", testCase{
-				workspaceDir: "testdata/workspace6",
-				expectedErr:  fmt.Sprintf(`failed to process Envoy config template coming from %q: failed to render Envoy config template: template: :22:34: executing "" at <.GetEnvoy.Extension.Code>: error calling Code: unable to resolve Wasm module [???]: not supported yet`, abs("testdata/workspace6/.getenvoy/extension/examples/default/lds.tmpl.yaml")),
-			}),
-			Entry("envoy.tmpl.yaml + lds.tmpl.yaml + cds.tmpl.yaml: invalid placeholder in cds.tmpl.yaml", testCase{
-				workspaceDir: "testdata/workspace7",
-				expectedErr:  fmt.Sprintf(`failed to process Envoy config template coming from %q: failed to render Envoy config template: template: :1:18: executing "" at <.GetEnvoy.Extension.Config>: error calling Config: unable to resolve a named config [???]: not supported yet`, abs("testdata/workspace7/.getenvoy/extension/examples/default/cds.tmpl.yaml")),
-			}),
-		)
-	})
-})
+			ctx := runContext(workspace, example)
+
+			configDir, err := NewConfigDir(ctx)
+			require.Nil(t, configDir)
+			require.EqualError(t, err, test.expectedErr)
+		})
+	}
+}
diff --git a/pkg/extension/workspace/toolchain/builtin/builtin_suite_test.go b/pkg/extension/workspace/toolchain/builtin/builtin_suite_test.go
deleted file mode 100644
index deb45f0..0000000
--- a/pkg/extension/workspace/toolchain/builtin/builtin_suite_test.go
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright 2020 Tetrate
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package builtin_test
-
-import (
-	"testing"
-
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/gomega"
-)
-
-func TestBuiltin(t *testing.T) {
-	RegisterFailHandler(Fail)
-	RunSpecs(t, "Builtin Suite")
-}
diff --git a/pkg/extension/workspace/toolchain/builtin/default_config_test.go b/pkg/extension/workspace/toolchain/builtin/default_config_test.go
index 3ce88c6..a3bce6b 100644
--- a/pkg/extension/workspace/toolchain/builtin/default_config_test.go
+++ b/pkg/extension/workspace/toolchain/builtin/default_config_test.go
@@ -17,55 +17,48 @@ package builtin
 import (
 	"fmt"
 	"io/ioutil"
+	"testing"
 
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/ginkgo/extensions/table"
-	. "github.com/onsi/gomega"
-	"github.com/pkg/errors"
+	"github.com/stretchr/testify/require"
 
 	"github.com/tetratelabs/getenvoy/pkg/extension/workspace/config"
 	"github.com/tetratelabs/getenvoy/pkg/extension/workspace/config/extension"
 )
 
-var _ = Describe("defaultConfigFor()", func() {
+func TestDefaultConfigForValidate(t *testing.T) {
 	type testCase struct {
+		name      string
 		extension *extension.Descriptor
 		expected  string
 	}
-	DescribeTable("should generate proper default config for every supported programming language",
-		func(given testCase) {
-			cfg := defaultConfigFor(given.extension)
-			Expect(cfg.Validate()).To(Succeed())
 
-			actual, err := config.Marshal(cfg)
-			Expect(err).ToNot(HaveOccurred())
+	tests := make([]testCase, len(extension.Languages))
+	for i, lang := range extension.Languages {
+		expected, err := ioutil.ReadFile(fmt.Sprintf("testdata/default_config/%s.toolchain.yaml", lang))
+		if err != nil {
+			panic(fmt.Errorf("missing default config for language %s: %w", lang, err))
+		}
+		tests[i] = testCase{lang.String(), &extension.Descriptor{Language: lang}, string(expected)}
+	}
 
-			Expect(actual).To(MatchYAML(given.expected))
-		},
-		func() []TableEntry {
-			entries := make([]TableEntry, len(extension.Languages))
-			for i, lang := range extension.Languages {
-				expected, err := ioutil.ReadFile(fmt.Sprintf("testdata/default_config/%s.toolchain.yaml", lang))
-				if err != nil {
-					panic(errors.Wrapf(err, "missing default config for language %q", lang))
-				}
-				entries[i] = Entry(lang.String(), testCase{
-					extension: &extension.Descriptor{
-						Language: lang,
-					},
-					expected: string(expected),
-				})
-			}
-			return entries
-		}()...,
-	)
+	for _, test := range tests {
+		test := test // pin! see https://github.com/kyoh86/scopelint for why
 
-	It("should panic if the programming language is unknown", func() {
-		descriptor := &extension.Descriptor{
-			Language: "",
-		}
+		t.Run(test.name, func(t *testing.T) {
+			cfg := defaultConfigFor(test.extension)
+
+			err := cfg.Validate()
+			require.NoError(t, err)
+
+			actual, err := config.Marshal(cfg)
+			require.NoError(t, err)
+			require.YAMLEq(t, test.expected, string(actual))
+		})
+	}
+}
 
-		Expect(func() { defaultConfigFor(descriptor) }).
-			To(PanicWith(MatchError(`failed to determine default build image for unsupported programming language ""`)))
+func TestDefaultConfigForPanicsOnUnknownLanguage(t *testing.T) {
+	require.PanicsWithError(t, `failed to determine default build image for unsupported programming language ""`, func() {
+		_ = defaultConfigFor(&extension.Descriptor{Language: ""})
 	})
-})
+}
diff --git a/pkg/extension/workspace/toolchain/builtin/example_config_test.go b/pkg/extension/workspace/toolchain/builtin/example_config_test.go
index c5c9d7d..34c9a2d 100644
--- a/pkg/extension/workspace/toolchain/builtin/example_config_test.go
+++ b/pkg/extension/workspace/toolchain/builtin/example_config_test.go
@@ -12,55 +12,46 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package builtin_test
+package builtin
 
 import (
 	"fmt"
 	"io/ioutil"
+	"testing"
 
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/ginkgo/extensions/table"
-	. "github.com/onsi/gomega"
-	"github.com/pkg/errors"
+	"github.com/stretchr/testify/require"
 
 	"github.com/tetratelabs/getenvoy/pkg/extension/workspace/config/extension"
-	. "github.com/tetratelabs/getenvoy/pkg/extension/workspace/toolchain/builtin"
 )
 
-var _ = Describe("ExampleConfig()", func() {
+func TestExampleConfigValidate(t *testing.T) {
 	type testCase struct {
+		name      string
 		extension *extension.Descriptor
 		expected  string
 	}
-	DescribeTable("should generate proper example config for every supported programming language",
-		func(given testCase) {
-			actual := ExampleConfig(given.extension)
-			Expect(string(actual)).To(Equal(given.expected))
-		},
-		func() []TableEntry {
-			entries := make([]TableEntry, len(extension.Languages))
-			for i, lang := range extension.Languages {
-				expected, err := ioutil.ReadFile(fmt.Sprintf("testdata/example_config/%s.toolchain.yaml", lang))
-				if err != nil {
-					panic(errors.Wrapf(err, "missing example config for language %q", lang))
-				}
-				entries[i] = Entry(lang.String(), testCase{
-					extension: &extension.Descriptor{
-						Language: lang,
-					},
-					expected: string(expected),
-				})
-			}
-			return entries
-		}()...,
-	)
 
-	It("should panic if the programming language is unknown", func() {
-		descriptor := &extension.Descriptor{
-			Language: "",
+	tests := make([]testCase, len(extension.Languages))
+	for i, lang := range extension.Languages {
+		expected, err := ioutil.ReadFile(fmt.Sprintf("testdata/example_config/%s.toolchain.yaml", lang))
+		if err != nil {
+			panic(fmt.Errorf("missing example config for language %s: %w", lang, err))
 		}
+		tests[i] = testCase{lang.String(), &extension.Descriptor{Language: lang}, string(expected)}
+	}
+
+	for _, test := range tests {
+		test := test // pin! see https://github.com/kyoh86/scopelint for why
+
+		t.Run(test.name, func(t *testing.T) {
+			cfg := ExampleConfig(test.extension)
+			require.Equal(t, test.expected, string(cfg))
+		})
+	}
+}
 
-		Expect(func() { ExampleConfig(descriptor) }).
-			To(PanicWith(MatchError(`failed to determine default build image for unsupported programming language ""`)))
+func TestExampleConfigPanicsOnUnknownLanguage(t *testing.T) {
+	require.PanicsWithError(t, `failed to determine default build image for unsupported programming language ""`, func() {
+		_ = ExampleConfig(&extension.Descriptor{Language: ""})
 	})
-})
+}
diff --git a/pkg/extension/workspace/toolchain/builtin/factory_test.go b/pkg/extension/workspace/toolchain/builtin/factory_test.go
index 213d5a0..d890cd3 100644
--- a/pkg/extension/workspace/toolchain/builtin/factory_test.go
+++ b/pkg/extension/workspace/toolchain/builtin/factory_test.go
@@ -11,68 +11,31 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-
-package builtin_test
+package builtin
 
 import (
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/ginkgo/extensions/table"
-	. "github.com/onsi/gomega"
+	"testing"
+
+	"github.com/stretchr/testify/require"
 
 	workspaces "github.com/tetratelabs/getenvoy/pkg/extension/workspace"
 	"github.com/tetratelabs/getenvoy/pkg/extension/workspace/config"
 	extensionconfig "github.com/tetratelabs/getenvoy/pkg/extension/workspace/config/extension"
 	builtinconfig "github.com/tetratelabs/getenvoy/pkg/extension/workspace/config/toolchain/builtin"
 	"github.com/tetratelabs/getenvoy/pkg/extension/workspace/model"
-	. "github.com/tetratelabs/getenvoy/pkg/extension/workspace/toolchain/builtin"
 	"github.com/tetratelabs/getenvoy/pkg/extension/workspace/toolchain/registry"
 )
 
-var _ = Describe("built-in toolchain factory", func() {
-
-	var workspace model.Workspace
-
-	BeforeEach(func() {
-		w, err := workspaces.GetWorkspaceAt("testdata/workspace")
-		Expect(err).ToNot(HaveOccurred())
-		workspace = w
-	})
-
-	type testCase struct {
+func TestBuiltinToolchainLoadConfig(t *testing.T) {
+	workspace, err := workspaces.GetWorkspaceAt("testdata/workspace")
+	require.NoError(t, err)
+	tests := []struct {
+		name     string
 		config   string
 		expected string
-	}
-
-	DescribeTable("should load toolchain config and apply defaults",
-		func(given testCase) {
-			By("verifying built-in toolchain is registered")
-			factory, exists := registry.Get(builtinconfig.Kind)
-			Expect(exists).To(BeTrue())
-
-			By("loading toolchain config")
-			builder, err := factory.LoadConfig(registry.LoadConfigArgs{
-				Workspace: workspace,
-				Toolchain: registry.ToolchainConfig{
-					Name: "example",
-					Config: &model.File{
-						Source:  "<memory>",
-						Content: []byte(given.config),
-					},
-				},
-			})
-			Expect(err).ToNot(HaveOccurred())
-
-			By("verifying that defaults get applied to the toolchain config")
-			actual, err := config.Marshal(builder.GetConfig())
-			Expect(err).ToNot(HaveOccurred())
-			Expect(actual).To(MatchYAML(given.expected))
-
-			By("creating a toolchain")
-			toolchain, err := builder.Build()
-			Expect(err).ToNot(HaveOccurred())
-			Expect(toolchain).ToNot(BeNil())
-		},
-		Entry("empty config", testCase{
+	}{
+		{
+			name:   "empty config",
 			config: `kind: BuiltinToolchain`,
 			expected: `
             kind: BuiltinToolchain
@@ -82,8 +45,9 @@ var _ = Describe("built-in toolchain factory", func() {
               output:
                 wasmFile: target/getenvoy/extension.wasm
 `,
-		}),
-		Entry("example config", testCase{
+		},
+		{
+			name: "example config",
 			config: string(ExampleConfig(&extensionconfig.Descriptor{
 				Language: extensionconfig.LanguageRust,
 			})),
@@ -95,8 +59,9 @@ var _ = Describe("built-in toolchain factory", func() {
               output:
                 wasmFile: target/getenvoy/extension.wasm
 `,
-		}),
-		Entry("full config", testCase{
+		},
+		{
+			name: "full config",
 			config: `
             kind: BuiltinToolchain
             container:
@@ -135,6 +100,39 @@ var _ = Describe("built-in toolchain factory", func() {
                 - -v
                 - /host:/container
 `,
-		}),
-	)
-})
+		},
+	}
+
+	for _, test := range tests {
+		test := test // pin! see https://github.com/kyoh86/scopelint for why
+
+		t.Run(test.name, func(t *testing.T) {
+			// verify built-in toolchain is registered
+			factory, exists := registry.Get(builtinconfig.Kind)
+			require.True(t, exists)
+
+			// load toolchain config
+			builder, err := factory.LoadConfig(registry.LoadConfigArgs{
+				Workspace: workspace,
+				Toolchain: registry.ToolchainConfig{
+					Name: "example",
+					Config: &model.File{
+						Source:  "<memory>",
+						Content: []byte(test.config),
+					},
+				},
+			})
+			require.NoError(t, err)
+
+			// verify defaults get applied to the toolchain config
+			actual, err := config.Marshal(builder.GetConfig())
+			require.NoError(t, err)
+			require.YAMLEq(t, test.expected, string(actual))
+
+			// create a toolchain
+			toolchain, err := builder.Build()
+			require.NoError(t, err)
+			require.NotNil(t, toolchain)
+		})
+	}
+}
diff --git a/pkg/extension/workspace/toolchain/builtin/toolchain_test.go b/pkg/extension/workspace/toolchain/builtin/toolchain_test.go
index c7c231e..d84b967 100644
--- a/pkg/extension/workspace/toolchain/builtin/toolchain_test.go
+++ b/pkg/extension/workspace/toolchain/builtin/toolchain_test.go
@@ -19,168 +19,74 @@ package builtin_test
 import (
 	"bytes"
 	"fmt"
-	"os"
 	"os/user"
-	"path/filepath"
 	"runtime"
-	"strings"
+	"testing"
 
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/ginkgo/extensions/table"
-	. "github.com/onsi/gomega"
+	"github.com/stretchr/testify/require"
 
 	workspaces "github.com/tetratelabs/getenvoy/pkg/extension/workspace"
 	"github.com/tetratelabs/getenvoy/pkg/extension/workspace/config"
 	builtinconfig "github.com/tetratelabs/getenvoy/pkg/extension/workspace/config/toolchain/builtin"
-	"github.com/tetratelabs/getenvoy/pkg/extension/workspace/model"
 	. "github.com/tetratelabs/getenvoy/pkg/extension/workspace/toolchain/builtin"
 	"github.com/tetratelabs/getenvoy/pkg/extension/workspace/toolchain/types"
+	"github.com/tetratelabs/getenvoy/pkg/test/cmd"
+	. "github.com/tetratelabs/getenvoy/pkg/test/morerequire"
 	ioutil "github.com/tetratelabs/getenvoy/pkg/util/io"
 )
 
-var _ = Describe("built-in toolchain", func() {
+func TestBuiltinToolchain(t *testing.T) {
+	// We use a fake docker command to capture the commandline that would be invoked
+	dockerDir, revertPath := RequireOverridePath(t, "testdata/toolchain")
+	defer revertPath()
 
-	var pathBackup string
+	// Fake the current user so we can test it is used in the docker args
+	expectedUser := user.User{Uid: "1001", Gid: "1002"}
+	revertGetCurrentUser := cmd.OverrideGetCurrentUser(&expectedUser)
+	defer revertGetCurrentUser()
 
-	BeforeEach(func() {
-		pathBackup = os.Getenv("PATH")
-	})
+	workspace, err := workspaces.GetWorkspaceAt("testdata/workspace")
+	require.NoError(t, err)
 
-	AfterEach(func() {
-		os.Setenv("PATH", pathBackup)
-	})
-
-	BeforeEach(func() {
-		// override PATH to overshadow `docker` executable during the test
-		path := strings.Join([]string{"testdata/toolchain", pathBackup}, string(filepath.ListSeparator))
-		os.Setenv("PATH", path)
-	})
-
-	var getCurrentUserBackup func() (*user.User, error)
-
-	BeforeEach(func() {
-		getCurrentUserBackup = GetCurrentUser
-	})
-
-	AfterEach(func() {
-		GetCurrentUser = getCurrentUserBackup
-	})
-
-	BeforeEach(func() {
-		GetCurrentUser = func() (*user.User, error) {
-			return &user.User{Uid: "1001", Gid: "1002"}, nil
-		}
-	})
-
-	var workspace model.Workspace
-
-	BeforeEach(func() {
-		w, err := workspaces.GetWorkspaceAt("testdata/workspace")
-		Expect(err).ToNot(HaveOccurred())
-		workspace = w
-	})
-
-	var stdin *bytes.Buffer
-	var stdout *bytes.Buffer
-	var stderr *bytes.Buffer
-	var stdio ioutil.StdStreams
-
-	BeforeEach(func() {
-		stdin = new(bytes.Buffer)
-		stdout = new(bytes.Buffer)
-		stderr = new(bytes.Buffer)
-		stdio = ioutil.StdStreams{
-			In:  stdin,
-			Out: stdout,
-			Err: stderr,
-		}
-	})
-
-	type testCase struct {
+	tests := []struct {
+		name           string
 		config         string
-		tool           func(toolchain types.Toolchain) error
-		expectedStdOut string
+		tool           func(toolchain types.Toolchain, stdio ioutil.StdStreams) error
+		expectedStdout string
 		expectedErr    string
-	}
-
-	parseConfig := func(yaml string) *builtinconfig.ToolchainConfig {
-		var cfg builtinconfig.ToolchainConfig
-		Expect(config.Unmarshal([]byte(yaml), &cfg)).To(Succeed())
-		Expect(cfg.Validate()).To(Succeed())
-		return &cfg
-	}
-
-	build := func(toolchain types.Toolchain) error {
-		By("running 'build' tool")
-		return toolchain.Build(types.BuildContext{IO: stdio})
-	}
-
-	test := func(toolchain types.Toolchain) error {
-		By("running 'test' tool")
-		return toolchain.Test(types.TestContext{IO: stdio})
-	}
-
-	clean := func(toolchain types.Toolchain) error {
-		By("running 'clean' tool")
-		return toolchain.Clean(types.CleanContext{IO: stdio})
-	}
-
-	DescribeTable("should execute `docker run` with a given container image and Docker cli options",
-		func(givenFn func() testCase) {
-			given := givenFn()
-			cfg := parseConfig(given.config)
-
-			By("creating builtin toolchain")
-			toolchain := NewToolchain("test", cfg, workspace)
-
-			By("using toolchain")
-			err := given.tool(toolchain)
-			if given.expectedErr == "" {
-				Expect(err).ToNot(HaveOccurred())
-			} else {
-				Expect(err).To(MatchError(given.expectedErr))
-			}
-
-			By("verifying stdout and stderr")
-			Expect(stdout.String()).To(Equal(given.expectedStdOut))
-			Expect(stderr.String()).To(Equal("docker stderr\n"))
-		},
-		Entry("build using default container image", func() testCase {
-			return testCase{
-				config: `
+	}{{
+		name: "build using default container image",
+		config: `
                 kind: BuiltinToolchain
                 container:
                   image: default/image
 `,
-				tool:           build,
-				expectedStdOut: fmt.Sprintf("testdata/toolchain/docker run -u 1001:1002 --rm -e GETENVOY_GOOS=%s -t -v %s:/source -w /source --init default/image build --output-file extension.wasm\n", runtime.GOOS, workspace.GetDir().GetRootDir()),
-			}
-		}),
-		Entry("test using default container image", func() testCase {
-			return testCase{
-				config: `
+		tool:           build,
+		expectedStdout: fmt.Sprintf("%s/docker run -u 1001:1002 --rm -e GETENVOY_GOOS=%s -t -v %s:/source -w /source --init default/image build --output-file extension.wasm\n", dockerDir, runtime.GOOS, workspace.GetDir().GetRootDir()),
+	},
+		{
+			name: "test using default container image",
+			config: `
                 kind: BuiltinToolchain
                 container:
                   image: default/image
 `,
-				tool:           test,
-				expectedStdOut: fmt.Sprintf("testdata/toolchain/docker run -u 1001:1002 --rm -e GETENVOY_GOOS=%s -t -v %s:/source -w /source --init default/image test\n", runtime.GOOS, workspace.GetDir().GetRootDir()),
-			}
-		}),
-		Entry("clean using default container image", func() testCase {
-			return testCase{
-				config: `
+			tool:           test,
+			expectedStdout: fmt.Sprintf("%s/docker run -u 1001:1002 --rm -e GETENVOY_GOOS=%s -t -v %s:/source -w /source --init default/image test\n", dockerDir, runtime.GOOS, workspace.GetDir().GetRootDir()),
+		},
+		{
+			name: "clean using default container image",
+			config: `
                 kind: BuiltinToolchain
                 container:
                   image: default/image
 `,
-				tool:           clean,
-				expectedStdOut: fmt.Sprintf("testdata/toolchain/docker run -u 1001:1002 --rm -e GETENVOY_GOOS=%s -t -v %s:/source -w /source --init default/image clean\n", runtime.GOOS, workspace.GetDir().GetRootDir()),
-			}
-		}),
-		Entry("build using given container image", func() testCase {
-			return testCase{
-				config: `
+			tool:           clean,
+			expectedStdout: fmt.Sprintf("%s/docker run -u 1001:1002 --rm -e GETENVOY_GOOS=%s -t -v %s:/source -w /source --init default/image clean\n", dockerDir, runtime.GOOS, workspace.GetDir().GetRootDir()),
+		},
+		{
+			name: "build using test container image",
+			config: `
                 kind: BuiltinToolchain
                 container:
                   image: default/image
@@ -194,13 +100,13 @@ var _ = Describe("built-in toolchain", func() {
                   container:
                     image: test/image
 `,
-				tool:           build,
-				expectedStdOut: fmt.Sprintf("testdata/toolchain/docker run -u 1001:1002 --rm -e GETENVOY_GOOS=%s -t -v %s:/source -w /source --init build/image build --output-file output/file.wasm\n", runtime.GOOS, workspace.GetDir().GetRootDir()),
-			}
-		}),
-		Entry("build using given container image and Docker cli options", func() testCase {
-			return testCase{
-				config: `
+			tool:           build,
+			expectedStdout: fmt.Sprintf("%s/docker run -u 1001:1002 --rm -e GETENVOY_GOOS=%s -t -v %s:/source -w /source --init build/image build --output-file output/file.wasm\n", dockerDir, runtime.GOOS, workspace.GetDir().GetRootDir()),
+		},
+		{
+			name: "build using test container image and Docker cli options",
+
+			config: `
                 kind: BuiltinToolchain
                 container:
                   image: default/image
@@ -220,13 +126,12 @@ var _ = Describe("built-in toolchain", func() {
                     - -v
                     - /host/path=/container/path
 `,
-				tool:           build,
-				expectedStdOut: fmt.Sprintf("testdata/toolchain/docker run -u 1001:1002 --rm -e GETENVOY_GOOS=%s -t -v %s:/source -w /source --init -e VAR=VALUE build/image build --output-file output/file.wasm\n", runtime.GOOS, workspace.GetDir().GetRootDir()),
-			}
-		}),
-		Entry("build fails with a non-0 exit code", func() testCase {
-			return testCase{
-				config: `
+			tool:           build,
+			expectedStdout: fmt.Sprintf("%s/docker run -u 1001:1002 --rm -e GETENVOY_GOOS=%s -t -v %s:/source -w /source --init -e VAR=VALUE build/image build --output-file output/file.wasm\n", dockerDir, runtime.GOOS, workspace.GetDir().GetRootDir()),
+		},
+		{
+			name: "build fails with a non-0 exit code",
+			config: `
                 kind: BuiltinToolchain
                 container:
                   image: default/image
@@ -239,14 +144,13 @@ var _ = Describe("built-in toolchain", func() {
                   output:
                     wasmFile: output/file.wasm
 `,
-				tool:           build,
-				expectedStdOut: fmt.Sprintf("testdata/toolchain/docker run -u 1001:1002 --rm -e GETENVOY_GOOS=%s -t -v %s:/source -w /source --init -e DOCKER_EXIT_CODE=3 build/image build --output-file output/file.wasm\n", runtime.GOOS, workspace.GetDir().GetRootDir()),
-				expectedErr:    fmt.Sprintf("failed to execute an external command \"testdata/toolchain/docker run -u 1001:1002 --rm -e GETENVOY_GOOS=%s -t -v %s:/source -w /source --init -e DOCKER_EXIT_CODE=3 build/image build --output-file output/file.wasm\": exit status 3", runtime.GOOS, workspace.GetDir().GetRootDir()),
-			}
-		}),
-		Entry("test using given container image", func() testCase {
-			return testCase{
-				config: `
+			tool:           build,
+			expectedStdout: fmt.Sprintf("%s/docker run -u 1001:1002 --rm -e GETENVOY_GOOS=%s -t -v %s:/source -w /source --init -e DOCKER_EXIT_CODE=3 build/image build --output-file output/file.wasm\n", dockerDir, runtime.GOOS, workspace.GetDir().GetRootDir()),
+			expectedErr:    fmt.Sprintf("failed to execute an external command \"%s/docker run -u 1001:1002 --rm -e GETENVOY_GOOS=%s -t -v %s:/source -w /source --init -e DOCKER_EXIT_CODE=3 build/image build --output-file output/file.wasm\": exit status 3", dockerDir, runtime.GOOS, workspace.GetDir().GetRootDir()),
+		},
+		{
+			name: "test using test container image",
+			config: `
                 kind: BuiltinToolchain
                 container:
                   image: default/image
@@ -260,13 +164,12 @@ var _ = Describe("built-in toolchain", func() {
                   container:
                     image: test/image
 `,
-				tool:           test,
-				expectedStdOut: fmt.Sprintf("testdata/toolchain/docker run -u 1001:1002 --rm -e GETENVOY_GOOS=%s -t -v %s:/source -w /source --init test/image test\n", runtime.GOOS, workspace.GetDir().GetRootDir()),
-			}
-		}),
-		Entry("test using given container image and Docker cli options", func() testCase {
-			return testCase{
-				config: `
+			tool:           test,
+			expectedStdout: fmt.Sprintf("%s/docker run -u 1001:1002 --rm -e GETENVOY_GOOS=%s -t -v %s:/source -w /source --init test/image test\n", dockerDir, runtime.GOOS, workspace.GetDir().GetRootDir()),
+		},
+		{
+			name: "test using test container image and Docker cli options",
+			config: `
                 kind: BuiltinToolchain
                 container:
                   image: default/image
@@ -286,13 +189,12 @@ var _ = Describe("built-in toolchain", func() {
                     - -v
                     - /host/path=/container/path
 `,
-				tool:           test,
-				expectedStdOut: fmt.Sprintf("testdata/toolchain/docker run -u 1001:1002 --rm -e GETENVOY_GOOS=%s -t -v %s:/source -w /source --init -v /host/path=/container/path test/image test\n", runtime.GOOS, workspace.GetDir().GetRootDir()),
-			}
-		}),
-		Entry("test fails with a non-0 exit code", func() testCase {
-			return testCase{
-				config: `
+			tool:           test,
+			expectedStdout: fmt.Sprintf("%s/docker run -u 1001:1002 --rm -e GETENVOY_GOOS=%s -t -v %s:/source -w /source --init -v /host/path=/container/path test/image test\n", dockerDir, runtime.GOOS, workspace.GetDir().GetRootDir()),
+		},
+		{
+			name: "test fails with a non-0 exit code",
+			config: `
                 kind: BuiltinToolchain
                 container:
                   image: default/image
@@ -303,14 +205,13 @@ var _ = Describe("built-in toolchain", func() {
                     - -e
                     - DOCKER_EXIT_CODE=3
 `,
-				tool:           test,
-				expectedStdOut: fmt.Sprintf("testdata/toolchain/docker run -u 1001:1002 --rm -e GETENVOY_GOOS=%s -t -v %s:/source -w /source --init -e DOCKER_EXIT_CODE=3 test/image test\n", runtime.GOOS, workspace.GetDir().GetRootDir()),
-				expectedErr:    fmt.Sprintf("failed to execute an external command \"testdata/toolchain/docker run -u 1001:1002 --rm -e GETENVOY_GOOS=%s -t -v %s:/source -w /source --init -e DOCKER_EXIT_CODE=3 test/image test\": exit status 3", runtime.GOOS, workspace.GetDir().GetRootDir()),
-			}
-		}),
-		Entry("clean using given container image", func() testCase {
-			return testCase{
-				config: `
+			tool:           test,
+			expectedStdout: fmt.Sprintf("%s/docker run -u 1001:1002 --rm -e GETENVOY_GOOS=%s -t -v %s:/source -w /source --init -e DOCKER_EXIT_CODE=3 test/image test\n", dockerDir, runtime.GOOS, workspace.GetDir().GetRootDir()),
+			expectedErr:    fmt.Sprintf("failed to execute an external command \"%s/docker run -u 1001:1002 --rm -e GETENVOY_GOOS=%s -t -v %s:/source -w /source --init -e DOCKER_EXIT_CODE=3 test/image test\": exit status 3", dockerDir, runtime.GOOS, workspace.GetDir().GetRootDir()),
+		},
+		{
+			name: "clean using test container image",
+			config: `
                 kind: BuiltinToolchain
                 container:
                   image: default/image
@@ -324,13 +225,12 @@ var _ = Describe("built-in toolchain", func() {
                   container:
                     image: clean/image
 `,
-				tool:           clean,
-				expectedStdOut: fmt.Sprintf("testdata/toolchain/docker run -u 1001:1002 --rm -e GETENVOY_GOOS=%s -t -v %s:/source -w /source --init clean/image clean\n", runtime.GOOS, workspace.GetDir().GetRootDir()),
-			}
-		}),
-		Entry("clean using given container image and Docker cli options", func() testCase {
-			return testCase{
-				config: `
+			tool:           clean,
+			expectedStdout: fmt.Sprintf("%s/docker run -u 1001:1002 --rm -e GETENVOY_GOOS=%s -t -v %s:/source -w /source --init clean/image clean\n", dockerDir, runtime.GOOS, workspace.GetDir().GetRootDir()),
+		},
+		{
+			name: "clean using test container image and Docker cli options",
+			config: `
                 kind: BuiltinToolchain
                 container:
                   image: default/image
@@ -350,13 +250,12 @@ var _ = Describe("built-in toolchain", func() {
                     - -v
                     - /host/path=/container/path
 `,
-				tool:           clean,
-				expectedStdOut: fmt.Sprintf("testdata/toolchain/docker run -u 1001:1002 --rm -e GETENVOY_GOOS=%s -t -v %s:/source -w /source --init -v /host/path=/container/path clean/image clean\n", runtime.GOOS, workspace.GetDir().GetRootDir()),
-			}
-		}),
-		Entry("clean fails with a non-0 exit code", func() testCase {
-			return testCase{
-				config: `
+			tool:           clean,
+			expectedStdout: fmt.Sprintf("%s/docker run -u 1001:1002 --rm -e GETENVOY_GOOS=%s -t -v %s:/source -w /source --init -v /host/path=/container/path clean/image clean\n", dockerDir, runtime.GOOS, workspace.GetDir().GetRootDir()),
+		},
+		{
+			name: "clean fails with a non-0 exit code",
+			config: `
                 kind: BuiltinToolchain
                 container:
                   image: default/image
@@ -367,10 +266,49 @@ var _ = Describe("built-in toolchain", func() {
                     - -e
                     - DOCKER_EXIT_CODE=3
 `,
-				tool:           clean,
-				expectedStdOut: fmt.Sprintf("testdata/toolchain/docker run -u 1001:1002 --rm -e GETENVOY_GOOS=%s -t -v %s:/source -w /source --init -e DOCKER_EXIT_CODE=3 clean/image clean\n", runtime.GOOS, workspace.GetDir().GetRootDir()),
-				expectedErr:    fmt.Sprintf("failed to execute an external command \"testdata/toolchain/docker run -u 1001:1002 --rm -e GETENVOY_GOOS=%s -t -v %s:/source -w /source --init -e DOCKER_EXIT_CODE=3 clean/image clean\": exit status 3", runtime.GOOS, workspace.GetDir().GetRootDir()),
+			tool:           clean,
+			expectedStdout: fmt.Sprintf("%s/docker run -u 1001:1002 --rm -e GETENVOY_GOOS=%s -t -v %s:/source -w /source --init -e DOCKER_EXIT_CODE=3 clean/image clean\n", dockerDir, runtime.GOOS, workspace.GetDir().GetRootDir()),
+			expectedErr:    fmt.Sprintf("failed to execute an external command \"%s/docker run -u 1001:1002 --rm -e GETENVOY_GOOS=%s -t -v %s:/source -w /source --init -e DOCKER_EXIT_CODE=3 clean/image clean\": exit status 3", dockerDir, runtime.GOOS, workspace.GetDir().GetRootDir()),
+		},
+	}
+
+	for _, test := range tests {
+		test := test // pin! see https://github.com/kyoh86/scopelint for why
+
+		t.Run(test.name, func(t *testing.T) {
+			var cfg builtinconfig.ToolchainConfig
+			err = config.Unmarshal([]byte(test.config), &cfg)
+			require.NoError(t, err)
+
+			err = cfg.Validate()
+			require.NoError(t, err)
+
+			toolchain := NewToolchain("test", &cfg, workspace)
+			stdin, stdout, stderr := new(bytes.Buffer), new(bytes.Buffer), new(bytes.Buffer)
+			stdio := ioutil.StdStreams{In: stdin, Out: stdout, Err: stderr}
+
+			// invoke the toolchain
+			err := test.tool(toolchain, stdio)
+			if test.expectedErr == "" {
+				require.NoError(t, err)
+			} else {
+				require.EqualError(t, err, test.expectedErr)
 			}
-		}),
-	)
-})
+
+			require.Equal(t, test.expectedStdout, stdout.String())
+			require.Equal(t, "docker stderr\n", stderr.String())
+		})
+	}
+}
+
+func build(toolchain types.Toolchain, stdio ioutil.StdStreams) error {
+	return toolchain.Build(types.BuildContext{IO: stdio})
+}
+
+func test(toolchain types.Toolchain, stdio ioutil.StdStreams) error {
+	return toolchain.Test(types.TestContext{IO: stdio})
+}
+
+func clean(toolchain types.Toolchain, stdio ioutil.StdStreams) error {
+	return toolchain.Clean(types.CleanContext{IO: stdio})
+}
diff --git a/pkg/extension/workspace/toolchain/lookup_test.go b/pkg/extension/workspace/toolchain/lookup_test.go
index 09eb4a7..7a97eff 100644
--- a/pkg/extension/workspace/toolchain/lookup_test.go
+++ b/pkg/extension/workspace/toolchain/lookup_test.go
@@ -16,70 +16,64 @@ package toolchain_test
 
 import (
 	"fmt"
-	"io/ioutil"
-	"os"
+	"testing"
 
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/gomega"
+	"github.com/stretchr/testify/require"
 
 	workspaces "github.com/tetratelabs/getenvoy/pkg/extension/workspace"
 	"github.com/tetratelabs/getenvoy/pkg/extension/workspace/fs"
 	"github.com/tetratelabs/getenvoy/pkg/extension/workspace/model"
 	toolchains "github.com/tetratelabs/getenvoy/pkg/extension/workspace/toolchain"
+	"github.com/tetratelabs/getenvoy/pkg/test/morerequire"
 )
 
-var _ = Describe("LoadToolchain()", func() {
-	It("should fail to load toolchains other than `default`", func() {
-		workspace, err := workspaces.GetWorkspaceAt("testdata/workspace1")
-		Expect(err).ToNot(HaveOccurred())
-
-		_, err = toolchains.LoadToolchain("non-existing", workspace)
-		Expect(err).To(MatchError(`unknown toolchain "non-existing". At the moment, only "default" toolchain is supported`))
-	})
-
-	//nolint:lll
-	It("should fail to load toolchain of unknown kind", func() {
-		workspace, err := workspaces.GetWorkspaceAt("testdata/workspace1")
-		Expect(err).ToNot(HaveOccurred())
-
-		_, err = toolchains.LoadToolchain(toolchains.Default, workspace)
-		Expect(err).To(MatchError(fmt.Sprintf(`toolchain "default" has invalid configuration coming from "%s/.getenvoy/extension/toolchains/default.yaml": `+
-			`unknown toolchain kind "UnknownToolchain"`, workspace.GetDir().GetRootDir())))
-	})
-
-	//nolint:lll
-	It("should fail to load toolchain with invalid config", func() {
-		workspace, err := workspaces.GetWorkspaceAt("testdata/workspace2")
-		Expect(err).ToNot(HaveOccurred())
-
-		_, err = toolchains.LoadToolchain(toolchains.Default, workspace)
-		Expect(err).To(MatchError(fmt.Sprintf(`toolchain "default" has invalid configuration coming from "%s/.getenvoy/extension/toolchains/default.yaml": `+
-			`'build' tool config is not valid: container configuration is not valid: "?invalid value?" is not a valid image name: invalid reference format`, workspace.GetDir().GetRootDir())))
-	})
-
-	It("should load toolchain with a valid config", func() {
-		workspace, err := workspaces.GetWorkspaceAt("testdata/workspace3")
-		Expect(err).ToNot(HaveOccurred())
-
-		builder, err := toolchains.LoadToolchain(toolchains.Default, workspace)
-		Expect(err).ToNot(HaveOccurred())
-
-		toolchain, err := builder.Build()
-		Expect(err).ToNot(HaveOccurred())
-		Expect(toolchain).ToNot(BeNil())
-	})
-
-	It("should create default toolchain if missing", func() {
-		tempDir, err := ioutil.TempDir("", "workspace")
-		Expect(err).ToNot(HaveOccurred())
-		defer func() {
-			Expect(os.RemoveAll(tempDir)).To(Succeed())
-		}()
-
-		dir, err := fs.CreateWorkspaceDir(tempDir)
-		Expect(err).ToNot(HaveOccurred())
-
-		err = dir.WriteFile(model.DescriptorFile, []byte(`
+func TestLoadToolchainFailsIfNotDefault(t *testing.T) {
+	workspace, err := workspaces.GetWorkspaceAt("testdata/workspace1")
+	require.NoError(t, err)
+
+	_, err = toolchains.LoadToolchain("non-existing", workspace)
+	require.EqualError(t, err, `unknown toolchain "non-existing". At the moment, only "default" toolchain is supported`) //nolint:lll
+}
+
+func TestLoadToolchainFailsIfUnknown(t *testing.T) {
+	workspace, err := workspaces.GetWorkspaceAt("testdata/workspace1")
+	require.NoError(t, err)
+
+	_, err = toolchains.LoadToolchain(toolchains.Default, workspace)
+	require.EqualError(t, err, fmt.Sprintf(`toolchain "default" has invalid configuration coming from "%s/.getenvoy/extension/toolchains/default.yaml": `+ //nolint:lll
+		`unknown toolchain kind "UnknownToolchain"`, workspace.GetDir().GetRootDir()))
+}
+
+//nolint:lll
+func TestLoadToolchainFailsOnInvalidConfig(t *testing.T) {
+	workspace, err := workspaces.GetWorkspaceAt("testdata/workspace2")
+	require.NoError(t, err)
+
+	_, err = toolchains.LoadToolchain(toolchains.Default, workspace)
+	require.EqualError(t, err, fmt.Sprintf(`toolchain "default" has invalid configuration coming from "%s/.getenvoy/extension/toolchains/default.yaml": `+
+		`'build' tool config is not valid: container configuration is not valid: "?invalid value?" is not a valid image name: invalid reference format`, workspace.GetDir().GetRootDir()))
+}
+
+func TestLoadToolchain(t *testing.T) {
+	workspace, err := workspaces.GetWorkspaceAt("testdata/workspace3")
+	require.NoError(t, err)
+
+	builder, err := toolchains.LoadToolchain(toolchains.Default, workspace)
+	require.NoError(t, err)
+
+	toolchain, err := builder.Build()
+	require.NoError(t, err)
+	require.NotNil(t, toolchain)
+}
+
+func TestLoadToolchainCreatesDefaultWhenMissing(t *testing.T) {
+	outputDir, revertOutputDir := morerequire.RequireNewTempDir(t)
+	defer revertOutputDir()
+
+	dir, err := fs.CreateWorkspaceDir(outputDir)
+	require.NoError(t, err)
+
+	err = dir.WriteFile(model.DescriptorFile, []byte(`
 kind: Extension
 
 name: mycompany.filters.http.custom_metrics
@@ -92,22 +86,24 @@ runtime:
   envoy:
     version: standard:1.17.0
 `))
-		Expect(err).ToNot(HaveOccurred())
+	require.NoError(t, err)
+
+	workspace, err := workspaces.GetWorkspaceAt(outputDir)
+	require.NoError(t, err)
 
-		workspace, err := workspaces.GetWorkspaceAt(tempDir)
-		Expect(err).ToNot(HaveOccurred())
+	builder, err := toolchains.LoadToolchain(toolchains.Default, workspace)
+	require.NoError(t, err)
 
-		builder, err := toolchains.LoadToolchain(toolchains.Default, workspace)
-		Expect(err).ToNot(HaveOccurred())
+	toolchain, err := builder.Build()
+	require.NoError(t, err)
+	require.NotNil(t, toolchain)
 
-		toolchain, err := builder.Build()
-		Expect(err).ToNot(HaveOccurred())
-		Expect(toolchain).ToNot(BeNil())
+	hasDefaultToolchain, err := workspace.HasToolchain(toolchains.Default)
+	require.NoError(t, err)
+	require.True(t, hasDefaultToolchain)
 
-		Expect(workspace.HasToolchain(toolchains.Default)).To(BeTrue())
-		file, err := workspace.GetToolchainConfig(toolchains.Default)
-		Expect(err).ToNot(HaveOccurred())
-		Expect(file).ToNot(BeNil())
-		Expect(file.Content).ToNot(BeEmpty())
-	})
-})
+	file, err := workspace.GetToolchainConfig(toolchains.Default)
+	require.NoError(t, err)
+	require.NotNil(t, file)
+	require.NotEmpty(t, file.Content)
+}
diff --git a/pkg/extension/workspace/toolchain/toolchain_suite_test.go b/pkg/extension/workspace/toolchain/toolchain_suite_test.go
deleted file mode 100644
index 7d4479b..0000000
--- a/pkg/extension/workspace/toolchain/toolchain_suite_test.go
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright 2020 Tetrate
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package toolchain_test
-
-import (
-	"testing"
-
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/gomega"
-)
-
-func TestToolchain(t *testing.T) {
-	RegisterFailHandler(Fail)
-	RunSpecs(t, "Toolchain Suite")
-}
diff --git a/pkg/test/cmd/context.go b/pkg/test/cmd/context.go
deleted file mode 100644
index 57b7fe0..0000000
--- a/pkg/test/cmd/context.go
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright 2020 Tetrate
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package cmd
-
-import (
-	"os/user"
-
-	. "github.com/onsi/ginkgo" //nolint:golint
-
-	builtintoolchain "github.com/tetratelabs/getenvoy/pkg/extension/workspace/toolchain/builtin"
-)
-
-// GetCurrentUserFunc returns the current user.
-type GetCurrentUserFunc func() (*user.User, error)
-
-// SetDefaultUser configures built-in toolchain to resolve the current user to a predefined value.
-func SetDefaultUser() {
-	SetUser(func() (*user.User, error) {
-		return &user.User{Uid: "1001", Gid: "1002"}, nil
-	})
-}
-
-// SetUser configures built-in toolchain to resolve the current user using a given function.
-func SetUser(fn GetCurrentUserFunc) {
-	var getCurrentUserBackup GetCurrentUserFunc
-
-	BeforeEach(func() {
-		getCurrentUserBackup = builtintoolchain.GetCurrentUser
-	})
-
-	AfterEach(func() {
-		builtintoolchain.GetCurrentUser = getCurrentUserBackup
-	})
-
-	BeforeEach(func() {
-		builtintoolchain.GetCurrentUser = fn
-	})
-}
diff --git a/pkg/types/types_suite_test.go b/pkg/types/types_suite_test.go
deleted file mode 100644
index a7c4674..0000000
--- a/pkg/types/types_suite_test.go
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright 2020 Tetrate
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package types_test
-
-import (
-	"testing"
-
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/gomega"
-)
-
-func TestTypes(t *testing.T) {
-	RegisterFailHandler(Fail)
-	RunSpecs(t, "Types Suite")
-}
diff --git a/pkg/types/types_test.go b/pkg/types/types_test.go
index 23e948b..d233e05 100644
--- a/pkg/types/types_test.go
+++ b/pkg/types/types_test.go
@@ -15,129 +15,154 @@
 package types_test
 
 import (
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/ginkgo/extensions/table"
-	. "github.com/onsi/gomega"
+	"testing"
+
+	"github.com/stretchr/testify/require"
 
 	. "github.com/tetratelabs/getenvoy/pkg/types"
 )
 
-var _ = Describe("ParseReference()", func() {
-	Describe("valid input", func() {
-		type testCase struct {
-			input    string
-			expected Reference
-		}
-		DescribeTable("should parse valid input",
-			func(given testCase) {
-				ref, err := ParseReference(given.input)
+func TestParseReference(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    string
+		expected Reference
+	}{
+		{
+			name:     "standard:1.11.0",
+			input:    `standard:1.11.0`,
+			expected: Reference{Flavor: "standard", Version: "1.11.0", Platform: ""},
+		},
+		{
+			name:     "standard:1.11.0/darwin",
+			input:    `standard:1.11.0/darwin`,
+			expected: Reference{Flavor: "standard", Version: "1.11.0", Platform: "darwin"},
+		},
+		{
+			name:     "standard:1.11.0/linux-glibc",
+			input:    `standard:1.11.0/linux-glibc`,
+			expected: Reference{Flavor: "standard", Version: "1.11.0", Platform: "linux-glibc"},
+		},
+		{
+			name:     "wasm:1.15",
+			input:    `wasm:1.15`,
+			expected: Reference{Flavor: "wasm", Version: "1.15", Platform: ""},
+		},
+		{
+			name:     "wasm:1.15/darwin",
+			input:    `wasm:1.15/darwin`,
+			expected: Reference{Flavor: "wasm", Version: "1.15", Platform: "darwin"},
+		},
+		{
+			name:     "wasm:1.15/linux-glibc",
+			input:    `wasm:1.15/linux-glib`,
+			expected: Reference{Flavor: "wasm", Version: "1.15", Platform: "linux-glib"},
+		},
+		{
+			name:     "mixed case",
+			input:    `Wasm:NightlY/LINUX-GLIBC`,
+			expected: Reference{Flavor: "wasm", Version: "nightly", Platform: "linux-glibc"},
+		},
+		{
+			name:     "trailing slash",
+			input:    `standard:1.11.0/`,
+			expected: Reference{Flavor: "standard", Version: "1.11.0", Platform: ""},
+		},
+		{
+			name:     "special characters",
+			input:    `abcd-EFGH.01234_:-56789.XYZ_/`,
+			expected: Reference{Flavor: "abcd-efgh.01234_", Version: "-56789.xyz_", Platform: ""},
+		},
+	}
+
+	for _, test := range tests {
+		test := test // pin! see https://github.com/kyoh86/scopelint for why
+
+		t.Run(test.name, func(t *testing.T) {
+			ref, err := ParseReference(test.input)
+			require.NoError(t, err)
+			require.Equal(t, &test.expected, ref)
+		})
+	}
+}
+
+func TestParseReferenceValidatesInput(t *testing.T) {
+	tests := []struct {
+		name        string
+		input       string
+		expectedErr string
+	}{
+		{
+			name:        "empty string",
+			input:       ``,
+			expectedErr: `"" is not a valid GetEnvoy reference. Expected format: <flavor>:<version>[/<platform>]`,
+		},
+		{
+			name:        "empty components",
+			input:       `:/`,
+			expectedErr: `":/" is not a valid GetEnvoy reference. Expected format: <flavor>:<version>[/<platform>]`,
+		},
+		{
+			name:        "no flavor",
+			input:       `:1.11.0/darwin`,
+			expectedErr: `":1.11.0/darwin" is not a valid GetEnvoy reference. Expected format: <flavor>:<version>[/<platform>]`,
+		},
+		{
+			name:        "no version",
+			input:       `standard:/darwin`,
+			expectedErr: `"standard:/darwin" is not a valid GetEnvoy reference. Expected format: <flavor>:<version>[/<platform>]`,
+		},
+		{
+			name:        "invalid character in flavor",
+			input:       `stan dard:1.11.0/darwin`,
+			expectedErr: `"stan dard:1.11.0/darwin" is not a valid GetEnvoy reference. Expected format: <flavor>:<version>[/<platform>]`,
+		},
+	}
+
+	for _, test := range tests {
+		test := test // pin! see https://github.com/kyoh86/scopelint for why
+
+		t.Run(test.name, func(t *testing.T) {
+			actual, err := ParseReference(test.input)
+			require.EqualError(t, err, test.expectedErr)
+			require.Nil(t, actual)
+		})
+	}
+}
+
+func TestParseReferenceNormalizesStringForm(t *testing.T) {
+	tests := []struct {
+		input    string
+		expected string
+	}{
+		{
+			input:    `standard:1.11.0`,
+			expected: `standard:1.11.0`,
+		},
+		{
+			input:    `standard:1.11.0/darwin`,
+			expected: `standard:1.11.0/darwin`,
+		},
+		{
+			input:    `standard:1.11.0/`,
+			expected: `standard:1.11.0`,
+		},
+	}
 
-				Expect(err).ToNot(HaveOccurred())
-				Expect(*ref).To(Equal(given.expected))
-			},
-			Entry("standard:1.11.0", testCase{
-				input:    `standard:1.11.0`,
-				expected: Reference{Flavor: "standard", Version: "1.11.0", Platform: ""},
-			}),
-			Entry("standard:1.11.0/darwin", testCase{
-				input:    `standard:1.11.0/darwin`,
-				expected: Reference{Flavor: "standard", Version: "1.11.0", Platform: "darwin"},
-			}),
-			Entry("standard:1.11.0/linux-glibc", testCase{
-				input:    `standard:1.11.0/linux-glibc`,
-				expected: Reference{Flavor: "standard", Version: "1.11.0", Platform: "linux-glibc"},
-			}),
-			Entry("wasm:1.15", testCase{
-				input:    `wasm:1.15`,
-				expected: Reference{Flavor: "wasm", Version: "1.15", Platform: ""},
-			}),
-			Entry("wasm:1.15/darwin", testCase{
-				input:    `wasm:1.15/darwin`,
-				expected: Reference{Flavor: "wasm", Version: "1.15", Platform: "darwin"},
-			}),
-			Entry("wasm:1.15/linux-glibc", testCase{
-				input:    `wasm:1.15/linux-glib`,
-				expected: Reference{Flavor: "wasm", Version: "1.15", Platform: "linux-glib"},
-			}),
-			Entry("mixed case", testCase{
-				input:    `Wasm:NightlY/LINUX-GLIBC`,
-				expected: Reference{Flavor: "wasm", Version: "nightly", Platform: "linux-glibc"},
-			}),
-			Entry("trailing slash", testCase{
-				input:    `standard:1.11.0/`,
-				expected: Reference{Flavor: "standard", Version: "1.11.0", Platform: ""},
-			}),
-			Entry("special characters", testCase{
-				input:    `abcd-EFGH.01234_:-56789.XYZ_/`,
-				expected: Reference{Flavor: "abcd-efgh.01234_", Version: "-56789.xyz_", Platform: ""},
-			}),
-		)
-	})
-	Describe("invalid input", func() {
-		type testCase struct {
-			input       string
-			expectedErr string
-		}
-		DescribeTable("should fail on invalid input",
-			func(given testCase) {
-				ref, err := ParseReference(given.input)
+	for _, test := range tests {
+		test := test // pin! see https://github.com/kyoh86/scopelint for why
 
-				Expect(err).To(MatchError(given.expectedErr))
-				Expect(ref).To(BeNil())
-			},
-			Entry("empty string", testCase{
-				input:       ``,
-				expectedErr: `"" is not a valid GetEnvoy reference. Expected format: <flavor>:<version>[/<platform>]`,
-			}),
-			Entry("empty components", testCase{
-				input:       `:/`,
-				expectedErr: `":/" is not a valid GetEnvoy reference. Expected format: <flavor>:<version>[/<platform>]`,
-			}),
-			Entry("no flavor", testCase{
-				input:       `:1.11.0/darwin`,
-				expectedErr: `":1.11.0/darwin" is not a valid GetEnvoy reference. Expected format: <flavor>:<version>[/<platform>]`,
-			}),
-			Entry("no version", testCase{
-				input:       `standard:/darwin`,
-				expectedErr: `"standard:/darwin" is not a valid GetEnvoy reference. Expected format: <flavor>:<version>[/<platform>]`,
-			}),
-			Entry("invalid character in flavor", testCase{
-				input:       `stan dard:1.11.0/darwin`,
-				expectedErr: `"stan dard:1.11.0/darwin" is not a valid GetEnvoy reference. Expected format: <flavor>:<version>[/<platform>]`,
-			}),
-		)
-	})
-})
-var _ = Describe("Reference", func() {
-	Describe("ToString()", func() {
-		type testCase struct {
-			input    string
-			expected string
-		}
-		DescribeTable("should parse valid input",
-			func(given testCase) {
-				ref, err := ParseReference(given.input)
-				Expect(err).ToNot(HaveOccurred())
+		t.Run(test.input, func(t *testing.T) {
+			ref, err := ParseReference(test.input)
+			require.NoError(t, err)
 
-				actual := ref.String()
-				Expect(actual).To(Equal(given.expected))
+			// Ensure ref.String() normalizes the input
+			require.Equal(t, test.expected, ref.String())
 
-				ref2, err := ParseReference(actual)
-				Expect(err).ToNot(HaveOccurred())
-				Expect(ref2).To(Equal(ref))
-			},
-			Entry("standard:1.11.0", testCase{
-				input:    `standard:1.11.0`,
-				expected: `standard:1.11.0`,
-			}),
-			Entry("standard:1.11.0/darwin", testCase{
-				input:    `standard:1.11.0/darwin`,
-				expected: `standard:1.11.0/darwin`,
-			}),
-			Entry("standard:1.11.0/", testCase{
-				input:    `standard:1.11.0/`,
-				expected: `standard:1.11.0`,
-			}),
-		)
-	})
-})
+			// Round-trip that the ultimate string form makes the same reference
+			ref2, err := ParseReference(ref.String())
+			require.NoError(t, err)
+			require.Equal(t, ref, ref2)
+		})
+	}
+}
diff --git a/pkg/util/args/args_suite_test.go b/pkg/util/args/args_suite_test.go
deleted file mode 100644
index 7b33408..0000000
--- a/pkg/util/args/args_suite_test.go
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright 2020 Tetrate
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package args_test
-
-import (
-	"testing"
-
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/gomega"
-)
-
-func TestArgs(t *testing.T) {
-	RegisterFailHandler(Fail)
-	RunSpecs(t, "Args Suite")
-}
diff --git a/pkg/util/args/args_test.go b/pkg/util/args/args_test.go
index 5246c4e..2c3327d 100644
--- a/pkg/util/args/args_test.go
+++ b/pkg/util/args/args_test.go
@@ -15,35 +15,36 @@
 package args_test
 
 import (
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/ginkgo/extensions/table"
-	. "github.com/onsi/gomega"
+	"testing"
+
+	"github.com/stretchr/testify/require"
 
 	. "github.com/tetratelabs/getenvoy/pkg/util/args"
 )
 
-var _ = Describe("SplitCommandLine()", func() {
-	type testCase struct {
+func TestSplitCommandLine(t *testing.T) {
+	tests := []struct {
+		name     string
 		input    []string
 		expected []string
-	}
-	DescribeTable("should split command line properly",
-		func(given testCase) {
-			Expect(SplitCommandLine(given.input...)).To(Equal(given.expected))
-		},
-		Entry("nil", testCase{
+	}{
+		{
+			name:     "nil",
 			input:    nil,
 			expected: []string{},
-		}),
-		Entry("empty", testCase{
+		},
+		{
+			name:     "empty",
 			input:    []string{},
 			expected: []string{},
-		}),
-		Entry("already split", testCase{
+		},
+		{
+			name:     "already split",
 			input:    []string{"-e", "VAR=VALUE"},
 			expected: []string{"-e", "VAR=VALUE"},
-		}),
-		Entry("command line", testCase{
+		},
+		{
+			name:  "command line",
 			input: []string{"-e VAR=VALUE -v /host/path:/container/path"},
 			expected: []string{
 				"-e",
@@ -51,13 +52,24 @@ var _ = Describe("SplitCommandLine()", func() {
 				"-v",
 				"/host/path:/container/path",
 			},
-		}),
-		Entry("quoted command line", testCase{
+		},
+		{
+			name:  "quoted command line",
 			input: []string{`'-e VAR=VALUE' "-v /host/path:/container/path"`},
 			expected: []string{
 				"-e VAR=VALUE",
 				"-v /host/path:/container/path",
 			},
-		}),
-	)
-})
+		},
+	}
+
+	for _, test := range tests {
+		test := test // pin! see https://github.com/kyoh86/scopelint for why
+
+		t.Run(test.name, func(t *testing.T) {
+			actual, err := SplitCommandLine(test.input...)
+			require.NoError(t, err)
+			require.Equal(t, test.expected, actual)
+		})
+	}
+}
diff --git a/pkg/util/cmd/cmd_suite_test.go b/pkg/util/cmd/cmd_suite_test.go
deleted file mode 100644
index c89c6f2..0000000
--- a/pkg/util/cmd/cmd_suite_test.go
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright 2020 Tetrate
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package cmd_test
-
-import (
-	"testing"
-
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/gomega"
-)
-
-func TestCmd(t *testing.T) {
-	RegisterFailHandler(Fail)
-	RunSpecs(t, "Cmd Suite")
-}
diff --git a/pkg/util/cmd/cmd_test.go b/pkg/util/cmd/cmd_test.go
index 1bf1b46..3abfe65 100644
--- a/pkg/util/cmd/cmd_test.go
+++ b/pkg/util/cmd/cmd_test.go
@@ -16,131 +16,132 @@ package cmd_test
 
 import (
 	"bytes"
+	"fmt"
 	"syscall"
+	"testing"
 
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/ginkgo/extensions/table"
-	. "github.com/onsi/gomega"
 	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
+	"github.com/stretchr/testify/require"
 
 	commonerrors "github.com/tetratelabs/getenvoy/pkg/errors"
 	. "github.com/tetratelabs/getenvoy/pkg/util/cmd"
 )
 
-var _ = Describe("Execute()", func() {
-
-	var stdout *bytes.Buffer
-	var stderr *bytes.Buffer
-
-	BeforeEach(func() {
-		stdout = new(bytes.Buffer)
-		stderr = new(bytes.Buffer)
-	})
-
-	Describe("should properly format errors", func() {
-		It(`should properly format "unknown command" error`, func() {
-			rootCmd := &cobra.Command{
+func TestExecute(t *testing.T) {
+	stdout, stderr := new(bytes.Buffer), new(bytes.Buffer)
+
+	c := &cobra.Command{
+		Use: "getenvoy",
+		Run: func(_ *cobra.Command, _ []string) {},
+	}
+	c.SetOut(stdout)
+	c.SetErr(stderr)
+	c.SetArgs([]string{})
+
+	err := Execute(c)
+	require.NoError(t, err)
+	require.Empty(t, stdout)
+	require.Empty(t, stderr)
+}
+
+func TestExecuteValidatesArgs(t *testing.T) {
+	tests := []struct {
+		name        string
+		args        []string
+		expectedErr string
+	}{
+		{
+			name:        "unknown flag",
+			args:        []string{"--xyz"},
+			expectedErr: `unknown flag: --xyz`,
+		},
+		{
+			name:        "unknown command",
+			args:        []string{"other command"},
+			expectedErr: `unknown command "other command" for "getenvoy"`,
+		},
+	}
+
+	for _, test := range tests {
+		test := test // pin! see https://github.com/kyoh86/scopelint for why
+
+		t.Run(test.name, func(t *testing.T) {
+			stdout, stderr := new(bytes.Buffer), new(bytes.Buffer)
+
+			c := &cobra.Command{
 				Use: "getenvoy",
+				RunE: func(_ *cobra.Command, _ []string) error {
+					return errors.New("unexpected root error")
+				},
 			}
-			rootCmd.AddCommand(&cobra.Command{
+			c.AddCommand(&cobra.Command{
 				Use: "init",
 				RunE: func(_ *cobra.Command, _ []string) error {
-					return errors.New("unexpected error")
+					return errors.New("unexpected subcommand error")
 				},
 			})
-			rootCmd.SetOut(stdout)
-			rootCmd.SetErr(stderr)
-			rootCmd.SetArgs([]string{"other", "command"})
+			c.SetOut(stdout)
+			c.SetErr(stderr)
+			c.SetArgs(test.args)
+
+			err := Execute(c)
+			require.EqualError(t, err, test.expectedErr, `expected an error running [%v]`, c)
+			require.Empty(t, stdout)
+			expectedStderr := fmt.Sprintf("Error: %s\n\nRun 'getenvoy --help' for usage.\n", test.expectedErr)
+			require.Equal(t, expectedStderr, stderr.String(), `unexpected stderr running [%v]`, c)
+		})
+	}
+}
+
+func TestExecuteApplicationSpecificError(t *testing.T) {
+	tests := []struct {
+		name           string
+		expectedErr    error
+		expectedStderr string
+	}{
+		{
+			name:        "arbitrary error",
+			expectedErr: errors.New("expected error"),
+			expectedStderr: `Error: expected error
 
-			err := Execute(rootCmd)
-			Expect(err).To(HaveOccurred())
+Run 'getenvoy --help' for usage.
+`,
+		},
+		{
+			name:        "shutdown error",
+			expectedErr: commonerrors.NewShutdownError(syscall.SIGINT),
+			expectedStderr: `NOTE: Shutting down early because a Ctrl-C ("interrupt") was received.
+`,
+		},
+		{
+			name:        "wrapped shutdown error",
+			expectedErr: errors.Wrap(commonerrors.NewShutdownError(syscall.SIGINT), "wrapped"),
+			expectedStderr: `NOTE: Shutting down early because a Ctrl-C ("interrupt") was received.
+`,
+		},
+	}
 
-			Expect(stdout.String()).To(BeEmpty())
-			Expect(stderr.String()).To(Equal(`Error: unknown command "other" for "getenvoy"
+	for _, test := range tests {
+		test := test // pin! see https://github.com/kyoh86/scopelint for why
 
-Run 'getenvoy --help' for usage.
-`))
-		})
+		t.Run(test.name, func(t *testing.T) {
+			stdout, stderr := new(bytes.Buffer), new(bytes.Buffer)
 
-		It(`should properly format "unknown flag" error`, func() {
-			rootCmd := &cobra.Command{
+			c := &cobra.Command{
 				Use: "getenvoy",
 				RunE: func(_ *cobra.Command, _ []string) error {
-					return errors.New("unexpected error")
+					return test.expectedErr
 				},
 			}
-			rootCmd.SetOut(stdout)
-			rootCmd.SetErr(stderr)
-			rootCmd.SetArgs([]string{"--xyz"})
-
-			err := Execute(rootCmd)
-			Expect(err).To(HaveOccurred())
-
-			Expect(stdout.String()).To(BeEmpty())
-			Expect(stderr.String()).To(Equal(`Error: unknown flag: --xyz
-
-Run 'getenvoy --help' for usage.
-`))
-		})
-
-		Context("application-specific errors", func() {
-			type testCase struct {
-				err         error
-				expectedOut string
-			}
-			DescribeTable("should properly format application-specific errors",
-				func(given testCase) {
-					rootCmd := &cobra.Command{
-						Use: "getenvoy",
-						RunE: func(_ *cobra.Command, _ []string) error {
-							return given.err
-						},
-					}
-					rootCmd.SetOut(stdout)
-					rootCmd.SetErr(stderr)
-					rootCmd.SetArgs([]string{})
-
-					err := Execute(rootCmd)
-					Expect(err).To(Equal(given.err))
-
-					Expect(stdout.String()).To(BeEmpty())
-					Expect(stderr.String()).To(Equal(given.expectedOut))
-				},
-				Entry("arbitrary error", testCase{
-					err: errors.New("expected error"),
-					expectedOut: `Error: expected error
-
-Run 'getenvoy --help' for usage.
-`,
-				}),
-				Entry("shutdown error", testCase{
-					err: commonerrors.NewShutdownError(syscall.SIGINT),
-					expectedOut: `NOTE: Shutting down early because a Ctrl-C ("interrupt") was received.
-`,
-				}),
-				Entry("wrapped shutdown error", testCase{
-					err: errors.Wrap(commonerrors.NewShutdownError(syscall.SIGINT), "wrapped"),
-					expectedOut: `NOTE: Shutting down early because a Ctrl-C ("interrupt") was received.
-`,
-				}),
-			)
+			c.SetOut(stdout)
+			c.SetErr(stderr)
+			c.SetArgs([]string{})
+
+			err := Execute(c)
+			require.Equal(t, test.expectedErr, err)
+			require.Empty(t, stdout)
+			require.Equal(t, test.expectedStderr, stderr.String())
 		})
-	})
-
-	It(`should support commands that run without an error`, func() {
-		rootCmd := &cobra.Command{
-			Use: "getenvoy",
-			Run: func(_ *cobra.Command, _ []string) {},
-		}
-		rootCmd.SetOut(stdout)
-		rootCmd.SetErr(stderr)
-		rootCmd.SetArgs([]string{})
-
-		err := Execute(rootCmd)
-
-		Expect(err).ToNot(HaveOccurred())
-		Expect(stdout.String()).To(BeEmpty())
-		Expect(stderr.String()).To(BeEmpty())
-	})
-})
+	}
+}
diff --git a/pkg/util/cmd/run_test.go b/pkg/util/cmd/run_test.go
index c017c32..438dafa 100644
--- a/pkg/util/cmd/run_test.go
+++ b/pkg/util/cmd/run_test.go
@@ -16,109 +16,106 @@ package cmd_test
 
 import (
 	"errors"
+	"testing"
 
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/gomega"
 	"github.com/spf13/cobra"
+	"github.com/stretchr/testify/require"
 
 	. "github.com/tetratelabs/getenvoy/pkg/util/cmd"
 )
 
-var _ = Describe("ComposableRunE", func() {
-	Describe("ThenE()", func() {
-		It("should not call the next func if a previous one fails", func() {
-			expectedErr := errors.New("expected")
-			unexpectedErr := errors.New("unexpected")
+func TestComposableRunEThenE(t *testing.T) {
+	expectedErr := errors.New("expected")
 
-			compositeFn := ComposableRunE(func(cmd *cobra.Command, args []string) error {
-				return expectedErr
-			}).ThenE(func(cmd *cobra.Command, args []string) error {
-				return unexpectedErr
-			})
-
-			actualErr := compositeFn(new(cobra.Command), nil)
-
-			Expect(actualErr).To(Equal(expectedErr))
-		})
+	compositeFn := ComposableRunE(func(cmd *cobra.Command, args []string) error {
+		return nil
+	}).ThenE(func(cmd *cobra.Command, args []string) error {
+		return expectedErr
+	})
 
-		It("should call the next func if a previous one doesn't fail", func() {
-			expectedErr := errors.New("expected")
+	actualErr := compositeFn(new(cobra.Command), nil)
 
-			compositeFn := ComposableRunE(func(cmd *cobra.Command, args []string) error {
-				return nil
-			}).ThenE(func(cmd *cobra.Command, args []string) error {
-				return expectedErr
-			})
+	// We expect ThenE to be called because the upstream didn't err
+	require.Equal(t, expectedErr, actualErr)
+}
 
-			actualErr := compositeFn(new(cobra.Command), nil)
+func TestComposableRunEThenEPriorError(t *testing.T) {
+	expectedErr := errors.New("expected")
 
-			Expect(actualErr).To(Equal(expectedErr))
-		})
+	compositeFn := ComposableRunE(func(cmd *cobra.Command, args []string) error {
+		return expectedErr
+	}).ThenE(func(cmd *cobra.Command, args []string) error {
+		return errors.New("unexpected")
 	})
 
-	Describe("Then()", func() {
-		It("should not call the next func if a previous one fails", func() {
-			expectedErr := errors.New("expected")
-			nextCalled := false
+	actualErr := compositeFn(new(cobra.Command), nil)
 
-			compositeFn := ComposableRunE(func(cmd *cobra.Command, args []string) error {
-				return expectedErr
-			}).Then(func(cmd *cobra.Command, args []string) {
-				nextCalled = true
-			})
+	// We expect ThenE to not be called because the upstream erred
+	require.Equal(t, expectedErr, actualErr)
+}
 
-			actualErr := compositeFn(new(cobra.Command), nil)
+func TestComposableRunEThen(t *testing.T) {
+	nextCalled := false
 
-			Expect(actualErr).To(Equal(expectedErr))
-			Expect(nextCalled).To(BeFalse())
-		})
+	compositeFn := ComposableRunE(func(cmd *cobra.Command, args []string) error {
+		return nil
+	}).Then(func(cmd *cobra.Command, args []string) {
+		nextCalled = true
+	})
 
-		It("should call the next func if a previous one doesn't fail", func() {
-			nextCalled := false
+	err := compositeFn(new(cobra.Command), nil)
 
-			compositeFn := ComposableRunE(func(cmd *cobra.Command, args []string) error {
-				return nil
-			}).Then(func(cmd *cobra.Command, args []string) {
-				nextCalled = true
-			})
+	// We expect Then to be called because the upstream didn't err
+	require.NoError(t, err)
+	require.True(t, nextCalled)
+}
 
-			actualErr := compositeFn(new(cobra.Command), nil)
+func TestComposableRunEThenPriorError(t *testing.T) {
+	expectedErr := errors.New("expected")
+	nextCalled := false
 
-			Expect(actualErr).To(BeNil())
-			Expect(nextCalled).To(BeTrue())
-		})
+	compositeFn := ComposableRunE(func(cmd *cobra.Command, args []string) error {
+		return expectedErr
+	}).Then(func(cmd *cobra.Command, args []string) {
+		nextCalled = true
 	})
-})
-
-var _ = Describe("CallParentPersistentPreRunE()", func() {
-	It("should call PersistentPreRunE on the parent", func() {
-		expectedErr := errors.New("expected")
-		root := &cobra.Command{
-			PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
-				return expectedErr
-			},
-		}
-		nested := new(cobra.Command)
-		root.AddCommand(nested)
-
-		actualErr := CallParentPersistentPreRunE()(nested, nil)
-
-		Expect(actualErr).To(Equal(expectedErr))
-	})
-
-	It("should call PersistentPreRun on the parent", func() {
-		parentCalled := false
-		root := &cobra.Command{
-			PersistentPreRun: func(cmd *cobra.Command, args []string) {
-				parentCalled = true
-			},
-		}
-		nested := new(cobra.Command)
-		root.AddCommand(nested)
 
-		actualErr := CallParentPersistentPreRunE()(nested, nil)
-
-		Expect(actualErr).To(BeNil())
-		Expect(parentCalled).To(BeTrue())
-	})
-})
+	actualErr := compositeFn(new(cobra.Command), nil)
+
+	// We expect Then to not be called because the upstream erred
+	require.Equal(t, expectedErr, actualErr)
+	require.False(t, nextCalled)
+}
+
+func TestCallParentPersistentPreRunE(t *testing.T) {
+	parentCalled := false
+	root := &cobra.Command{
+		PersistentPreRun: func(cmd *cobra.Command, args []string) {
+			parentCalled = true
+		},
+	}
+	nested := new(cobra.Command)
+	root.AddCommand(nested)
+
+	err := CallParentPersistentPreRunE()(nested, nil)
+
+	// We expect the parent to be called
+	require.NoError(t, err)
+	require.True(t, parentCalled)
+}
+
+func TestCallParentPersistentPreRunEParentError(t *testing.T) {
+	expectedErr := errors.New("expected")
+	root := &cobra.Command{
+		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
+			return expectedErr
+		},
+	}
+	nested := new(cobra.Command)
+	root.AddCommand(nested)
+
+	err := CallParentPersistentPreRunE()(nested, nil)
+
+	// We expect to see the error from the root's PersistentPreRunE
+	require.Equal(t, expectedErr, err)
+}
diff --git a/pkg/util/os/os_suite_test.go b/pkg/util/os/os_suite_test.go
deleted file mode 100644
index e0f8fe4..0000000
--- a/pkg/util/os/os_suite_test.go
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright 2020 Tetrate
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package os_test
-
-import (
-	"testing"
-
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/gomega"
-)
-
-func TestOs(t *testing.T) {
-	RegisterFailHandler(Fail)
-	RunSpecs(t, "Os Suite")
-}
diff --git a/pkg/util/os/signals_test.go b/pkg/util/os/signals_test.go
index 97e2766..11d6eea 100644
--- a/pkg/util/os/signals_test.go
+++ b/pkg/util/os/signals_test.go
@@ -18,136 +18,158 @@ import (
 	"context"
 	"os"
 	"syscall"
+	"testing"
+	"time"
 
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/gomega"
+	"github.com/stretchr/testify/require"
 )
 
-var _ = Describe("SetupSignalHandler()", func() {
-	It("should register for SIGTERM and SIGINT", func() {
-		Expect(shutdownSignals).To(ConsistOf(syscall.SIGINT, syscall.SIGTERM))
-	})
-
-	Context("with a given set of shutdown signals", func() {
-		var relevantSignal = syscall.SIGUSR1
-		var irrelevantSignal = syscall.SIGUSR2
-
-		var shutdownSignalsBackup []os.Signal
-
-		BeforeEach(func() {
-			shutdownSignalsBackup = shutdownSignals
-			shutdownSignals = []os.Signal{relevantSignal}
-		})
-
-		AfterEach(func() {
-			shutdownSignals = shutdownSignalsBackup
-		})
-
-		It("should not close the returned channel on irrelevant signals", func() {
-			By("doing set up")
-			ctx, cancel := context.WithCancel(context.Background())
-			defer cancel()
-			stopCh := SetupSignalHandler(ctx)
-
-			By("sending an irrelevant signal to the process")
-			Expect(syscall.Kill(syscall.Getpid(), irrelevantSignal)).To(Succeed())
-			Consistently(stopCh).ShouldNot(Receive())
-			Expect(stopCh).NotTo(BeClosed())
-		})
-
-		It("should close the returned channel on the first relevant signal", func() {
-			By("doing set up")
-			ctx, cancel := context.WithCancel(context.Background())
-			defer cancel()
-			stopCh := SetupSignalHandler(ctx)
-
-			By("sending a relevant signal to the process")
-			Expect(syscall.Kill(syscall.Getpid(), relevantSignal)).To(Succeed())
-			Eventually(stopCh).Should(Receive(Equal(relevantSignal)))
-			Eventually(stopCh).Should(BeClosed())
-		})
-
-		Context("with a stub instead of terminate logic", func() {
-
-			var terminateBackup func()
-			var terminateCh chan struct{}
-
-			BeforeEach(func() {
-				terminateBackup = terminate
-				terminateCh = make(chan struct{})
-				terminate = func() {
-					close(terminateCh)
-				}
-			})
-
-			AfterEach(func() {
-				terminate = terminateBackup
-			})
-
-			It("should terminate the process on the second relevant signal", func() {
-				By("doing set up")
-				ctx, cancel := context.WithCancel(context.Background())
-				defer cancel()
-				stopCh := SetupSignalHandler(ctx)
-
-				By("sending first relevant signal to the process")
-				Expect(syscall.Kill(syscall.Getpid(), relevantSignal)).To(Succeed())
-				Eventually(stopCh).Should(Receive(Equal(relevantSignal)))
-				Eventually(stopCh).Should(BeClosed())
-				Consistently(terminateCh).ShouldNot(BeClosed())
-
-				By("sending second relevant signal to the process")
-				Expect(syscall.Kill(syscall.Getpid(), relevantSignal)).To(Succeed())
-				Expect(stopCh).To(BeClosed())
-				Eventually(terminateCh).Should(BeClosed())
-			})
-
-			It("should not terminate the process if the context becomes done before the first signal", func() {
-				By("doing set up")
-				ctx, cancel := context.WithCancel(context.Background())
-				defer cancel()
-				stopCh := SetupSignalHandler(ctx)
-
-				By("canceling the context before the first signal")
-				cancel()
-				// give SetupSignalHandler some time to notice that the context is done
-				Consistently(stopCh).ShouldNot(BeClosed())
-
-				By("sending first relevant signal to the process")
-				Expect(syscall.Kill(syscall.Getpid(), relevantSignal)).To(Succeed())
-				Consistently(stopCh).ShouldNot(Receive())
-				Expect(stopCh).NotTo(BeClosed())
-				Consistently(terminateCh).ShouldNot(BeClosed())
-
-				By("sending second relevant signal to the process")
-				Expect(syscall.Kill(syscall.Getpid(), relevantSignal)).To(Succeed())
-				Consistently(stopCh).ShouldNot(Receive())
-				Expect(stopCh).NotTo(BeClosed())
-				Consistently(terminateCh).ShouldNot(BeClosed())
-			})
-
-			It("should not terminate the process if the context becomes done before the second signal", func() {
-				By("doing set up")
-				ctx, cancel := context.WithCancel(context.Background())
-				defer cancel()
-				stopCh := SetupSignalHandler(ctx)
-
-				By("sending first relevant signal to the process")
-				Expect(syscall.Kill(syscall.Getpid(), relevantSignal)).To(Succeed())
-				Eventually(stopCh).Should(Receive(Equal(relevantSignal)))
-				Eventually(stopCh).Should(BeClosed())
-				Consistently(terminateCh).ShouldNot(BeClosed())
-
-				By("canceling the context before the second signal")
-				cancel()
-				// give SetupSignalHandler some time to notice that the context is done
-				Consistently(terminateCh).ShouldNot(BeClosed())
-
-				By("sending second relevant signal to the process")
-				Expect(syscall.Kill(syscall.Getpid(), relevantSignal)).To(Succeed())
-				Expect(stopCh).To(BeClosed())
-				Consistently(terminateCh).ShouldNot(BeClosed())
-			})
-		})
-	})
-})
+func TestShutdownSignals(t *testing.T) {
+	// This is a base-case, just verifying the default values
+	require.Equal(t, []os.Signal{syscall.SIGINT, syscall.SIGTERM}, shutdownSignals)
+}
+
+func TestSetupSignalHandlerCatchesRelevantSignalAndClosesTheChannel(t *testing.T) {
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	stopCh := SetupSignalHandler(ctx)
+
+	// send a relevant signal to the process
+	err := syscall.Kill(syscall.Getpid(), syscall.SIGINT)
+	require.NoError(t, err)
+
+	requireSignal(t, syscall.SIGINT, stopCh)
+	requireChannelClosed(t, stopCh)
+}
+
+func TestSetupSignalHandlerIgnoresWhenContextCanceledBeforeSignal(t *testing.T) {
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	stopCh := SetupSignalHandler(ctx)
+
+	cancel() // context canceled
+
+	// send a relevant signal to the process
+	err := syscall.Kill(syscall.Getpid(), syscall.SIGINT)
+	require.NoError(t, err)
+
+	// The signal is ignored because the context closed prior to receiving it
+	requireNoSignal(t, stopCh)
+}
+
+func TestSetupSignalHandlerIgnoresIrrelevantSignal(t *testing.T) {
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	stopCh := SetupSignalHandler(ctx)
+
+	// send an irrelevant signal to the process
+	err := syscall.Kill(syscall.Getpid(), syscall.SIGUSR2)
+	require.NoError(t, err)
+
+	requireNoSignal(t, stopCh)
+}
+
+func requireSignal(t *testing.T, expected os.Signal, ch <-chan os.Signal) {
+	require.Eventually(t, func() bool {
+		select {
+		case s, ok := <-ch:
+			return s == expected && ok
+		default:
+			return false
+		}
+	}, 50*time.Millisecond, 10*time.Millisecond)
+}
+
+func requireChannelClosed(t *testing.T, ch <-chan os.Signal) {
+	select {
+	case _, ok := <-ch:
+		require.False(t, ok)
+	default:
+		t.Fatal()
+	}
+}
+
+// requireNoSignal ensures the channel is open, but there are no signals in it.
+func requireNoSignal(t *testing.T, ch <-chan os.Signal) {
+	require.Never(t, func() bool {
+		select {
+		case v, ok := <-ch:
+			return v != nil || ok
+		default:
+			return false
+		}
+	}, 100*time.Millisecond, 10*time.Millisecond)
+}
+
+// overrideTerminateWithBool returns a boolean made true on terminate. The function returned reverts the original.
+func overrideTerminateWithBool() (*bool, func()) {
+	previous := terminate
+	closed := false
+	terminate = func() {
+		closed = true
+	}
+	return &closed, func() {
+		terminate = previous
+	}
+}
+
+func TestSetupSignalHandlerTerminatesOnSecondRelevantSignal(t *testing.T) {
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	terminated, revertTerminate := overrideTerminateWithBool()
+	defer revertTerminate()
+
+	stopCh := SetupSignalHandler(ctx)
+
+	// send a relevant signal to the process
+	err := syscall.Kill(syscall.Getpid(), syscall.SIGINT)
+	require.NoError(t, err)
+
+	// First relevant signal closes the channel, but doesn't terminate
+	requireSignal(t, syscall.SIGINT, stopCh)
+	requireChannelClosed(t, stopCh)
+	require.False(t, *terminated)
+
+	// send another relevant signal to the process
+	err = syscall.Kill(syscall.Getpid(), syscall.SIGINT)
+	require.NoError(t, err)
+
+	// Second relevant signal terminates the process
+	require.Eventually(t, func() bool {
+		return *terminated
+	}, 50*time.Millisecond, 10*time.Millisecond)
+}
+
+func TestSetupSignalHandlerDoesntTerminateWhenContextCanceledBeforeSecondRelevantSignal(t *testing.T) {
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	terminated, revertTerminate := overrideTerminateWithBool()
+	defer revertTerminate()
+
+	stopCh := SetupSignalHandler(ctx)
+
+	// send a relevant signal to the process
+	err := syscall.Kill(syscall.Getpid(), syscall.SIGINT)
+	require.NoError(t, err)
+
+	// First relevant signal closes the channel, but doesn't terminate
+	requireSignal(t, syscall.SIGINT, stopCh)
+	requireChannelClosed(t, stopCh)
+	require.False(t, *terminated)
+
+	cancel() // context canceled
+
+	// send another relevant signal to the process
+	err = syscall.Kill(syscall.Getpid(), syscall.SIGINT)
+	require.NoError(t, err)
+
+	// Second relevant signal doesn't terminate the process
+	require.Never(t, func() bool {
+		return *terminated
+	}, 50*time.Millisecond, 10*time.Millisecond)
+}
diff --git a/pkg/version/version_suite_test.go b/pkg/version/version_suite_test.go
deleted file mode 100644
index 6d8bbf7..0000000
--- a/pkg/version/version_suite_test.go
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright 2020 Tetrate
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package version_test
-
-import (
-	"testing"
-
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/gomega"
-)
-
-func TestVersion(t *testing.T) {
-	RegisterFailHandler(Fail)
-	RunSpecs(t, "Version Suite")
-}
diff --git a/pkg/version/version_test.go b/pkg/version/version_test.go
index ee4ffcb..5ddd910 100644
--- a/pkg/version/version_test.go
+++ b/pkg/version/version_test.go
@@ -15,79 +15,78 @@
 package version
 
 import (
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/ginkgo/extensions/table"
-	. "github.com/onsi/gomega"
-)
-
-var _ = Describe("versionOrDefault()", func() {
-
-	var backupVersion string
-
-	BeforeEach(func() {
-		backupVersion = version
-	})
+	"testing"
 
-	AfterEach(func() {
-		version = backupVersion
-	})
+	"github.com/stretchr/testify/require"
+)
 
-	type testCase struct {
+func TestVersionOrDefault(t *testing.T) {
+	tests := []struct {
+		name     string
 		version  string
 		expected string
-	}
-
-	DescribeTable("should fallback to `dev` version if `pkg/version.version` was not set via compiler options",
-		func(given testCase) {
-			version = given.version
-
-			Expect(versionOrDefault()).To(Equal(given.expected))
-		},
-		Entry("ad-hoc build", testCase{
+	}{
+		{
+			name:     "ad-hoc build",
 			version:  "",
 			expected: "dev",
-		}),
-		Entry("dev build", testCase{
+		},
+		{
+			name:     "dev build",
 			version:  "dev",
 			expected: "dev",
-		}),
-		Entry("release build", testCase{
+		},
+		{
+			name:     "release build",
 			version:  "0.0.1",
 			expected: "0.0.1",
-		}),
-	)
-})
-
-var _ = Describe("IsDevBuild()", func() {
+		},
+	}
 
-	var backupBuild BuildInfo
+	for _, test := range tests {
+		test := test // pin! see https://github.com/kyoh86/scopelint for why
 
-	BeforeEach(func() {
-		backupBuild = Build
-	})
+		t.Run(test.name, func(t *testing.T) {
+			previous := version
+			defer func() {
+				version = previous
+			}()
 
-	AfterEach(func() {
-		Build = backupBuild
-	})
+			version = test.version
+			require.Equal(t, test.expected, versionOrDefault())
+		})
+	}
+}
 
-	type testCase struct {
+func TestIsDevBuild(t *testing.T) {
+	tests := []struct {
+		name     string
 		build    BuildInfo
 		expected bool
-	}
-
-	DescribeTable("should consider builds with `pkg/version.version` unset or set to `dev` as 'development builds'",
-		func(given testCase) {
-			Build = given.build
-
-			Expect(IsDevBuild()).To(Equal(given.expected))
-		},
-		Entry("dev build", testCase{
+	}{
+		{
+			name:     "dev build",
 			build:    BuildInfo{Version: "dev"},
 			expected: true,
-		}),
-		Entry("release build", testCase{
+		},
+		{
+			name:     "release build",
 			build:    BuildInfo{Version: "0.0.1"},
 			expected: false,
-		}),
-	)
-})
+		},
+	}
+
+	for _, test := range tests {
+		test := test // pin! see https://github.com/kyoh86/scopelint for why
+
+		t.Run(test.name, func(t *testing.T) {
+			previous := Build
+			defer func() {
+				Build = previous
+			}()
+
+			Build = test.build
+			require.Equal(t, test.expected, IsDevBuild())
+		})
+	}
+}