Repository: exercism/cli
Pull request #540 (2018-06-10 22:09:01)

Break out test into subtests.
This allow the test tools to show individuals reports.
Other tests in the CLI uses the pattern of having a
slice of input+expectedOutput+name, they may also benefit
form this solution (I can make the change if you like this
commit)

kytrinyx: I'm going to let @nywilken review and merge this, but had a small unrelated comment. It looks like GitHub doesn't recognize the email that you've used to commit with. You can tweak this in https://github.com/settings/emails without making your email visible on your profile.
FabienTregan: I think it's ok now :)
kytrinyx: > I think it's ok now

Indeed! Nicely done 🌷 
nywilken: @FabienTregan is there anytime else I can help with on this PR? There’s no rush; I want to make sure that you are getting the feedback you need to get this merged. Cheers!
FabienTregan: @nywilken No it's ok, I think I'll be done tomorrow morning for this one (paris time).

I only refactored the tests I needed to reach a first milestone of PR #539, but now it's done I think I'll refactor the remaining tests tomorrow morning (paris time)

Thanks to you and @kytrinyx , I really leant a lot this week and coding was fun :)
FabienTregan: The same pattern can be applyed in those test files :
- [ ] api/client_test.go
- [ ] cli/cli_test.go
- [x] cmd/configure_test.go
- [ ] cmd/download_test.go
- [ ] cmd/upgrade_test.go
- [ ] cmd/version_test.go
- [ ] comms/question_test.go
- [ ] comm/selection_test.go (tests)
- [ ] comm/selection_test.go (things)
- [ ] config/track_test.go
- [ ] config/user_config_test.go
- [ ] config/user_config_windows_test.go
- [x] workspace/path_type_test.go
- [ ] workspace/solution_test.go (1)
- [ ] workspace/solution_test.go (2)
- [ ] workspace/transmission_test.go
- [ ] workspace/workspace_test.go (1)
- [ ] workspace/workspace_test.go (2)
- [ ] workspace/workspace_test.go (3)
- [ ] workspace/workspace_test.go (4)

nywilken: This is looking good. Having the comments actually turned into test case descriptions is a bonus within this refactor.   I’ll take another pass once behind a machine to get this merged.  


