feat: add test command (#38)

Currently it's just a simple pass-through to bazel
diff --git a/cmd/aspect/clean/BUILD.bazel b/cmd/aspect/clean/BUILD.bazel
index 80e22c2..d33eca3 100644
--- a/cmd/aspect/clean/BUILD.bazel
+++ b/cmd/aspect/clean/BUILD.bazel
@@ -7,8 +7,6 @@
     visibility = ["//visibility:public"],
     deps = [
         "//pkg/aspect/clean",
-        "//pkg/bazel",
-        "//pkg/ioutils",
         "@com_github_mattn_go_isatty//:go-isatty",
         "@com_github_spf13_cobra//:cobra",
     ],
diff --git a/cmd/aspect/main.go b/cmd/aspect/main.go
index b7c645c..264275d 100644
--- a/cmd/aspect/main.go
+++ b/cmd/aspect/main.go
@@ -28,6 +28,12 @@
 	//     ask the user if they want to install for all users of the workspace, if so
 	//         - tools/bazel file and put our bootstrap code in there
 	//
+
+	// Convenience for local development: under `bazel run //:aspect` respect the
+	// users working directory, don't run in the execroot
+	if wd, exists := os.LookupEnv("BUILD_WORKING_DIRECTORY"); exists {
+		_ = os.Chdir(wd)
+	}
 	cmd := root.NewDefaultRootCmd()
 	if err := cmd.ExecuteContext(context.Background()); err != nil {
 		var exitErr *aspecterrors.ExitError
diff --git a/cmd/aspect/root/BUILD.bazel b/cmd/aspect/root/BUILD.bazel
index d186a75..cea46cb 100644
--- a/cmd/aspect/root/BUILD.bazel
+++ b/cmd/aspect/root/BUILD.bazel
@@ -13,6 +13,7 @@
         "//cmd/aspect/clean",
         "//cmd/aspect/docs",
         "//cmd/aspect/info",
+        "//cmd/aspect/test",
         "//cmd/aspect/version",
         "//docs/help/topics",
         "//pkg/ioutils",
diff --git a/cmd/aspect/root/root.go b/cmd/aspect/root/root.go
index 608513e..55686fd 100644
--- a/cmd/aspect/root/root.go
+++ b/cmd/aspect/root/root.go
@@ -19,6 +19,7 @@
 	"aspect.build/cli/cmd/aspect/clean"
 	"aspect.build/cli/cmd/aspect/docs"
 	"aspect.build/cli/cmd/aspect/info"
+	"aspect.build/cli/cmd/aspect/test"
 	"aspect.build/cli/cmd/aspect/version"
 	"aspect.build/cli/docs/help/topics"
 	"aspect.build/cli/pkg/ioutils"
@@ -74,6 +75,7 @@
 	cmd.AddCommand(version.NewDefaultVersionCmd())
 	cmd.AddCommand(docs.NewDefaultDocsCmd())
 	cmd.AddCommand(info.NewDefaultInfoCmd())
+	cmd.AddCommand(test.NewDefaultTestCmd())
 
 	// ### "Additional help topic commands" which are not runnable
 	// https://pkg.go.dev/github.com/spf13/cobra#Command.IsAdditionalHelpTopicCommand
diff --git a/cmd/aspect/test/BUILD.bazel b/cmd/aspect/test/BUILD.bazel
new file mode 100644
index 0000000..2fbf5c4
--- /dev/null
+++ b/cmd/aspect/test/BUILD.bazel
@@ -0,0 +1,14 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "test",
+    srcs = ["test.go"],
+    importpath = "aspect.build/cli/cmd/aspect/test",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//pkg/aspect/test",
+        "//pkg/bazel",
+        "//pkg/ioutils",
+        "@com_github_spf13_cobra//:cobra",
+    ],
+)
diff --git a/cmd/aspect/test/test.go b/cmd/aspect/test/test.go
new file mode 100644
index 0000000..7037eb9
--- /dev/null
+++ b/cmd/aspect/test/test.go
@@ -0,0 +1,42 @@
+/*
+Copyright © 2021 Aspect Build Systems
+
+Not licensed for re-use
+*/
+
+package test
+
+import (
+	"github.com/spf13/cobra"
+
+	"aspect.build/cli/pkg/aspect/test"
+	"aspect.build/cli/pkg/bazel"
+	"aspect.build/cli/pkg/ioutils"
+)
+
+func NewDefaultTestCmd() *cobra.Command {
+	return NewTestCmd(ioutils.DefaultStreams, bazel.New())
+}
+
+func NewTestCmd(streams ioutils.Streams, bzl bazel.Spawner) *cobra.Command {
+	v := test.New(streams, bzl)
+
+	cmd := &cobra.Command{
+		Use:   "test",
+		Short: "Builds the specified targets and runs all test targets among them.",
+		Long: `Builds the specified targets and runs all test targets among them (test targets
+might also need to satisfy provided tag, size or language filters) using
+the specified options.
+
+This command accepts all valid options to 'build', and inherits
+defaults for 'build' from your .bazelrc.  If you don't use .bazelrc,
+don't forget to pass all your 'build' options to 'test' too.
+
+See 'bazel help target-syntax' for details and examples on how to
+specify targets.
+`,
+		RunE: v.Run,
+	}
+
+	return cmd
+}
diff --git a/docs/BUILD.bazel b/docs/BUILD.bazel
index 95a8cc4..15f106d 100644
--- a/docs/BUILD.bazel
+++ b/docs/BUILD.bazel
@@ -2,12 +2,14 @@
 load("@bazel_skylib//rules:write_file.bzl", "write_file")
 
 # This list must be updated when we add a new command
