diff --git a/go.mod b/go.mod
index 566e99fb4100d8110b596b47618e99840b61bd06..ed9d811c4a262cf6f54b95ab45eb7ed7819a7f79 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module gitlab.hedenstroem.com/go/snok
 go 1.22.3
 
 require (
+	github.com/docker/docker v25.0.5+incompatible
 	github.com/spf13/cobra v1.8.0
 	github.com/spf13/pflag v1.0.5
 	github.com/stretchr/testify v1.9.0
@@ -22,7 +23,6 @@ require (
 	github.com/cpuguy83/dockercfg v0.3.1 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/distribution/reference v0.5.0 // indirect
-	github.com/docker/docker v25.0.5+incompatible // indirect
 	github.com/docker/go-connections v0.5.0 // indirect
 	github.com/docker/go-units v0.5.0 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
diff --git a/helpers.go b/helpers.go
index 93f26623bff2ca5fb6b7ba4e3bdfb34cf4a8a01f..0458e34a96831939b3af07c9555087196cdecda0 100644
--- a/helpers.go
+++ b/helpers.go
@@ -8,8 +8,8 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-func PrintEnv(tests *Tests, t *testing.T, args []string) {
-	if tests.Debug {
+func PrintEnv(s *TestSuite, t *testing.T, args []string) {
+	if s.GetLogLevel() >= DebugLevel {
 		t.Helper()
 		envs := os.Environ()
 		sort.Strings(envs)
@@ -19,7 +19,7 @@ func PrintEnv(tests *Tests, t *testing.T, args []string) {
 	}
 }
 
-func TempFile(tests *Tests, t *testing.T, args []string) {
+func TempFile(s *TestSuite, t *testing.T, args []string) {
 	t.Helper()
 	file, err := os.CreateTemp("", "test_")
 	require.NoError(t, err)
diff --git a/logging.go b/logging.go
new file mode 100644
index 0000000000000000000000000000000000000000..c61cfe185091ec111d9119c95c81b6e2c3e9469c
--- /dev/null
+++ b/logging.go
@@ -0,0 +1,50 @@
+package snok
+
+import (
+	"fmt"
+	"strings"
+	"testing"
+
+	"github.com/testcontainers/testcontainers-go"
+)
+
+type LogLevel uint32
+
+const (
+	ErrorLevel LogLevel = iota
+	WarnLevel
+	InfoLevel
+	DebugLevel
+	TraceLevel
+)
+
+func ParseLogLevel(lvl string) (LogLevel, error) {
+	switch strings.ToLower(lvl) {
+	case "error":
+		return ErrorLevel, nil
+	case "warn", "warning":
+		return WarnLevel, nil
+	case "info":
+		return InfoLevel, nil
+	case "debug":
+		return DebugLevel, nil
+	case "trace":
+		return TraceLevel, nil
+	default:
+		return InfoLevel, fmt.Errorf("uknown log level %q, using info.", lvl)
+	}
+}
+
+type TestLogConsumer struct {
+	t *testing.T
+	testcontainers.LogConsumer
+}
+
+func NewTestLogConsumer(t *testing.T) *TestLogConsumer {
+	return &TestLogConsumer{t: t}
+}
+
+func (l *TestLogConsumer) Accept(log testcontainers.Log) {
+	l.t.Helper()
+	l.t.Logf("%s", log.Content)
+}
diff --git a/snok.go b/snok.go
deleted file mode 100644
index a94367276aae4a0e5c14899de512652c8cdb2144..0000000000000000000000000000000000000000
--- a/snok.go
+++ /dev/null
@@ -1,201 +0,0 @@
-package snok
-
-import (
-	"bytes"
-	"encoding/json"
-	"flag"
-	"fmt"
-	"io"
-	"os"
-	"regexp"
-	"strings"
-	"sync"
-	"testing"
-	"unicode"
-
-	"github.com/spf13/cobra"
-	"github.com/spf13/pflag"
-	"github.com/stretchr/testify/require"
-	"gonum.org/v1/gonum/graph"
-	"gonum.org/v1/gonum/graph/simple"
-	"gonum.org/v1/gonum/graph/topo"
-)
-
-type Tests struct {
-	t             *testing.T
-	RootCmd       *cobra.Command
-	Debug         bool
-	Trace         bool
-	graph         *simple.DirectedGraph
-	unexpectedErr func(require.TestingT, error, ...interface{})
-}
-
-func NewTestSuite(cmd *cobra.Command) *Tests {
-	ct := &Tests{
-		RootCmd:       cmd,
-		Debug:         false,
-		Trace:         false,
-		graph:         simple.NewDirectedGraph(),
-		unexpectedErr: require.NoError,
-	}
-	flag.BoolVar(&ct.Debug, "debug", false, "debug")
-	flag.BoolVar(&ct.Trace, "trace", false, "trace")
-	flag.Parse()
-	if os.Getenv("SNOK_DEBUG") == "true" {
-		ct.Debug = true
-	}
-	if os.Getenv("SNOK_TRACE") == "true" {
-		ct.Trace = true
-	}
-	return ct
-}
-
-func (ct *Tests) GetTest(name string) (*test, error) {
-	node, new := ct.graph.NodeWithID(hash(name))
-	if new {
-		return nil, fmt.Errorf("undefined test: %s", name)
-	}
-	if test, ok := node.(*test); ok {
-		return test, nil
-	}
-	return nil, fmt.Errorf("node type is not test: %s", name)
-}
-
-func (tests *Tests) AddHelper(name string, fn func(*Tests, *testing.T, []string), needs ...string) *helper {
-	helper := &helper{
-		node: node{
-			Name:  name,
-			Needs: needs,
-		},
-		fn: fn,
-	}
-	tests.graph.AddNode(helper)
-	return helper
-}
-
-func (tests *Tests) AddHelperCleanup(f func()) {
-	tests.t.Cleanup(f)
-}
-
-func (tests *Tests) addTests(t *testing.T, cmd *cobra.Command, cmds []string) {
-	cmds = append(cmds, cmd.Name())
-	annotation, exists := cmd.Annotations["tests"]
-	if exists {
-		nodes := []test{}
-		err := json.Unmarshal([]byte(annotation), &nodes)
-		require.NoError(t, err, "Malformed annotation: %s", annotation)
-		for _, test := range nodes {
-			test.cmd = cmd
-			test.cmds = cmds
-			tests.graph.AddNode(&test)
-		}
-	}
-	if cmd.HasSubCommands() {
-		for _, cmd := range cmd.Commands() {
-			tests.addTests(t, cmd, cmds)
-		}
-	}
-}
-
-func (tests *Tests) calculateExecutionOrder(t *testing.T) []graph.Node {
-	for _, n := range graph.NodesOf(tests.graph.Nodes()) {
-		if node, ok := n.(Node); ok {
-			for _, id := range node.DependencyIDs() {
-				dependency, new := tests.graph.NodeWithID(id)
-				require.False(t, new, "'%s' has invalid needs", node)
-				dependency.(Node).SetNeeded(true)
-				tests.graph.SetEdge(simple.Edge{F: dependency, T: node})
-			}
-		}
-	}
-	cycles := topo.DirectedCyclesIn(tests.graph)
-	require.Empty(t, cycles, "Detected %d cycles", len(cycles))
-	sorted, err := topo.Sort(tests.graph)
-	require.NoError(t, err)
-	return sorted
-}
-
-func (tests *Tests) Run(t *testing.T) {
-	tests.t = t
-	tests.addTests(t, tests.RootCmd, []string{})
-	nodes := tests.calculateExecutionOrder(t)
-	for _, node := range nodes {
-		if helper, ok := node.(*helper); ok && helper.needed {
-			t.Run(fmt.Sprintf("[%s]", helper.Name), func(tt *testing.T) {
-				helper.fn(tests, t, helper.args)
-			})
-		}
-		if test, ok := node.(*test); ok {
-			tests.executeTest(t, test)
-		}
-	}
-}
-
-func resetFlags(t *testing.T, cmd *cobra.Command) {
-	cmd.Flags().VisitAll(func(pf *pflag.Flag) {
-		if pf.Changed {
-			err := pf.Value.Set(pf.DefValue)
-			require.NoError(t, err, "Error setting %d to %d", pf.Name, pf.DefValue)
-			pf.Changed = false
-		}
-	})
-}
-
-func (tests *Tests) executeTest(t *testing.T, test *test) {
-	t.Run(test.Name, func(t *testing.T) {
-		var input io.Reader
-		if test.Input != nil {
-			input = openURI(t, *test.Input)
-			defer func() {
-				if r, ok := input.(io.Closer); ok {
-					r.Close()
-				}
-			}()
-		}
-		output, err := tests.executeCmd(append(test.cmds[1:], test.Args...), input)
-		resetFlags(t, test.cmd)
-		if test.ExpectError {
-			require.Error(t, err, "Expected error")
-		} else if err != nil {
-			tests.unexpectedErr(t, err, "Unexpected error")
-		} else if test.Output != nil {
-			output = strings.TrimRightFunc(output, unicode.IsSpace)
-			if strings.HasPrefix(*test.Output, "/") && strings.HasSuffix(*test.Output, "/") {
-				expr := (*test.Output)[1 : len(*test.Output)-1]
-				re := regexp.MustCompile(expr)
-				require.True(t, re.MatchString(output), "Expected output to match /%s/, got '%s'", expr, output)
-			} else {
-				require.Equal(t, *test.Output, output, "Expected output '%s', got '%s'", *test.Output, output)
-			}
-		}
-	})
-}
-
-func (tests *Tests) executeCmd(args []string, input io.Reader) (string, error) {
-
-	osStdout := os.Stdout                   // keep backup of the real stdout
-	defer func() { os.Stdout = osStdout }() // restore the real stdout
-	r, w, _ := os.Pipe()
-	os.Stdout = w
-
-	output := new(bytes.Buffer)
-
-	var wg sync.WaitGroup
-	wg.Add(1)
-	go func() {
-		_, err := io.Copy(output, r)
-		require.NoError(tests.t, err, "IO Error")
-		wg.Done()
-	}()
-
-	tests.RootCmd.SetIn(input)
-	tests.RootCmd.SetOutput(w)
-	tests.RootCmd.SetArgs(args)
-	err := tests.RootCmd.Execute()
-
-	w.Close()
-	wg.Wait()
-
-	return output.String(), err
-
-}
diff --git a/snok_test.go b/snok_test.go
index e61f32f63f08ca05fc768e3a4291837f03c00517..ae32f22efc18f89ad7ba6405f43d8ab6fd47bd8d 100644
--- a/snok_test.go
+++ b/snok_test.go
@@ -18,7 +18,7 @@ import (
 	"github.com/testcontainers/testcontainers-go/modules/mockserver"
 )
 
-var tests *Tests
+var suite *TestSuite
 
 func mockUnexpectedError(t require.TestingT, err error, msgAndArgs ...interface{}) {
 	if err.Error() != "expect the unexpected" {
@@ -30,30 +30,30 @@ func TestMain(m *testing.M) {
 	rootCmd.AddCommand(envCmd, echoCmd, errorCmd)
 	rootCmd.PersistentFlags().BoolP("debug", "d", false, "debug flag")
 	echoCmd.Flags().BoolP("reverse", "r", false, "reverse the output")
-	tests = NewTestSuite(rootCmd)
-	tests.unexpectedErr = mockUnexpectedError
-	tests.AddHelper("PrintEnv", PrintEnv)
-	tests.AddHelper("TempFile", TempFile).SetArgs("TEMP_FILE")
-	tests.AddHelper("TestContainer", mockContainer)
-	tests.AddHelper("PatchURL", patchTestInput, "TestContainer")
+	suite = NewTestSuite(rootCmd)
+	suite.unexpectedErr = mockUnexpectedError
+	suite.AddHelper("PrintEnv", PrintEnv)
+	suite.AddHelper("TempFile", TempFile).SetArgs("TEMP_FILE")
+	suite.AddHelper("TestContainer", mockContainer)
+	suite.AddHelper("PatchURL", patchTestInput, "TestContainer")
 	os.Exit(m.Run())
 }
 
 func TestCmds(t *testing.T) {
 	testcontainers.Logger = log.New(&ioutils.NopWriter{}, "", 0)
-	if tests.Trace {
+	if suite.GetLogLevel() >= TraceLevel {
 		testcontainers.Logger = log.New(os.Stdout, "  ", 0)
 	}
-	tests.Run(t)
+	suite.Run(t)
 }
 
-func mockContainer(tests *Tests, t *testing.T, args []string) {
+func mockContainer(s *TestSuite, t *testing.T, args []string) {
 	if os.Getenv("MOCKSERVER_URL") == "" {
 		ctx := context.Background()
 		opts := []testcontainers.ContainerCustomizer{
 			testcontainers.WithImage("mockserver/mockserver:5.15.0"),
 		}
-		if tests.Debug || tests.Trace {
+		if s.GetLogLevel() >= DebugLevel {
 			opts = append(opts, testcontainers.WithLogConsumers(NewTestLogConsumer(t)))
 		}
 		mockserverContainer, err := mockserver.RunContainer(ctx, opts...)
@@ -63,19 +63,19 @@ func mockContainer(tests *Tests, t *testing.T, args []string) {
 		require.NoError(t, err)
 		url, err := mockserverContainer.URL(ctx)
 		require.NoError(t, err)
-		tests.AddHelperCleanup(func() {
+		s.AddHelperCleanup(func() {
 			require.NoError(t, mockserverContainer.Terminate(ctx))
 		})
-		t.Setenv("MOCKSERVER_URL", url)
+		s.Setenv("MOCKSERVER_URL", url)
 	}
 }
 
-func patchTestInput(tests *Tests, t *testing.T, args []string) {
-	_, err := tests.GetTest("TestContainer")
+func patchTestInput(s *TestSuite, t *testing.T, args []string) {
+	_, err := s.GetTest("TestContainer")
 	require.Error(t, err)
-	_, err = tests.GetTest("Does not exist")
+	_, err = s.GetTest("Does not exist")
 	require.Error(t, err)
-	test, err := tests.GetTest("Echo Input (http)")
+	test, err := s.GetTest("Echo Input (http)")
 	dashboardUrl := os.Getenv("MOCKSERVER_URL") + "/mockserver/dashboard"
 	t.Logf("Modifying %s", test.String())
 	test.Input = &dashboardUrl
diff --git a/test_command.go b/test_command.go
new file mode 100644
index 0000000000000000000000000000000000000000..61b69aeb54ba34e0cd18183308c7e82ab3df69a5
--- /dev/null
+++ b/test_command.go
@@ -0,0 +1,53 @@
+package snok
+
+import (
+	"encoding/json"
+
+	"github.com/spf13/cobra"
+)
+
+type TestConfiguration struct {
+	TestName    string   `json:"name,omitempty"`
+	Needs       []string `json:"needs,omitempty"`
+	Args        []string `json:"args,omitempty"`
+	Input       *string  `json:"input,omitempty"`
+	Output      *string  `json:"output,omitempty"`
+	ExpectError bool     `json:"expectError,omitempty"`
+}
+
+type TestCommand struct {
+	TestJob
+	TestConfiguration
+	Cmd  *cobra.Command
+	Cmds []string
+}
+
+func NewTestCommand(configuration *TestConfiguration, cmd *cobra.Command) *TestCommand {
+	job := NewTestJob(configuration.TestName)
+	return &TestCommand{
+		TestJob:           *job,
+		TestConfiguration: *configuration,
+		Cmd:               cmd,
+	}
+}
+
+func FromAnnotation(annotation string) ([]*TestCommand, error) {
+	configurations := []*TestConfiguration{}
+	err := json.Unmarshal([]byte(annotation), &configurations)
+	if err != nil {
+		return nil, err
+	}
+	tests := make([]*TestCommand, len(configurations))
+	for i, configuration := range configurations {
+		tests[i] = NewTestCommand(configuration, nil)
+	}
+	return tests, nil
+}
+
+func (c *TestCommand) SetCmd(cmd *cobra.Command) {
+	c.Cmd = cmd
+}
+
+func (c *TestCommand) SetCmds(cmds []string) {
+	c.Cmds = cmds
+}
diff --git a/test_graph.go b/test_graph.go
new file mode 100644
index 0000000000000000000000000000000000000000..68010b5973821e24ceebbaa74adbe322bb24f7e5
--- /dev/null
+++ b/test_graph.go
@@ -0,0 +1,88 @@
+package snok
+
+import (
+	"fmt"
+
+	"gonum.org/v1/gonum/graph"
+	"gonum.org/v1/gonum/graph/simple"
+	"gonum.org/v1/gonum/graph/topo"
+)
+
+type dependency interface {
+	SetNeeded(bool)
+}
+
+type TestGraph struct {
+	Graph *simple.DirectedGraph
+}
+
+func NewTestGraph() *TestGraph {
+	return &TestGraph{simple.NewDirectedGraph()}
+}
+
+func (g *TestGraph) AddJob(job graph.Node) {
+	g.Graph.AddNode(job)
+}
+
+func (g *TestGraph) GetJob(name string) (any, error) {
+	job, new := g.Graph.NodeWithID(hash(name))
+	if new {
+		return nil, fmt.Errorf("undefined test: %s", name)
+	}
+	switch job := job.(type) {
+	case *TestCommand:
+		return job, nil
+	case *TestHelper:
+		return job, nil
+	default:
+		return nil, fmt.Errorf("%s has unknown job type", name)
+	}
+}
+
+func (g *TestGraph) DeleteJob(name string) {
+	g.Graph.RemoveNode(hash(name))
+}
+
+func (g *TestGraph) ResetDeps() {
+	for _, edge := range graph.EdgesOf(g.Graph.Edges()) {
+		g.Graph.RemoveEdge(edge.From().ID(), edge.To().ID())
+	}
+}
+
+func (g *TestGraph) ResolveDeps() error {
+	for _, node := range graph.NodesOf(g.Graph.Nodes()) {
+		var job *TestJob
+		var needs []string
+		switch node := node.(type) {
+		case *TestCommand:
+			job = &node.TestJob
+			needs = node.Needs
+		case *TestHelper:
+			job = &node.TestJob
+			needs = node.Needs
+		}
+		if needs == nil {
+			continue
+		}
+		for _, need := range needs {
+			dep, new := g.Graph.NodeWithID(hash(need))
+			if new {
+				return fmt.Errorf("%s needed by %s is undefined", need, job)
+			}
+			if dep.ID() == node.ID() {
+				return fmt.Errorf("%s cannot need itself", job)
+			}
+			dep.(dependency).SetNeeded(true)
+			g.Graph.SetEdge(simple.Edge{F: dep, T: node})
+		}
+	}
+	cycles := topo.DirectedCyclesIn(g.Graph)
+	if len(cycles) > 0 {
+		return fmt.Errorf("detected %d cycles", len(cycles))
+	}
+	return nil
+}
+
+func (g *TestGraph) SortJobs() ([]graph.Node, error) {
+	return topo.Sort(g.Graph)
+}
diff --git a/test_helper.go b/test_helper.go
new file mode 100644
index 0000000000000000000000000000000000000000..7efc2964de930750136e954e37fbb405167149ff
--- /dev/null
+++ b/test_helper.go
@@ -0,0 +1,29 @@
+package snok
+
+import "testing"
+
+type TestHelperFn func(*TestSuite, *testing.T, []string)
+
+type TestHelper struct {
+	TestJob
+	Fn    TestHelperFn
+	Needs []string
+	Args  []string
+}
+
+func NewTestHelper(name string, fn TestHelperFn, needs ...string) *TestHelper {
+	job := NewTestJob(name)
+	return &TestHelper{
+		TestJob: *job,
+		Fn:      fn,
+		Needs:   needs,
+	}
+}
+
+func (h *TestHelper) SetArgs(args ...string) {
+	h.Args = args
+}
+
+func (h *TestHelper) Execute(s *TestSuite, t *testing.T) {
+	h.Fn(s, t, h.Args)
+}
diff --git a/test_job.go b/test_job.go
new file mode 100644
index 0000000000000000000000000000000000000000..a94540b9226497187d26aaa1d7a45d0e7751de49
--- /dev/null
+++ b/test_job.go
@@ -0,0 +1,31 @@
+package snok
+
+type TestJob struct {
+	hash   int64
+	name   string
+	needed bool
+}
+
+func NewTestJob(name string) *TestJob {
+	return &TestJob{
+		hash:   hash(name),
+		name:   name,
+		needed: false,
+	}
+}
+
+func (j *TestJob) ID() int64 {
+	return j.hash
+}
+
+func (j *TestJob) Name() string {
+	return j.name
+}
+
+func (j *TestJob) String() string {
+	return j.Name()
+}
+
+func (j *TestJob) SetNeeded(needed bool) {
+	j.needed = needed
+}
diff --git a/test_suite.go b/test_suite.go
new file mode 100644
index 0000000000000000000000000000000000000000..51f9437cb226cf52598b95d95c6d6e81b449592c
--- /dev/null
+++ b/test_suite.go
@@ -0,0 +1,172 @@
+package snok
+
+import (
+	"bytes"
+	"flag"
+	"fmt"
+	"io"
+	"os"
+	"regexp"
+	"strings"
+	"sync"
+	"testing"
+	"unicode"
+
+	"github.com/spf13/cobra"
+	"github.com/stretchr/testify/require"
+)
+
+type TestSuite struct {
+	t             *testing.T
+	RootCmd       *cobra.Command
+	LogLevel      LogLevel
+	Graph         *TestGraph
+	unexpectedErr func(require.TestingT, error, ...interface{})
+}
+
+func NewTestSuite(cmd *cobra.Command) *TestSuite {
+	s := &TestSuite{
+		RootCmd:       cmd,
+		LogLevel:      InfoLevel,
+		Graph:         NewTestGraph(),
+		unexpectedErr: require.NoError,
+	}
+	var l string
+	flag.StringVar(&l, "l", "info", "Log level (error, warn, info, debug, trace)")
+	flag.Parse()
+	if os.Getenv("SNOK_LOG_LEVEL") != "" {
+		l = os.Getenv("SNOK_LOG_LEVEL")
+	}
+	lvl, err := ParseLogLevel(l)
+	if err != nil {
+		fmt.Println(err)
+	}
+	s.LogLevel = lvl
+	return s
+}
+
+func (s *TestSuite) GetLogLevel() LogLevel {
+	return s.LogLevel
+}
+
+func (s *TestSuite) Setenv(key, value string) {
+	s.t.Setenv(key, value)
+}
+
+func (s *TestSuite) GetTest(name string) (*TestCommand, error) {
+	test, err := s.Graph.GetJob(name)
+	if err != nil {
+		return nil, err
+	}
+	if testCmd, ok := test.(*TestCommand); ok {
+		return testCmd, nil
+	}
+	return nil, fmt.Errorf("%s is not a test", name)
+}
+
+func (s *TestSuite) AddHelper(name string, fn TestHelperFn, needs ...string) *TestHelper {
+	helper := NewTestHelper(name, fn, needs...)
+	s.Graph.AddJob(helper)
+	return helper
+}
+
+func (s *TestSuite) AddHelperCleanup(f func()) {
+	s.t.Cleanup(f)
+}
+
+func (s *TestSuite) addTests(t *testing.T, cmd *cobra.Command, cmds []string) {
+	cmds = append(cmds, cmd.Name())
+	annotation, exists := cmd.Annotations["tests"]
+	if exists {
+		cmdTests, err := FromAnnotation(annotation)
+		require.NoError(t, err, "Malformed annotation: %s", annotation)
+		for _, cmdTest := range cmdTests {
+			cmdTest.SetCmd(cmd)
+			cmdTest.SetCmds(cmds)
+			s.Graph.AddJob(cmdTest)
+		}
+	}
+	if cmd.HasSubCommands() {
+		for _, cmd := range cmd.Commands() {
+			s.addTests(t, cmd, cmds)
+		}
+	}
+}
+
+func (s *TestSuite) Run(t *testing.T) {
+	s.t = t
+	s.addTests(t, s.RootCmd, []string{})
+	err := s.Graph.ResolveDeps()
+	require.NoError(t, err)
+	jobs, err := s.Graph.SortJobs()
+	require.NoError(t, err)
+	for _, job := range jobs {
+		switch job := job.(type) {
+		case *TestHelper:
+			t.Run(fmt.Sprintf("[%s]", job.Name()), func(th *testing.T) {
+				job.Execute(s, th)
+			})
+		case *TestCommand:
+			s.executeTest(t, job)
+		}
+	}
+}
+
+func (s *TestSuite) executeTest(t *testing.T, test *TestCommand) {
+	t.Run(test.Name(), func(t *testing.T) {
+		var input io.Reader
+		if test.Input != nil {
+			input = openURI(t, *test.Input)
+			defer func() {
+				if r, ok := input.(io.Closer); ok {
+					r.Close()
+				}
+			}()
+		}
+		output, err := s.executeCmd(append(test.Cmds[1:], test.Args...), input)
+		resetCmdFlags(test.Cmd)
+		if test.ExpectError {
+			require.Error(t, err, "Expected error")
+		} else if err != nil {
+			s.unexpectedErr(t, err, "Unexpected error")
+		} else if test.Output != nil {
+			output = strings.TrimRightFunc(output, unicode.IsSpace)
+			if strings.HasPrefix(*test.Output, "/") && strings.HasSuffix(*test.Output, "/") {
+				expr := (*test.Output)[1 : len(*test.Output)-1]
+				re := regexp.MustCompile(expr)
+				require.True(t, re.MatchString(output), "Expected output to match /%s/, got '%s'", expr, output)
+			} else {
+				require.Equal(t, *test.Output, output, "Expected output '%s', got '%s'", *test.Output, output)
+			}
+		}
+	})
+}
+
+func (s *TestSuite) executeCmd(args []string, input io.Reader) (string, error) {
+
+	osStdout := os.Stdout                   // keep backup of the real stdout
+	defer func() { os.Stdout = osStdout }() // restore the real stdout
+	r, w, _ := os.Pipe()
+	os.Stdout = w
+
+	output := new(bytes.Buffer)
+
+	var wg sync.WaitGroup
+	wg.Add(1)
+	go func() {
+		_, err := io.Copy(output, r)
+		require.NoError(s.t, err, "IO Error")
+		wg.Done()
+	}()
+
+	s.RootCmd.SetIn(input)
+	s.RootCmd.SetOutput(w)
+	s.RootCmd.SetArgs(args)
+	err := s.RootCmd.Execute()
+
+	w.Close()
+	wg.Wait()
+
+	return output.String(), err
+
+}
diff --git a/types.go b/types.go
deleted file mode 100644
index 40c2f5cf3b628c75736c93a5ad88a7462ca9c088..0000000000000000000000000000000000000000
--- a/types.go
+++ /dev/null
@@ -1,96 +0,0 @@
-package snok
-
-import (
-	"hash/fnv"
-	"testing"
-
-	"github.com/spf13/cobra"
-	"github.com/testcontainers/testcontainers-go"
-	"gonum.org/v1/gonum/graph"
-)
-
-// hash calculates the hash value of a string using the FNV-1a algorithm.
-func hash(s string) int64 {
-	hash := fnv.New64a()
-	hash.Write([]byte(s))
-	return int64(hash.Sum64())
-}
-
-// Node is a graph node. It returns a graph-unique integer ID and a list of
-// integer IDs of nodes that it depends on.
-type Node interface {
-	graph.Node
-	// Needed sets the needed flag to true or false.
-	SetNeeded(bool)
-	// DependencyIDs returns a list of graph-unique integer ID for each dependency.
-	DependencyIDs() []int64
-}
-
-// node in the graph. helper and test inherit from this.
-type node struct {
-	id     *int64
-	Name   string   `json:"name,omitempty"`
-	Needs  []string `json:"needs,omitempty"`
-	needed bool
-}
-
-func (n *node) ID() int64 {
-	if n.id == nil {
-		n.id = new(int64)
-		*n.id = hash(n.Name)
-	}
-	return *n.id
-}
-
-func (n *node) SetNeeded(needed bool) {
-	n.needed = needed
-}
-
-func (n *node) DependencyIDs() []int64 {
-	needs := make([]int64, len(n.Needs))
-	for i, need := range n.Needs {
-		needs[i] = hash(need)
-	}
-	return needs
-}
-
-func (n *node) String() string {
-	return n.Name
-}
-
-// helper node in the graph.
-type helper struct {
-	node
-	fn   func(*Tests, *testing.T, []string)
-	args []string
-}
-
-func (h *helper) SetArgs(args ...string) {
-	h.args = args
-}
-
-// test node in the graph.
-type test struct {
-	node
-	cmd         *cobra.Command
-	cmds        []string
-	Args        []string `json:"args,omitempty"`
-	Input       *string  `json:"input,omitempty"`
-	Output      *string  `json:"output,omitempty"`
-	ExpectError bool     `json:"expectError,omitempty"`
-}
-
-// TestLogConsumer consumes testcontainer logs.
-type TestLogConsumer struct {
-	t *testing.T
-	testcontainers.LogConsumer
-}
-
-func NewTestLogConsumer(t *testing.T) *TestLogConsumer {
-	return &TestLogConsumer{t: t}
-}
-
-func (l *TestLogConsumer) Accept(log testcontainers.Log) {
-	l.t.Helper()
-	l.t.Logf("%s", log.Content)
-}
diff --git a/uri.go b/utils.go
similarity index 74%
rename from uri.go
rename to utils.go
index 9139e367ddb68c1f96d4b3a2d1cddb48c0d336d5..32c77860f62281e2915584343a94770e3e374f5a 100644
--- a/uri.go
+++ b/utils.go
@@ -3,6 +3,7 @@ package snok
 import (
 	"bytes"
 	"encoding/base64"
+	"hash/fnv"
 	"io"
 	"net/http"
 	"net/url"
@@ -10,9 +11,27 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/spf13/cobra"
+	"github.com/spf13/pflag"
 	"github.com/stretchr/testify/require"
 )
 
+// hash calculates the hash value of a string using the FNV-1a algorithm.
+func hash(s string) int64 {
+	hash := fnv.New64a()
+	hash.Write([]byte(s))
+	return int64(hash.Sum64())
+}
+
+func resetCmdFlags(cmd *cobra.Command) {
+	cmd.Flags().VisitAll(func(pf *pflag.Flag) {
+		if pf.Changed {
+			_ = pf.Value.Set(pf.DefValue)
+			pf.Changed = false
+		}
+	})
+}
+
 func openURI(t *testing.T, uri string) io.Reader {
 	parsedURL, err := url.Parse(uri)
 	require.NoError(t, err)