Patch:
diff --git a/comms/question.go b/comms/question.go
index 494045b..f9afd6a 100644
--- a/comms/question.go
+++ b/comms/question.go
@@ -22,7 +22,7 @@ func (q Question) Read(r io.Reader) (string, error) {
 	if err != nil {
 		return "", err
 	}
-	s = strings.Trim(s, "\n")
+	s = strings.TrimSpace(s)
 	if s == "" {
 		return q.DefaultValue, nil
 	}


Test patch:
diff --git a/api/client_test.go b/api/client_test.go
index 4f3c6f9..ba069b7 100644
--- a/api/client_test.go
+++ b/api/client_test.go
@@ -18,19 +18,20 @@ func TestNewRequestSetsDefaultHeaders(t *testing.T) {
 
 	UserAgent = "BogusAgent"
 
-	tests := []struct {
+	testCases := []struct {
+		desc        string
 		client      *Client
 		auth        string
 		contentType string
 	}{
 		{
-			// Use defaults.
+			desc:        "User defaults",
 			client:      &Client{},
 			auth:        "",
 			contentType: "application/json",
 		},
 		{
-			// Override defaults.
+			desc: "Override defaults",
 			client: &Client{
 				UserConfig:  &config.UserConfig{Token: "abc123"},
 				ContentType: "bogus",
@@ -40,12 +41,14 @@ func TestNewRequestSetsDefaultHeaders(t *testing.T) {
 		},
 	}
 
-	for _, test := range tests {
-		req, err := test.client.NewRequest("GET", ts.URL, nil)
-		assert.NoError(t, err)
-		assert.Equal(t, "BogusAgent", req.Header.Get("User-Agent"))
-		assert.Equal(t, test.contentType, req.Header.Get("Content-Type"))
-		assert.Equal(t, test.auth, req.Header.Get("Authorization"))
+	for _, tc := range testCases {
+		t.Run(tc.desc, func(t *testing.T) {
+			req, err := tc.client.NewRequest("GET", ts.URL, nil)
+			assert.NoError(t, err)
+			assert.Equal(t, "BogusAgent", req.Header.Get("User-Agent"))
+			assert.Equal(t, tc.contentType, req.Header.Get("Content-Type"))
+			assert.Equal(t, tc.auth, req.Header.Get("Authorization"))
+		})
 	}
 }
 
diff --git a/cli/cli_test.go b/cli/cli_test.go
index b7968a0..c9e71e3 100644
--- a/cli/cli_test.go
+++ b/cli/cli_test.go
@@ -10,46 +10,49 @@ import (
 )
 
 func TestIsUpToDate(t *testing.T) {
-	tests := []struct {
+	testCases := []struct {
+		desc       string
 		cliVersion string
 		releaseTag string
 		ok         bool
 	}{
 		{
-			// It returns false for versions less than release.
+			desc:       "It returns false for versions less than release.",
 			cliVersion: "1.0.0",
 			releaseTag: "v1.0.1",
 			ok:         false,
 		},
 		{
-			// It returns false for pre-release versions of release.
+			desc:       "It returns false for pre-release versions of release.",
 			cliVersion: "1.0.1-alpha.1",
 			releaseTag: "v1.0.1",
 			ok:         false,
 		},
 		{
-			// It returns true for versions equal to release.
+			desc:       "It returns true for versions equal to release.",
 			cliVersion: "2.0.1",
 			releaseTag: "v2.0.1",
 			ok:         true,
 		},
 		{
-			// It returns true for versions greater than release.
+			desc:       "It returns true for versions greater than release.",
 			cliVersion: "2.0.2",
 			releaseTag: "v2.0.1",
 			ok:         true,
 		},
 	}
 
-	for _, test := range tests {
-		c := &CLI{
-			Version:       test.cliVersion,
-			LatestRelease: &Release{TagName: test.releaseTag},
-		}
+	for _, tc := range testCases {
+		t.Run(tc.desc, func(t *testing.T) {
+			c := &CLI{
+				Version:       tc.cliVersion,
+				LatestRelease: &Release{TagName: tc.releaseTag},
+			}
 
-		ok, err := c.IsUpToDate()
-		assert.NoError(t, err)
-		assert.Equal(t, test.ok, ok, test.cliVersion)
+			ok, err := c.IsUpToDate()
+			assert.NoError(t, err)
+			assert.Equal(t, tc.ok, ok, tc.cliVersion)
+		})
 	}
 }
 
diff --git a/cmd/configure_test.go b/cmd/configure_test.go
index 46bb326..d1d81ed 100644
--- a/cmd/configure_test.go
+++ b/cmd/configure_test.go
@@ -7,16 +7,18 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
+type testCase struct {
+	desc           string
+	args           []string
+	existingUsrCfg *config.UserConfig
+	expectedUsrCfg *config.UserConfig
+	existingAPICfg *config.APIConfig
+	expectedAPICfg *config.APIConfig
+}
+
 func TestConfigure(t *testing.T) {
-	tests := []struct {
-		desc           string
-		args           []string
-		existingUsrCfg *config.UserConfig
-		expectedUsrCfg *config.UserConfig
-		existingAPICfg *config.APIConfig
-		expectedAPICfg *config.APIConfig
-	}{
-		{
+	testCases := []testCase{
+		testCase{
 			desc:           "It writes the flags when there is no config file.",
 			args:           []string{"fakeapp", "configure", "--token", "a", "--workspace", "/a", "--api", "http://example.com"},
 			existingUsrCfg: nil,
@@ -24,7 +26,7 @@ func TestConfigure(t *testing.T) {
 			existingAPICfg: nil,
 			expectedAPICfg: &config.APIConfig{BaseURL: "http://example.com"},
 		},
-		{
+		testCase{
 			desc:           "It overwrites the flags in the config file.",
 			args:           []string{"fakeapp", "configure", "--token", "b", "--workspace", "/b", "--api", "http://example.com/v2"},
 			existingUsrCfg: &config.UserConfig{Token: "token-b", Workspace: "/workspace-b"},
@@ -32,13 +34,13 @@ func TestConfigure(t *testing.T) {
 			existingAPICfg: &config.APIConfig{BaseURL: "http://example.com/v1"},
 			expectedAPICfg: &config.APIConfig{BaseURL: "http://example.com/v2"},
 		},
-		{
+		testCase{
 			desc:           "It overwrites the flags that are passed, without losing the ones that are not.",
 			args:           []string{"fakeapp", "configure", "--token", "c"},
 			existingUsrCfg: &config.UserConfig{Token: "token-c", Workspace: "/workspace-c"},
 			expectedUsrCfg: &config.UserConfig{Token: "c", Workspace: "/workspace-c"},
 		},
-		{
+		testCase{
 			desc:           "It gets the default API base URL.",
 			args:           []string{"fakeapp", "configure"},
 			existingAPICfg: &config.APIConfig{},
@@ -46,37 +48,44 @@ func TestConfigure(t *testing.T) {
 		},
 	}
 
-	for _, test := range tests {
+	for _, tc := range testCases {
+		t.Run(tc.desc, makeTest(tc))
+	}
+}
+
+func makeTest(tc testCase) func(*testing.T) {
+
+	return func(t *testing.T) {
 		cmdTest := &CommandTest{
 			Cmd:    configureCmd,
 			InitFn: initConfigureCmd,
-			Args:   test.args,
+			Args:   tc.args,
 		}
 		cmdTest.Setup(t)
 		defer cmdTest.Teardown(t)
 
-		if test.existingUsrCfg != nil {
+		if tc.existingUsrCfg != nil {
 			// Write a fake config.
 			cfg := config.NewEmptyUserConfig()
-			cfg.Token = test.existingUsrCfg.Token
-			cfg.Workspace = test.existingUsrCfg.Workspace
+			cfg.Token = tc.existingUsrCfg.Token
+			cfg.Workspace = tc.existingUsrCfg.Workspace
 			err := cfg.Write()
-			assert.NoError(t, err, test.desc)
+			assert.NoError(t, err, tc.desc)
 		}
 
 		cmdTest.App.Execute()
 
-		if test.expectedUsrCfg != nil {
+		if tc.expectedUsrCfg != nil {
 			usrCfg, err := config.NewUserConfig()
-			assert.NoError(t, err, test.desc)
-			assert.Equal(t, test.expectedUsrCfg.Token, usrCfg.Token, test.desc)
-			assert.Equal(t, test.expectedUsrCfg.Workspace, usrCfg.Workspace, test.desc)
+			assert.NoError(t, err, tc.desc)
+			assert.Equal(t, tc.expectedUsrCfg.Token, usrCfg.Token, tc.desc)
+			assert.Equal(t, tc.expectedUsrCfg.Workspace, usrCfg.Workspace, tc.desc)
 		}
 
-		if test.expectedAPICfg != nil {
+		if tc.expectedAPICfg != nil {
 			apiCfg, err := config.NewAPIConfig()
-			assert.NoError(t, err, test.desc)
-			assert.Equal(t, test.expectedAPICfg.BaseURL, apiCfg.BaseURL, test.desc)
+			assert.NoError(t, err, tc.desc)
+			assert.Equal(t, tc.expectedAPICfg.BaseURL, apiCfg.BaseURL, tc.desc)
 		}
 	}
 }
diff --git a/cmd/download_test.go b/cmd/download_test.go
index 5129de6..406fad6 100644
--- a/cmd/download_test.go
+++ b/cmd/download_test.go
@@ -13,9 +13,37 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
+const payloadTemplate = `
+{
+	"solution": {
+		"id": "bogus-id",
+		"user": {
+			"handle": "alice",
+			"is_requester": true
+		},
+		"exercise": {
+			"id": "bogus-exercise",
+			"instructions_url": "http://example.com/bogus-exercise",
+			"auto_approve": false,
+			"track": {
+				"id": "bogus-track",
+				"language": "Bogus Language"
+			}
+		},
+		"file_download_base_url": "%s",
+		"files": [
+		"%s",
+		"%s",
+		"%s"
+		],
+		"iteration": {
+			"submitted_at": "2017-08-21t10:11:12.130z"
+		}
+	}
+}
+`
+
 func TestDownload(t *testing.T) {
-	// Let's not actually print to standard out while testing.
-	Out = ioutil.Discard
 
 	cmdTest := &CommandTest{
 		Cmd:    downloadCmd,
@@ -25,45 +53,66 @@ func TestDownload(t *testing.T) {
 	cmdTest.Setup(t)
 	defer cmdTest.Teardown(t)
 
-	// Write a fake user config setting the workspace to the temp dir.
-	userCfg := config.NewEmptyUserConfig()
-	userCfg.Workspace = cmdTest.TmpDir
-	err := userCfg.Write()
+	mockServer := makeMockServer()
+	defer mockServer.Close()
+
+	err := writeFakeUserConfigSetting(cmdTest.TmpDir)
+	assert.NoError(t, err)
+	err = writeFakeAPIConfigSetting(mockServer.URL)
 	assert.NoError(t, err)
 
-	payloadBody := `
-	{
-		"solution": {
-			"id": "bogus-id",
-			"user": {
-				"handle": "alice",
-				"is_requester": true
-			},
-			"exercise": {
-				"id": "bogus-exercise",
-				"instructions_url": "http://example.com/bogus-exercise",
-				"auto_approve": false,
-				"track": {
-					"id": "bogus-track",
-					"language": "Bogus Language"
-				}
-			},
-			"file_download_base_url": "%s",
-			"files": [
-			"%s",
-			"%s",
-			"%s"
-			],
-			"iteration": {
-				"submitted_at": "2017-08-21t10:11:12.130z"
-			}
-		}
+	testCases := []struct {
+		desc     string
+		path     string
+		contents string
+	}{
+		{
+			desc:     "It should download a file.",
+			path:     filepath.Join(cmdTest.TmpDir, "bogus-track", "bogus-exercise", "file-1.txt"),
+			contents: "this is file 1",
+		},
+		{
+			desc:     "It should download a file in a subdir.",
+			path:     filepath.Join(cmdTest.TmpDir, "bogus-track", "bogus-exercise", "subdir", "file-2.txt"),
+			contents: "this is file 2",
+		},
+		{
+			desc:     "It creates the .solution.json file.",
+			path:     filepath.Join(cmdTest.TmpDir, "bogus-track", "bogus-exercise", ".solution.json"),
+			contents: `{"track":"bogus-track","exercise":"bogus-exercise","id":"bogus-id","url":"","handle":"alice","is_requester":true,"auto_approve":false}`,
+		},
+	}
+
+	cmdTest.App.Execute()
+
+	for _, tc := range testCases {
+		t.Run(tc.desc, func(t *testing.T) {
+			b, err := ioutil.ReadFile(tc.path)
+			assert.NoError(t, err)
+			assert.Equal(t, tc.contents, string(b))
+		})
 	}
-	`
 
+	path := filepath.Join(cmdTest.TmpDir, "bogus-track", "bogus-exercise", "file-3.txt")
+	_, err = os.Lstat(path)
+	assert.True(t, os.IsNotExist(err), "It should not write the file if empty.")
+}
+
+func writeFakeUserConfigSetting(tmpDirPath string) error {
+	userCfg := config.NewEmptyUserConfig()
+	userCfg.Workspace = tmpDirPath
+	return userCfg.Write()
+}
+
+func writeFakeAPIConfigSetting(serverURL string) error {
+	apiCfg := config.NewEmptyAPIConfig()
+	apiCfg.BaseURL = serverURL
+	return apiCfg.Write()
+}
+
+func makeMockServer() *httptest.Server {
 	mux := http.NewServeMux()
 	server := httptest.NewServer(mux)
-	defer server.Close()
 
 	path1 := "file-1.txt"
 	mux.HandleFunc("/"+path1, func(w http.ResponseWriter, r *http.Request) {
@@ -80,45 +129,11 @@ func TestDownload(t *testing.T) {
 		fmt.Fprint(w, "")
 	})
 
-	payloadBody = fmt.Sprintf(payloadBody, server.URL+"/", path1, path2, path3)
+	payloadBody := fmt.Sprintf(payloadTemplate, server.URL+"/", path1, path2, path3)
 	mux.HandleFunc("/solutions/latest", func(w http.ResponseWriter, r *http.Request) {
 		fmt.Fprint(w, payloadBody)
 	})
 
-	// Write a fake api config setting the base url to the test server.
-	apiCfg := config.NewEmptyAPIConfig()
-	apiCfg.BaseURL = server.URL
-	err = apiCfg.Write()
-	assert.NoError(t, err)
-
-	tests := []struct {
-		path     string
-		contents string
-	}{
-		{
-			path:     filepath.Join(cmdTest.TmpDir, "bogus-track", "bogus-exercise", "file-1.txt"),
-			contents: "this is file 1",
-		},
-		{
-			path:     filepath.Join(cmdTest.TmpDir, "bogus-track", "bogus-exercise", "subdir", "file-2.txt"),
-			contents: "this is file 2",
-		},
-		{
-			path:     filepath.Join(cmdTest.TmpDir, "bogus-track", "bogus-exercise", ".solution.json"),
-			contents: `{"track":"bogus-track","exercise":"bogus-exercise","id":"bogus-id","url":"","handle":"alice","is_requester":true,"auto_approve":false}`,
-		},
-	}
-
-	cmdTest.App.Execute()
-
-	for _, test := range tests {
-		b, err := ioutil.ReadFile(test.path)
-		assert.NoError(t, err)
-		assert.Equal(t, test.contents, string(b))
-	}
+	return server
 
-	// It doesn't write the empty file.
-	path := filepath.Join(cmdTest.TmpDir, "bogus-track", "bogus-exercise", path3)
-	_, err = os.Lstat(path)
-	assert.True(t, os.IsNotExist(err))
 }
diff --git a/cmd/upgrade_test.go b/cmd/upgrade_test.go
index 6c34596..e2fd094 100644
--- a/cmd/upgrade_test.go
+++ b/cmd/upgrade_test.go
@@ -26,7 +26,7 @@ func TestUpgrade(t *testing.T) {
 	Out = ioutil.Discard
 	defer func() { Out = oldOut }()
 
-	tests := []struct {
+	testCases := []struct {
 		desc     string
 		upToDate bool
 		expected bool
@@ -43,11 +43,13 @@ func TestUpgrade(t *testing.T) {
 		},
 	}
 
-	for _, test := range tests {
-		fc := &fakeCLI{UpToDate: test.upToDate}
+	for _, tc := range testCases {
+		t.Run(tc.desc, func(t *testing.T) {
+			fc := &fakeCLI{UpToDate: tc.upToDate}
 
-		err := updateCLI(fc)
-		assert.NoError(t, err)
-		assert.Equal(t, test.expected, fc.UpgradeCalled, test.desc)
+			err := updateCLI(fc)
+			assert.NoError(t, err)
+			assert.Equal(t, tc.expected, fc.UpgradeCalled)
+		})
 	}
 }
diff --git a/cmd/version_test.go b/cmd/version_test.go
index e2a55a1..6c89162 100644
--- a/cmd/version_test.go
+++ b/cmd/version_test.go
@@ -25,36 +25,39 @@ func TestVersionUpdateCheck(t *testing.T) {
 	defer ts.Close()
 	cli.ReleaseURL = ts.URL
 
-	tests := []struct {
+	testCases := []struct {
+		desc     string
 		version  string
 		expected string
 	}{
 		{
-			// It returns new version available for versions older than latest.
+			desc:     "It returns new version available for versions older than latest.",
 			version:  "1.0.0",
 			expected: "A new CLI version is available. Run `exercism upgrade` to update to 2.0.0",
 		},
 		{
-			// It returns up to date for versions matching latest.
+			desc:     "It returns up to date for versions matching latest.",
 			version:  "2.0.0",
 			expected: "Your CLI version is up to date.",
 		},
 		{
-			// It returns up to date for versions newer than latest.
+			desc:     "It returns up to date for versions newer than latest.",
 			version:  "2.0.1",
 			expected: "Your CLI version is up to date.",
 		},
 	}
 
-	for _, test := range tests {
-		c := &cli.CLI{
-			Version: test.version,
-		}
+	for _, tc := range testCases {
+		t.Run(tc.desc, func(t *testing.T) {
+			c := &cli.CLI{
+				Version: tc.version,
+			}
 
-		actual, err := checkForUpdate(c)
+			actual, err := checkForUpdate(c)
 
-		assert.NoError(t, err)
-		assert.NotEmpty(t, actual)
-		assert.Equal(t, test.expected, actual)
+			assert.NoError(t, err)
+			assert.NotEmpty(t, actual)
+			assert.Equal(t, tc.expected, actual)
+		})
 	}
 }
diff --git a/comms/question_test.go b/comms/question_test.go
index f7721ae..1b77dad 100644
--- a/comms/question_test.go
+++ b/comms/question_test.go
@@ -9,7 +9,7 @@ import (
 )
 
 func TestQuestion(t *testing.T) {
-	tests := []struct {
+	testCases := []struct {
 		desc     string
 		given    string
 		fallback string
@@ -17,17 +17,22 @@ func TestQuestion(t *testing.T) {
 	}{
 		{"records interactive response", "hello\n", "", "hello"},
 		{"responds with default if response is empty", "\n", "Fine.", "Fine."},
+		{"removes trailing \\r in addition to trailing \\", "hello\r\n", "Fine.", "hello"},
+		{"removes trailing white spaces", "hello  \n", "Fine.", "hello"},
+		{"falls back to default value", "  \n", "Default", "Default"},
 	}
-	for _, test := range tests {
-		q := &Question{
-			Reader:       strings.NewReader(test.given),
-			Writer:       ioutil.Discard,
-			Prompt:       "Say something: ",
-			DefaultValue: test.fallback,
-		}
+	for _, tc := range testCases {
+		t.Run(tc.desc, func(t *testing.T) {
+			q := &Question{
+				Reader:       strings.NewReader(tc.given),
+				Writer:       ioutil.Discard,
+				Prompt:       "Say something: ",
+				DefaultValue: tc.fallback,
+			}
 
-		answer, err := q.Ask()
-		assert.NoError(t, err)
-		assert.Equal(t, answer, test.expected, test.desc)
+			answer, err := q.Ask()
+			assert.NoError(t, err)
+			assert.Equal(t, answer, tc.expected)
+		})
 	}
 }
diff --git a/comms/selection_test.go b/comms/selection_test.go
index ad6bc55..bbf2ce1 100644
--- a/comms/selection_test.go
+++ b/comms/selection_test.go
@@ -78,7 +78,7 @@ func TestSelectionRead(t *testing.T) {
 }
 
 func TestSelectionPick(t *testing.T) {
-	tests := []struct {
+	testCases := []struct {
 		desc      string
 		selection Selection
 		things    []thing
@@ -111,16 +111,18 @@ func TestSelectionPick(t *testing.T) {
 		},
 	}
 
-	for _, test := range tests {
-		test.selection.Writer = ioutil.Discard
-		for _, th := range test.things {
-			test.selection.Items = append(test.selection.Items, th)
-		}
-
-		item, err := test.selection.Pick("which one? %s")
-		assert.NoError(t, err)
-		th, ok := item.(thing)
-		assert.True(t, ok)
-		assert.Equal(t, test.expected, th.name)
+	for _, tc := range testCases {
+		t.Run(tc.desc, func(t *testing.T) {
+			tc.selection.Writer = ioutil.Discard
+			for _, th := range tc.things {
+				tc.selection.Items = append(tc.selection.Items, th)
+			}
+
+			item, err := tc.selection.Pick("which one? %s")
+			assert.NoError(t, err)
+			th, ok := item.(thing)
+			assert.True(t, ok)
+			assert.Equal(t, tc.expected, th.name)
+		})
 	}
 }
diff --git a/config/track_test.go b/config/track_test.go
index 8440e48..55f79ec 100644
--- a/config/track_test.go
+++ b/config/track_test.go
@@ -14,16 +14,26 @@ func TestTrackIgnoreString(t *testing.T) {
 		},
 	}
 
-	tests := map[string]bool{
+	testCases := map[string]bool{
 		"falcon.txt": false,
 		"beacon|txt": true,
 		"beacon.ext": true,
 		"proof":      false,
 	}
 
-	for name, acceptable := range tests {
-		ok, err := track.AcceptFilename(name)
-		assert.NoError(t, err, name)
-		assert.Equal(t, acceptable, ok, name)
+	for name, acceptable := range testCases {
+		testName := name + " should " + notIfNeeded(acceptable) + "be an acceptable name."
+		t.Run(testName, func(t *testing.T) {
+			ok, err := track.AcceptFilename(name)
+			assert.NoError(t, err, name)
+			assert.Equal(t, acceptable, ok, testName)
+		})
 	}
 }
+
+func notIfNeeded(b bool) string {
+	if !b {
+		return "not "
+	}
+	return ""
+}
diff --git a/config/user_config_test.go b/config/user_config_test.go
index 9d65aa5..b3ce81c 100644
--- a/config/user_config_test.go
+++ b/config/user_config_test.go
@@ -42,7 +42,7 @@ func TestNormalizeWorkspace(t *testing.T) {
 	assert.NoError(t, err)
 
 	cfg := &UserConfig{Home: "/home/alice"}
-	tests := []struct {
+	testCases := []struct {
 		in, out string
 	}{
 		{"", ""}, // don't make wild guesses
@@ -54,9 +54,12 @@ func TestNormalizeWorkspace(t *testing.T) {
 		{"relative///path", filepath.Join(cwd, "relative", "path")},
 	}
 
-	for _, test := range tests {
-		cfg.Workspace = test.in
-		cfg.Normalize()
-		assert.Equal(t, test.out, cfg.Workspace)
+	for _, tc := range testCases {
+		testName := "'" + tc.in + "' should be normalized as '" + tc.out + "'"
+		t.Run(testName, func(t *testing.T) {
+			cfg.Workspace = tc.in
+			cfg.Normalize()
+			assert.Equal(t, tc.out, cfg.Workspace, testName)
+		})
 	}
 }
diff --git a/config/user_config_windows_test.go b/config/user_config_windows_test.go
index 62c8255..5ea9a3e 100644
--- a/config/user_config_windows_test.go
+++ b/config/user_config_windows_test.go
@@ -40,7 +40,7 @@ func TestNormalizeWorkspace(t *testing.T) {
 	assert.NoError(t, err)
 
 	cfg := &UserConfig{Home: "C:\\Users\\alice"}
-	tests := []struct {
+	testCases := []struct {
 		in, out string
 	}{
 		{"", ""}, // don't make wild guesses
@@ -54,9 +54,13 @@ func TestNormalizeWorkspace(t *testing.T) {
 		{"relative///path", filepath.Join(cwd, "relative", "path")},
 	}
 
-	for _, test := range tests {
-		cfg.Workspace = test.in
-		cfg.Normalize()
-		assert.Equal(t, test.out, cfg.Workspace)
+	for _, tc := range testCases {
+		testName := "'" + tc.in + "' should be normalized as '" + tc.out + "'"
+
+		t.Run(testName, func(t *testing.T) {
+			cfg.Workspace = tc.in
+			cfg.Normalize()
+			assert.Equal(t, tc.out, cfg.Workspace, testName)
+		})
 	}
 }
diff --git a/workspace/path_type_test.go b/workspace/path_type_test.go
index 232634c..c233bcf 100644
--- a/workspace/path_type_test.go
+++ b/workspace/path_type_test.go
@@ -8,16 +8,18 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
+type testCase struct {
+	desc string
+	path string
+	pt   PathType
+}
+
 func TestDetectPathType(t *testing.T) {
 	_, cwd, _, _ := runtime.Caller(0)
 	root := filepath.Join(cwd, "..", "..", "fixtures", "detect-path-type")
 
-	tests := []struct {
-		desc string
-		path string
-		pt   PathType
-	}{
-		{
+	testCases := []testCase{
+		testCase{
 			desc: "absolute dir",
 			path: filepath.Join(root, "a-dir"),
 			pt:   TypeDir,
@@ -54,9 +56,16 @@ func TestDetectPathType(t *testing.T) {
 		},
 	}
 
-	for _, test := range tests {
-		pt, err := DetectPathType(test.path)
-		assert.NoError(t, err, test.desc)
-		assert.Equal(t, test.pt, pt, test.desc)
+	for _, tc := range testCases {
+		t.Run(tc.desc, makeTest(tc))
+
+	}
+}
+
+func makeTest(tc testCase) func(*testing.T) {
+	return func(t *testing.T) {
+		pt, err := DetectPathType(tc.path)
+		assert.NoError(t, err, tc.desc)
+		assert.Equal(t, tc.pt, pt, tc.desc)
 	}
 }
diff --git a/workspace/solution_test.go b/workspace/solution_test.go
index fb62109..7c1d1e0 100644
--- a/workspace/solution_test.go
+++ b/workspace/solution_test.go
@@ -43,7 +43,7 @@ func TestSolution(t *testing.T) {
 }
 
 func TestSuffix(t *testing.T) {
-	tests := []struct {
+	testCases := []struct {
 		solution Solution
 		suffix   string
 	}{
@@ -77,13 +77,16 @@ func TestSuffix(t *testing.T) {
 		},
 	}
 
-	for _, test := range tests {
-		assert.Equal(t, test.suffix, test.solution.Suffix())
+	for _, tc := range testCases {
+		testName := "Suffix of '" + tc.solution.Dir + "' should be " + tc.suffix
+		t.Run(testName, func(t *testing.T) {
+			assert.Equal(t, tc.suffix, tc.solution.Suffix(), testName)
+		})
 	}
 }
 
 func TestSolutionString(t *testing.T) {
-	tests := []struct {
+	testCases := []struct {
 		solution Solution
 		desc     string
 	}{
@@ -136,7 +139,10 @@ func TestSolutionString(t *testing.T) {
 		},
 	}
 
-	for _, test := range tests {
-		assert.Equal(t, test.desc, test.solution.String())
+	for _, tc := range testCases {
+		testName := "should stringify to '" + tc.desc + "'"
+		t.Run(testName, func(t *testing.T) {
+			assert.Equal(t, tc.desc, tc.solution.String())
+		})
 	}
 }
diff --git a/workspace/transmission_test.go b/workspace/transmission_test.go
index 828d40a..9faf7d6 100644
--- a/workspace/transmission_test.go
+++ b/workspace/transmission_test.go
@@ -18,7 +18,7 @@ func TestNewTransmission(t *testing.T) {
 	fileBird := filepath.Join(dirBird, "hummingbird.txt")
 	fileSugar := filepath.Join(dirFeeder, "sugar.txt")
 
-	tests := []struct {
+	testCases := []struct {
 		desc string
 		args []string
 		ok   bool
@@ -65,18 +65,20 @@ func TestNewTransmission(t *testing.T) {
 		},
 	}
 
-	for _, test := range tests {
-		tx, err := NewTransmission(root, test.args)
-		if test.ok {
-			assert.NoError(t, err, test.desc)
-		} else {
-			assert.Error(t, err, test.desc)
-		}
+	for _, tc := range testCases {
+		t.Run(tc.desc, func(t *testing.T) {
+			tx, err := NewTransmission(root, tc.args)
+			if tc.ok {
+				assert.NoError(t, err, tc.desc)
+			} else {
+				assert.Error(t, err, tc.desc)
+			}
 
-		if test.tx != nil {
-			assert.Equal(t, test.tx.Files, tx.Files, test.desc)
-			assert.Equal(t, test.tx.Dir, tx.Dir, test.desc)
-		}
+			if tc.tx != nil {
+				assert.Equal(t, tc.tx.Files, tx.Files)
+				assert.Equal(t, tc.tx.Dir, tx.Dir)
+			}
+		})
 	}
 }
 
diff --git a/workspace/workspace_test.go b/workspace/workspace_test.go
index 608ba71..e183dc0 100644
--- a/workspace/workspace_test.go
+++ b/workspace/workspace_test.go
@@ -16,7 +16,7 @@ func TestLocateErrors(t *testing.T) {
 
 	ws := New(filepath.Join(root, "workspace"))
 
-	tests := []struct {
+	testCases := []struct {
 		desc, arg string
 		errFn     func(error) bool
 	}{
@@ -47,9 +47,11 @@ func TestLocateErrors(t *testing.T) {
 		},
 	}
 
-	for _, test := range tests {
-		_, err := ws.Locate(test.arg)
-		assert.True(t, test.errFn(err), fmt.Sprintf("test: %s (arg: %s), %#v", test.desc, test.arg, err))
+	for _, tc := range testCases {
+		t.Run(tc.desc, func(t *testing.T) {
+			_, err := ws.Locate(tc.arg)
+			assert.True(t, tc.errFn(err), fmt.Sprintf("test: %s (arg: %s), %#v", tc.desc, tc.arg, err))
+		})
 	}
 }