+# buildifier: keep sorted
 _DOCS = [
     "aspect.md",
     "aspect_build.md",
     "aspect_clean.md",
     "aspect_docs.md",
     "aspect_info.md",
+    "aspect_test.md",
     "aspect_version.md",
 ]
 
diff --git a/docs/aspect.md b/docs/aspect.md
index f40087b..3fbd99b 100644
--- a/docs/aspect.md
+++ b/docs/aspect.md
@@ -20,6 +20,7 @@
 * [aspect clean](aspect_clean.md)	 - Removes the output tree.
 * [aspect docs](aspect_docs.md)	 - Open documentation in the browser.
 * [aspect info](aspect_info.md)	 - Displays runtime info about the bazel server.
+* [aspect test](aspect_test.md)	 - Builds the specified targets and runs all test targets among them.
 * [aspect version](aspect_version.md)	 - Print the version of aspect CLI as well as tools it invokes.
 
 ###### Auto generated by spf13/cobra
diff --git a/docs/aspect_test.md b/docs/aspect_test.md
new file mode 100644
index 0000000..20de475
--- /dev/null
+++ b/docs/aspect_test.md
@@ -0,0 +1,40 @@
+## aspect test
+
+Builds the specified targets and runs all test targets among them.
+
+### Synopsis
+
+Builds the specified targets and runs all test targets among them (test targets
+might also need to satisfy provided tag, size or language filters) using
+the specified options.
+
+This command accepts all valid options to 'build', and inherits
+defaults for 'build' from your .bazelrc.  If you don't use .bazelrc,
+don't forget to pass all your 'build' options to 'test' too.
+
+See 'bazel help target-syntax' for details and examples on how to
+specify targets.
+
+
+```
+aspect test [flags]
+```
+
+### Options
+
+```
+  -h, --help   help for test
+```
+
+### Options inherited from parent commands
+
+```
+      --config string   config file (default is $HOME/.aspect.yaml)
+      --interactive     Interactive mode (e.g. prompts for user input)
+```
+
+### SEE ALSO
+
+* [aspect](aspect.md)	 - Aspect.build bazel wrapper
+
+###### Auto generated by spf13/cobra
diff --git a/pkg/aspect/test/BUILD.bazel b/pkg/aspect/test/BUILD.bazel
new file mode 100644
index 0000000..592c2b1
--- /dev/null
+++ b/pkg/aspect/test/BUILD.bazel
@@ -0,0 +1,26 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "test",
+    srcs = ["test.go"],
+    importpath = "aspect.build/cli/pkg/aspect/test",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//pkg/aspecterrors",
+        "//pkg/bazel",
+        "//pkg/ioutils",
+        "@com_github_spf13_cobra//:cobra",
+    ],
+)
+
+go_test(
+    name = "test_test",
+    srcs = ["test_test.go"],
+    deps = [
+        ":test",
+        "//pkg/bazel/mock",
+        "//pkg/ioutils",
+        "@com_github_golang_mock//gomock",
+        "@com_github_onsi_gomega//:gomega",
+    ],
+)
diff --git a/pkg/aspect/test/test.go b/pkg/aspect/test/test.go
new file mode 100644
index 0000000..701706b
--- /dev/null
+++ b/pkg/aspect/test/test.go
@@ -0,0 +1,42 @@
+/*
+Copyright © 2021 Aspect Build Systems Inc
+
+Not licensed for re-use.
+*/
+
+package test
+
+import (
+	"aspect.build/cli/pkg/bazel"
+	"aspect.build/cli/pkg/ioutils"
+	"github.com/spf13/cobra"
+
+	"aspect.build/cli/pkg/aspecterrors"
+)
+
+type Test struct {
+	ioutils.Streams
+	bzl bazel.Spawner
+}
+
+func New(streams ioutils.Streams, bzl bazel.Spawner) *Test {
+	return &Test{
+		Streams: streams,
+		bzl:     bzl,
+	}
+}
+
+func (v *Test) Run(_ *cobra.Command, args []string) error {
+	bazelCmd := []string{"test"}
+	bazelCmd = append(bazelCmd, args...)
+
+	if exitCode, err := v.bzl.Spawn(bazelCmd); exitCode != 0 {
+		err = &aspecterrors.ExitError{
+			Err:      err,
+			ExitCode: exitCode,
+		}
+		return err
+	}
+
+	return nil
+}
diff --git a/pkg/aspect/test/test_test.go b/pkg/aspect/test/test_test.go
new file mode 100644
index 0000000..1a8d7c0
--- /dev/null
+++ b/pkg/aspect/test/test_test.go
@@ -0,0 +1,30 @@
+package test_test
+
+import (
+	"testing"
+
+	"aspect.build/cli/pkg/aspect/test"
+	"aspect.build/cli/pkg/bazel/mock"
+	"aspect.build/cli/pkg/ioutils"
+	"github.com/golang/mock/gomock"
+	. "github.com/onsi/gomega"
+)
+
+// Embrace the stutter :)
+func TestTest(t *testing.T) {
+
+	t.Run("test calls bazel test", func(t *testing.T) {
+		g := NewGomegaWithT(t)
+		ctrl := gomock.NewController(t)
+		defer ctrl.Finish()
+
+		spawner := mock.NewMockSpawner(ctrl)
+		spawner.
+			EXPECT().
+			Spawn([]string{"test"}).
+			Return(0, nil)
+
+		b := test.New(ioutils.Streams{}, spawner)
+		g.Expect(b.Run(nil, []string{})).Should(Succeed())
+	})
+}