Repository: SimonBaeumer/commander
Pull request #4 (2019-03-03 11:36:39)

First refactoring...


Patch:
diff --git a/README.md b/README.md
index 5ed0081..cffbf6e 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,7 @@ $ ./commander ./example/commander.yaml
 ```
 
 ## Todo:
+ - assert stderr & exit-code & line-count
  - suite fails -> error exit code
  - logging / verbose output
  - print errors in colors
@@ -48,18 +49,47 @@ $ ./commander ./example/commander.yaml
       - rhel
       - centos
       - alpine
-      
-## Open
 
- - support for...
-    - docker
-    - docker-compose
-    - lxc
-    - vagrant
+## Matching / Printing
 
-## Architecture
+- Diff libs
+    - https://github.com/sergi/go-diff
+    - https://github.com/pmezard/go-difflib
+    - https://github.com/sourcegraph/go-diff
+- regex matching
+- line count
+- assert specific lines
+- specific line
 
- - runtime?
-     - test-executer
-     - ordering?
- - interpreter?
+```
+tests:
+    it will print hello world:
+        cmd: echo hello world
+        stdout:
+            lines:
+                1: hello world
+            contains: 
+            - hello world
+        exit-code: 0
+            
+    it will print hello:
+        cmd: echo hello
+        stdout: hello
+        exit-code: 0
+        
+    echo hello:
+        exit-code: 0
+        
+    echo skip:
+        skip: true
+        exit-code: 0
+```
+
+## Docker
+
+```
+tests:
+    echo hello:
+        docker: ubuntu:18.04
+        exit-code: 0
+```
\ No newline at end of file
diff --git a/cmd/commander/commander.go b/cmd/commander/commander.go
index 0d89d9c..2d17d4a 100644
--- a/cmd/commander/commander.go
+++ b/cmd/commander/commander.go
@@ -1,7 +1,8 @@
 package main
 
 import (
-	"github.com/SimonBaeumer/commander/pkg"
+	"fmt"
+	"github.com/SimonBaeumer/commander/pkg/config"
 	"github.com/SimonBaeumer/commander/pkg/runtime"
 	"github.com/urfave/cli"
 	"io/ioutil"
@@ -36,14 +37,21 @@ func main() {
 			Usage:     "Execute the test suite",
 			ArgsUsage: "[file]",
 			Action: func(c *cli.Context) {
-				log.Println("Starting test suite")
-				file := c.Args().First()
-				if file == "" {
-					file = CommanderFile
+				file := CommanderFile
+				if c.Args().First() != "" {
+					file = c.Args().First()
+				}
+				content, err := ioutil.ReadFile(file)
+				if err != nil {
+					fmt.Println("Error " + err.Error())
+					os.Exit(1)
 				}
 
-				suite := commander.ParseYAMLFile(file)
-				runtime.Start(suite)
+				suite := config.ParseYAML(content)
+				results := runtime.Start(suite)
+				if !start(results) {
+					os.Exit(1)
+				}
 			},
 		},
 	}
@@ -52,3 +60,23 @@ func main() {
 		log.Fatal(err)
 	}
 }
+
+func start(results <-chan runtime.TestResult) bool {
+	testResults := []runtime.TestResult{}
+	success := true
+
+	for r := range results {
+		testResults = append(testResults, r)
+		if r.ValidationResult.Success {
+			fmt.Println("✓ ", r.TestCase.Title)
+		}
+
+		if !r.ValidationResult.Success {
+			success = false
+			fmt.Println("✗ ", r.TestCase.Title)
+			fmt.Println(r.ValidationResult.Diff)
+		}
+	}
+
+	return success
+}
diff --git a/commander.yaml b/commander.yaml
index fd07d32..3885c67 100644
--- a/commander.yaml
+++ b/commander.yaml
@@ -9,13 +9,19 @@ tests:
 
   it should execute tests:
     command: ./commander test ./integration/commander_test.yaml
-    stdout: ✓ it should exit with error code
+    stdout:
+      contains:
+        - ✓ it should exit with error code
     exit-code: 0
 
   it should assert that commander will fail:
     command: ./commander test ./integration/failing_suite.yaml
-    stdout: >-
-      ✗ it will fail
+    stdout:
+      contains:
+        - ✗ it will fail
+    exit-code: 1
 
-      Got 1, expected 0
+  it should assert stderr:
+    command: ">&2 echo hello"
+    stderr: hello
     exit-code: 0
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 3323d71..d0609f7 100644
--- a/go.mod
+++ b/go.mod
@@ -1,7 +1,8 @@
 module github.com/SimonBaeumer/commander
 
 require (
+	github.com/pmezard/go-difflib v1.0.0
 	github.com/stretchr/testify v1.3.0
-	github.com/urfave/cli v1.20.0 // indirect
+	github.com/urfave/cli v1.20.0
 	gopkg.in/yaml.v2 v2.2.2
 )
diff --git a/my.yml b/my.yml
new file mode 100644
index 0000000..bcdfae5
--- /dev/null
+++ b/my.yml
@@ -0,0 +1,4 @@
+tests:
+  it should:
+    command: exit 1;
+    exit-code: 1
\ No newline at end of file
diff --git a/pkg/cmd/command.go b/pkg/cmd/command.go
new file mode 100644
index 0000000..3ecbd55
--- /dev/null
+++ b/pkg/cmd/command.go
@@ -0,0 +1,84 @@
+package cmd
+
+import (
+    "bytes"
+    "os/exec"
+    "strings"
+    "syscall"
+)
+
+type Command struct {
+    Cmd      string
+    Env      []string
+    Dir      string
+    executed bool
+    stderr   string
+    stdout   string
+    exitCode int
+}
+
+func NewCommand(cmd string) *Command {
+    return &Command{
+        Cmd: cmd,
+        executed: false,
+    }
+}
+
+func (c *Command) Stdout() string {
+    c.evaluateExecuted("Stdout")
+    return c.stdout
+}
+
+func (c *Command) Stderr() string {
+    c.evaluateExecuted("Stderr")
+    return c.stderr
+}
+
+func (c *Command) ExitCode() int {
+    c.evaluateExecuted("ExitCode")
+    return c.exitCode
+}
+
+func (c *Command) Executed() bool {
+    return c.executed
+}
+
+func (c *Command) evaluateExecuted(property string) {
+    if !c.executed {
+        panic("Can not read " + property + " if command was not executed.")
+    }
+}
+
+func (c *Command) Execute() error {
+    cmd := exec.Command("sh", "-c", c.Cmd)
+    cmd.Env = c.Env
+    cmd.Dir = c.Dir
+
+    var (
+        outBuff bytes.Buffer
+        errBuff bytes.Buffer
+    )
+    cmd.Stdout = &outBuff
+    cmd.Stderr = &errBuff
+
+    err := cmd.Start()
+    if err != nil {
+        return err
+    }
+
+    c.exitCode = 0
+    if err := cmd.Wait(); err != nil {
+        if exitErr, ok := err.(*exec.ExitError); ok {
+            if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
+                c.exitCode = status.ExitStatus()
+            }
+        }
+    }
+
+    // Remove leading and trailing whitespaces
+    c.stderr = strings.Trim(errBuff.String(), "\n")
+    c.stdout = strings.Trim(outBuff.String(), "\n")
+    c.executed = true
+
+    return nil
+}
diff --git a/pkg/config/yaml_suite.go b/pkg/config/yaml_suite.go
new file mode 100644
index 0000000..7ea7da7
--- /dev/null
+++ b/pkg/config/yaml_suite.go
@@ -0,0 +1,136 @@
+package config
+
+import (
+	"fmt"
+	"github.com/SimonBaeumer/commander/pkg/runtime"
+	"gopkg.in/yaml.v2"
+	"log"
+)
+
+// YAMLConfig will be used for unmarshalling the yaml test suite
+type YAMLConfig struct {
+	Tests map[string]YAMLTest `yaml:"tests"`
+}
+
+// YAMLTest represents a test in the yaml test suite
+type YAMLTest struct {
+	Title    string      `yaml:"-"`
+	Command  string      `yaml:"command"`
+	ExitCode int         `yaml:"exit-code"`
+	Stdout   interface{} `yaml:"stdout,omitempty"`
+	Stderr   interface{} `yaml:"stderr,omitempty"`
+}
+
+// ParseYAML parses the Suite from a byte slice
+func ParseYAML(content []byte) []runtime.TestCase {
+	yamlConfig := YAMLConfig{}
+
+	err := yaml.Unmarshal(content, &yamlConfig)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return convertYAMLConfToTestCases(yamlConfig)
+}
+
+//Convert YAMlConfig to runtime TestCases
+func convertYAMLConfToTestCases(conf YAMLConfig) []runtime.TestCase {
+	var tests []runtime.TestCase
+	for _, t := range conf.Tests {
+		tests = append(tests, runtime.TestCase{
+			Title:    t.Title,
+			Command:  runtime.CommandUnderTest{
+				Cmd: t.Command,
+			},
+			Expected: runtime.Expected{
+				ExitCode: t.ExitCode,
+				Stdout: t.Stdout.(runtime.ExpectedOut),
+				Stderr: t.Stderr.(runtime.ExpectedOut),
+			},
+		})
+	}
+
+	return tests
+}
+
+func toString(s interface{}) string {
+	return fmt.Sprintf("%s", s)
+}
+
+// UnmarshalYAML unmarshals the yaml
+func (y *YAMLConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
+	var params struct {
+		Tests map[string]YAMLTest `yaml:"tests"`
+	}
+
+	err := unmarshal(&params)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// map key to title property
+	y.Tests = make(map[string]YAMLTest)
+	for k, v := range params.Tests {
+		test := YAMLTest{
+			Title: k,
+			Command: v.Command,
+			ExitCode: v.ExitCode,
+			Stdout: y.convertToExpectedOut(v.Stdout),
+			Stderr: y.convertToExpectedOut(v.Stderr),
+		}
+
+		// Set key as command, if command property was empty
+		if v.Command == "" {
+			test.Command = k
+		}
+
+		y.Tests[k] = test
+	}
+
+	return nil
+}
+
+//Converts given value to an ExpectedOut. Especially used for Stdout and Stderr.
+func (y *YAMLConfig) convertToExpectedOut(value interface{}) runtime.ExpectedOut {
+    exp := runtime.ExpectedOut{}
+
+    switch value.(type) {
+    //If only a string was passed it is assigned to exactly automatically
+    case string:
+        exp.Exactly = toString(value)
+        break
+
+    //If there is nested map set the properties will be assigned to the contains
+    case map[interface{}]interface{}:
+	    v := value.(map[interface{}]interface{})
+	    // Check if keys are parsable
+	    // TODO: Could be refactored into a registry maybe which holds all parsers
+	    for k, _ := range v {
+		    switch k {
+		    case
+			    "contains",
+			    "exactly":
+		        break;
+		    default:
+			    panic(fmt.Sprintf("Key %s is not allowed.", k))
+		    }
+	    }
+
+	    //Parse contains key
+	    if contains := v["contains"]; contains != nil {
+		    values := contains.([]interface{})
+		    for _, v := range values {
+			    exp.Contains = append(exp.Contains, toString(v))
+		    }
+	    }
+
+	    //Parse exactly key
+        if exactly := v["exactly"]; exactly != nil {
+        	exact := toString(exactly)
+            exp.Exactly = exact
+        }
+        break
+    }
+
+    return exp
+}
diff --git a/pkg/output/human_output.go b/pkg/output/human_output.go
deleted file mode 100644
index 8340ad0..0000000
--- a/pkg/output/human_output.go
+++ /dev/null
@@ -1,60 +0,0 @@
-package output
-
-import (
-	"fmt"
-	"log"
-)
-
-type HumanOutput struct {
-	buffer []string
-}
-
-func (o HumanOutput) BuildHeader() {
-	s := "Starting test suite...\n"
-	o.add(s)
-}
-
-func (o HumanOutput) BuildTestResult(test TestCase) string {
-	var s string
-
-	if test.Result.Success {
-		s = fmt.Sprintf("✓ %s", test.Title)
-	} else {
-		s = fmt.Sprintf("✗ %s\n", test.Title)
-		for _, p := range test.Result.FailureProperties {
-			log.Printf("Printing property result '%s'", p)
-
-			if p == "Stdout" {
-				s += fmt.Sprintf("Got '%s', expected '%s'\n", test.Result.Stdout, test.Stdout)
-			}
-			if p == "Stderr" {
-				s += fmt.Sprintf("Got %s, expected %s\n", test.Result.Stderr, test.Stderr)
-			}
-			if p == "ExitCode" {
-				s += fmt.Sprintf("Got %d, expected %d\n", test.Result.ExitCode, test.ExitCode)
-			}
-		}
-	}
-
-	o.add(s)
-	return s
-}
-
-func (o HumanOutput) BuildSuiteResult() {
-	s := "Duration: 3.24sec"
-	o.add(s)
-}
-
-func (o HumanOutput) add(out string) {
-	o.buffer = append(o.buffer, out)
-}
-
-func (o HumanOutput) GetBuffer() []string {
-	return o.buffer
-}
-
-func (o HumanOutput) Print() {
-	for _, s := range o.GetBuffer() {
-		fmt.Println(s)
-	}
-}
diff --git a/pkg/output/output.go b/pkg/output/output.go
index 114e9aa..4dc3e6b 100644
--- a/pkg/output/output.go
+++ b/pkg/output/output.go
@@ -1,14 +1,10 @@
 package output
 
-import (
-	"github.com/SimonBaeumer/commander/pkg"
-)
-
-type TestCase commander.TestCase
-
 type Output interface {
-	BuildHeader()
-	BuildTestResult(test TestCase)
-	BuildSuiteResult()
-	GetBuffer() []string
+    PrintResult()
+    PrintTestResult()
 }
+
+type DefaultOutput struct {
+
+}
\ No newline at end of file
diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go
index 614ae30..313143f 100644
--- a/pkg/runtime/runtime.go
+++ b/pkg/runtime/runtime.go
@@ -1,115 +1,172 @@
 package runtime
 
 import (
-    "bytes"
-    "fmt"
-    "github.com/SimonBaeumer/commander/pkg"
-    "github.com/SimonBaeumer/commander/pkg/output"
+    "github.com/SimonBaeumer/commander/pkg/cmd"
     "log"
-    "os"
-    "os/exec"
-    "strings"
-    "syscall"
+    "sync"
 )
 
-type Command struct {
-    cmd      string
-    args     string
-    exitCode int
-    stderr   string
-    stdout   string
 
-    //env
-    //timeout
-    //...
+// Constants for defining the various tested properties
+const (
+    ExitCode = "ExitCode"
+    Stdout   = "Stdout"
+    Stderr   = "Stderr"
+)
+
+// Result status codes
+const (
+    Success ResultStatus = iota
+    Failed
+    Skipped
+)
+
+// TestCase represents a test case which will be executed by the runtime
+type TestCase struct {
+    Title    string
+    Command  CommandUnderTest
+    Expected Expected
+    Result   CommandResult
 }
 
-// Start starts the given test suite
-func Start(suite commander.Suite) {
-    c := make(chan commander.TestCase)
+// ResultStatus represents the status code of a test result
+type ResultStatus int
 
-    for _, t := range suite.GetTestCases() {
-        go runTest(t, c)
-    }
+// CommandResult holds the result for a specific test
+type CommandResult struct {
+    Status            ResultStatus
+    Stdout            string
+    Stderr            string
+    ExitCode          int
+    FailureProperties []string
+}
 
-    printResults(c, suite)
+//Expected is the expected output of the command under test
+type Expected struct {
+    Stdout   ExpectedOut
+    Stderr   ExpectedOut
+    ExitCode int
 }
 
-func printResults(c chan commander.TestCase, suite commander.Suite) {
-    o := &output.HumanOutput{}
-    counter := 0
-    for r := range c {
-        s := o.BuildTestResult(output.TestCase(r))
-        fmt.Println(s)
+type ExpectedOut struct {
+    Contains    []string
+    Line        map[int]string
+    Exactly     string
+}
 
-        counter++
-        if counter >= len(suite.GetTestCases()) {
-            close(c)
-        }
+// CommandUnderTest represents the command under test
+type CommandUnderTest struct {
+    Cmd string
+    Env []string
+    Dir string
+}
+
+// CommandResult represents the TestCase and the ValidationResult
+type TestResult struct {
+    TestCase         TestCase
+    ValidationResult ValidationResult
+}
+
+// Start starts the given test suite
+func Start(tests []TestCase) <-chan TestResult {
+    in := make(chan TestCase)
+    out := make(chan TestResult)
+
+     go func(tests []TestCase) {
+         defer close(in)
+         for _, t := range tests {
+             in <- t
+         }
+     }(tests)
+
+    //TODO: Add more concurrency
+    workerCount := 1
+    var wg sync.WaitGroup
+    for i := 0; i < workerCount; i++ {
+        wg.Add(1)
+        go func() {
+            defer wg.Done()
+            for t := range in {
+                out <- runTest(t)
+            }
+
+        }()
     }
+
+    go func() {
+        wg.Wait()
+        close(out)
+    }()
+
+    return out
 }
 
-func runTest(test commander.TestCase, results chan<- commander.TestCase) {
-    // Create command
-    cmd := compile(test.Command)
+func runTest(test TestCase) TestResult {
+    // cut = command under test
+    cut := cmd.NewCommand(test.Command.Cmd)
 
-    // Execute command
-    if err := cmd.Execute(); err != nil {
+    if err := cut.Execute(); err != nil {
         log.Fatal(err)
     }
 
     // Write test result
-    test.Result = commander.TestResult{
-        ExitCode: cmd.exitCode,
-        Stdout:   cmd.stdout,
-        Stderr:   cmd.stderr,
+    test.Result = CommandResult{
+        ExitCode: cut.ExitCode(),
+        Stdout:   cut.Stdout(),
+        Stderr:   cut.Stderr(),
     }
 
-    result := Validate(test)
-    test.Result.Success = result.Success
-    test.Result.FailureProperties = result.Properties
+    validationResult := validateExpectedOut(test.Result.Stdout, test.Expected.Stdout)
+    if !validationResult.Success {
+        return TestResult{
+            ValidationResult: validationResult,
+            TestCase:         test,
+        }
+    }
 
-    // Send to result channel
-    results <- test
-}
+    validationResult = validateExpectedOut(test.Result.Stderr, test.Expected.Stderr)
+    if !validationResult.Success {
+        return TestResult{
+            ValidationResult: validationResult,
+            TestCase:         test,
+        }
+    }
+
+    validator := NewValidator(Equal)
+    validationResult = validator.Validate(test.Result.ExitCode, test.Expected.ExitCode)
+    if !validationResult.Success {
+        return TestResult{
+            ValidationResult: validationResult,
+            TestCase: test,
+        }
+    }
 
-func compile(command string) *Command {
-    return &Command{
-        cmd: command,
+    return TestResult{
+        ValidationResult: validationResult,
+        TestCase:         test,
     }
 }
 
-// Execute executes a command on the system
-func (c *Command) Execute() error {
-    cmd := exec.Command("sh", "-c", c.cmd)
-    env := os.Environ()
-    cmd.Env = env
-
-    var (
-        outBuff bytes.Buffer
-        errBuff bytes.Buffer
-    )
-    cmd.Stdout = &outBuff
-    cmd.Stderr = &errBuff
-
-    err := cmd.Start()
-    log.Println("Started command " + c.cmd)
-    if err != nil {
-        log.Println("Started command " + c.cmd + " err: " + err.Error())
+func validateExpectedOut(got string, expected  ExpectedOut) ValidationResult {
+    var v Validator
+    var result ValidationResult
+
+    if expected.Exactly != ""{
+        v = NewValidator(Text)
+        if result = v.Validate(got, expected.Exactly); !result.Success {
+            return result
+        }
     }
 
-    if err := cmd.Wait(); err != nil {
-        if exiterr, ok := err.(*exec.ExitError); ok {
-            if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
-                c.exitCode = status.ExitStatus()
-                //log.Printf("Exit Status: %d", status.ExitStatus())
+    if len(expected.Contains) > 0 {
+        v = NewValidator(Contains)
+        for _, c := range expected.Contains {
+            if result = v.Validate(got, c); !result.Success {
+                return result
             }
         }
-    } else {
-        c.exitCode = 0
     }
-    c.stderr = strings.Trim(errBuff.String(), "\n")
-    c.stdout = strings.Trim(outBuff.String(), "\n")
 
-    return nil
-}
+    result.Success = true
+    return result
+}
\ No newline at end of file
diff --git a/pkg/runtime/validator.go b/pkg/runtime/validator.go
index 3a13ce4..673855f 100644
--- a/pkg/runtime/validator.go
+++ b/pkg/runtime/validator.go
@@ -1,36 +1,99 @@
 package runtime
 
 import (
-	"github.com/SimonBaeumer/commander/pkg"
+	"fmt"
+	"github.com/pmezard/go-difflib/difflib"
 	"strings"
 )
 
+const (
+	Text     = "text"
+	Contains = "contains"
+	Equal    = "equal"
+)
+
+func NewValidator(validator string) Validator {
+	switch validator {
+	case Text:
+		return TextValidator{}
+	case Contains:
+		return ContainsValidator{}
+	case Equal:
+		return EqualValidator{}
+	default:
+		panic(fmt.Sprintf("Validator '%s' does not exist!", validator))
+	}
+}
+
+type Validator interface {
+    Validate(got interface{}, expected interface{}) ValidationResult
+}
+
 type ValidationResult struct {
 	Success    bool
-	Properties []string
+	Diff       string
+}
+
+type TextValidator struct {
 }
 
-func Validate(test commander.TestCase) ValidationResult {
-	r := &ValidationResult{
-		Success:    true,
-		Properties: []string{},
+func (v TextValidator) Validate(got interface{}, expected interface{}) ValidationResult {
+	result := true
+	if got != expected {
+		result = false
 	}
 
-	if test.Stdout != "" && !strings.Contains(test.Result.Stdout, test.Stdout) {
-		r.Properties = append(r.Properties, commander.Stdout)
+	diff := difflib.UnifiedDiff{
+		A: difflib.SplitLines(got.(string)),
+		B: difflib.SplitLines(expected.(string)),
+		FromFile: "Got",
+		ToFile: "Expected",
+		Context: 3,
 	}
+	diffText, _ := difflib.GetUnifiedDiffString(diff)
 
-	if test.Stderr != "" && !strings.Contains(test.Result.Stderr, test.Stderr) {
-		r.Properties = append(r.Properties, commander.Stderr)
+	return ValidationResult{
+		Diff:       diffText,
+		Success:    result,
 	}
+}
 
-	if test.ExitCode != test.Result.ExitCode {
-		r.Properties = append(r.Properties, commander.ExitCode)
+type ContainsValidator struct {
+}
+
+func (v ContainsValidator) Validate(got interface{}, expected interface{}) ValidationResult {
+	result := true
+	if strings.Contains(got.(string), expected.(string)) {
+		result = false
+	}
+
+	return ValidationResult{
+		Success: result,
+	}
+}
+
+type EqualValidator struct {
+}
+
+func (v EqualValidator) Validate(got interface{}, expected interface{}) ValidationResult {
+	if got == expected {
+		return ValidationResult{
+			Success: true,
+		}
 	}
 
-	if len(r.Properties) > 0 {
-		r.Success = false
+
+	diff := difflib.UnifiedDiff{
+		A: difflib.SplitLines(fmt.Sprintf("%d", got.(int))),
+		B: difflib.SplitLines(fmt.Sprintf("%d", expected.(int))),
+		FromFile: "Got",
+		ToFile: "Expected",
+		Context: 3,
 	}
+	diffText, _ := difflib.GetUnifiedDiffString(diff)
 
-	return *r
+	return ValidationResult{
+		Success: false,
+		Diff: diffText,
+	}
 }
diff --git a/pkg/suite.go b/pkg/suite.go
deleted file mode 100644
index 1948d12..0000000
--- a/pkg/suite.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package commander
-
-const (
-	ExitCode = "ExitCode"
-	Stdout   = "Stdout"
-	Stderr   = "Stderr"
-)
-
-// TestCase represents a test case which will be executed by the runtime
-type TestCase struct {
-	Title    string
-	Command  string
-	Stdout   string
-	Stderr   string
-	ExitCode int
-	Result   TestResult
-}
-
-// TestResult holds the result for a specific test
-type TestResult struct {
-	Success bool
-	//Skipped  bool
-	Stdout            string
-	Stderr            string
-	ExitCode          int
-	FailureProperties []string
-}
-
-// Suite
-type Suite interface {
-	GetTestCases() []TestCase
-}
diff --git a/pkg/yaml_suite.go b/pkg/yaml_suite.go
deleted file mode 100644
index 08f688a..0000000
--- a/pkg/yaml_suite.go
+++ /dev/null
@@ -1,91 +0,0 @@
-package commander
-
-import (
-	"gopkg.in/yaml.v2"
-	"io/ioutil"
-	"log"
-)
-
-// YAMLConfig will be used for unmarshalling the yaml test suite
-type YAMLConfig struct {
-	Tests map[string]YAMLTest `yaml:"tests"`
-}
-
-// YAMLTest represents a test in the yaml test suite
-type YAMLTest struct {
-	Title    string `yaml:"-"`
-	Command  string `yaml:"command"`
-	ExitCode int    `yaml:"exit-code"`
-	Stdout   string `yaml:"stdout,omitempty"`
-	Stderr   string `yaml:"stderr,omitempty"`
-}
-
-// YAMLSuite represents the complete test suite and implements the Suite interface
-type YAMLSuite struct {
-	TestCases []TestCase
-}
-
-// GetTestCases returns the test cases of the suite
-func (s YAMLSuite) GetTestCases() []TestCase {
-	return s.TestCases
-}
-
-// ParseYAMLFile takes a file and returns the Suite
-func ParseYAMLFile(file string) Suite {
-	c, err := ioutil.ReadFile(file)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	return ParseYAML(c)
-}
-
-// ParseYAML parses the Suite from a byte slice
-func ParseYAML(content []byte) Suite {
-	yamlConfig := YAMLConfig{}
-
-	err := yaml.Unmarshal(content, &yamlConfig)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	var s Suite
-	s = YAMLSuite{TestCases: convertYAMLConfToTestCases(yamlConfig)}
-	return s
-}
-
-func convertYAMLConfToTestCases(conf YAMLConfig) []TestCase {
-	var tests []TestCase
-	for _, t := range conf.Tests {
-		tests = append(tests, TestCase{
-			Title:    t.Title,
-			Command:  t.Command,
-			ExitCode: t.ExitCode,
-			Stdout:   t.Stdout,
-			Stderr:   t.Stderr,
-		})
-	}
-
-	return tests
-}
-
-// UnmarshalYAML unmarshals the yaml
-func (y *YAMLConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
-	var params struct {
-		Tests map[string]YAMLTest `yaml:"tests"`
-	}
-
-	err := unmarshal(&params)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	// map key to title property
-	y.Tests = make(map[string]YAMLTest)
-	for k, v := range params.Tests {
-		v.Title = k
-		y.Tests[k] = v
-	}
-
-	return nil
-}


Test patch:
diff --git a/pkg/cmd/command_test.go b/pkg/cmd/command_test.go
new file mode 100644
index 0000000..6476faa
--- /dev/null
+++ b/pkg/cmd/command_test.go
@@ -0,0 +1,39 @@
+package cmd
+
+import (
+    "github.com/stretchr/testify/assert"
+    "testing"
+)
+
+func TestCommand_NewCommand(t *testing.T) {
+    cmd := NewCommand("")
+    assert.False(t, cmd.Executed())
+}
+
+func TestCommand_Execute(t *testing.T) {
+    cmd := NewCommand("/bin/echo hello")
+
+    err := cmd.Execute()
+
+    assert.Nil(t, err)
+    assert.True(t, cmd.Executed())
+    assert.Equal(t, cmd.Stdout(), "hello")
+}
+
+func TestCommand_ExecuteStderr(t *testing.T) {
+    cmd := NewCommand(">&2 /bin/echo hello")
+
+    err := cmd.Execute()
+
+    assert.Nil(t, err)
+    assert.Equal(t, "hello", cmd.Stderr())
+}
+
+func TestCommand_ExitCode(t *testing.T) {
+    cmd := NewCommand("exit 120")
+
+    err := cmd.Execute()
+
+    assert.Nil(t, err)
+    assert.Equal(t, 120, cmd.ExitCode())
+}
\ No newline at end of file
diff --git a/pkg/config/yaml_suite_test.go b/pkg/config/yaml_suite_test.go
new file mode 100644
index 0000000..c07916f
--- /dev/null
+++ b/pkg/config/yaml_suite_test.go
@@ -0,0 +1,98 @@
+package config
+
+import (
+    "github.com/SimonBaeumer/commander/pkg/runtime"
+    "github.com/stretchr/testify/assert"
+	"testing"
+)
+
+func TestYAMLConfig_UnmarshalYAML(t *testing.T) {
+	yaml := []byte(`
+tests:
+    it should print hello:
+        command: echo hello
+        exit-code: 0
+        stdout: hello
+        stderr: anything
+`)
+	tests := ParseYAML(yaml)
+
+	assert.Len(t, tests, 1)
+	assert.Equal(t, "echo hello", tests[0].Command.Cmd)
+	assert.Equal(t, 0, tests[0].Expected.ExitCode)
+	assert.Equal(t, "it should print hello", tests[0].Title)
+	assert.Equal(t, "hello", tests[0].Expected.Stdout.Exactly)
+	assert.Equal(t, "anything", tests[0].Expected.Stderr.Exactly)
+}
+
+func TestYAMLConfig_UnmarshalYAML_ShouldUseTitleAsCommand(t *testing.T) {
+	yaml := []byte(`
+tests:
+    echo hello:
+        exit-code: 0
+        stdout: hello
+        stderr: anything
+`)
+	tests := ParseYAML(yaml)
+
+	assert.Equal(t, "echo hello", tests[0].Command.Cmd)
+	assert.Equal(t, "echo hello", tests[0].Title)
+}
+
+func TestYAMLConfig_UnmarshalYAML_ShouldConvertStdoutToExpectedOut(t *testing.T) {
+	yaml := []byte(`
+tests:
+    echo hello:
+        exit-code: 0
+        stdout:
+            contains:
+                - hello
+                - another hello
+            exactly: exactly hello
+`)
+	tests := ParseYAML(yaml)
+
+	assert.Equal(t, "hello", tests[0].Expected.Stdout.Contains[0])
+	assert.Equal(t, "exactly hello", tests[0].Expected.Stdout.Exactly)
+}
+
+func TestYAMLConfig_UnmarshalYAML_ShouldConvertWithoutContains(t *testing.T) {
+    yaml := []byte(`
+tests:
+    echo hello:
+        exit-code: 0
+        stderr:
+            exactly: exactly stderr
+`)
+    tests := ParseYAML(yaml)
+
+    assert.Equal(t, "exactly stderr", tests[0].Expected.Stderr.Exactly)
+    assert.IsType(t, runtime.ExpectedOut{}, tests[0].Expected.Stdout)
+}
+
+func Test_YAMLConfig_convertToExpectedOut(t *testing.T) {
+    in := map[interface{}]interface{}{"exactly": "exactly stderr"}
+
+    y := YAMLConfig{}
+    got := y.convertToExpectedOut(in)
+
+    assert.IsType(t, runtime.ExpectedOut{}, got)
+    assert.Equal(t, "exactly stderr", got.Exactly)
+}
+
+func TestYAMLConfig_UnmarshalYAML_ShouldPanicIfKeyDoesNotExist(t *testing.T) {
+    defer func() {
+        if r := recover(); r == nil {
+            t.Errorf("Unknown keys should not be parsed, the program should panic")
+        }
+    }()
+
+    yaml := []byte(`
+tests:
+    echo hello:
+        exit-code: 0
+        stderr:
+            typo: exactly stderr
+`)
+    _ = ParseYAML(yaml)
+}
\ No newline at end of file
diff --git a/pkg/runtime/runtime_test.go b/pkg/runtime/runtime_test.go
new file mode 100644
index 0000000..6db2fb7
--- /dev/null
+++ b/pkg/runtime/runtime_test.go
@@ -0,0 +1,32 @@
+package runtime
+
+import (
+    "github.com/stretchr/testify/assert"
+    "testing"
+)
+
+func TestRuntime_Start(t *testing.T) {
+    s := getExampleTestSuite()
+    got := Start(s)
+
+    assert.IsType(t, make(<-chan TestResult), got)
+}
+
+func getExampleTestSuite() []TestCase {
+    tests := []TestCase{
+        {
+            Command: CommandUnderTest{
+                Cmd: "echo hello",
+            },
+            Expected: Expected{
+                Stdout: ExpectedOut{
+                    Exactly: "hello",
+                },
+                ExitCode: 0,
+            },
+            Title: "Output hello",
+        },
+    }
+    return tests
+}
+
diff --git a/pkg/runtime/validator_test.go b/pkg/runtime/validator_test.go
deleted file mode 100644
index 1e4c482..0000000
--- a/pkg/runtime/validator_test.go
+++ /dev/null
@@ -1,171 +0,0 @@
-package runtime
-
-import (
-	"github.com/SimonBaeumer/commander/pkg"
-	"github.com/stretchr/testify/assert"
-	"testing"
-)
-
-const (
-	SuccessCode = 0
-	ErrorCode   = 1
-)
-
-func Test_ValidateExitCodeFail(t *testing.T) {
-	test := commander.TestCase{}
-	test.ExitCode = SuccessCode
-	test.Result = commander.TestResult{
-		ExitCode: ErrorCode,
-	}
-
-	got := Validate(test)
-
-	assert.False(t, got.Success)
-    assert.Len(t, got.Properties, 1)
-}
-
-func Test_ValidateExitCodeSuccess(t *testing.T) {
-	test := commander.TestCase{}
-	test.ExitCode = SuccessCode
-	test.Result = commander.TestResult{
-		ExitCode: SuccessCode,
-	}
-
-	got := Validate(test)
-
-	assert.True(t, got.Success)
-    assert.Len(t, got.Properties, 0)
-}
-
-func Test_ValidateStdoutFail(t *testing.T) {
-	test := commander.TestCase{}
-	test.Stdout = "foo"
-	test.Result = commander.TestResult{
-		Stdout: "bar",
-	}
-
-	got := Validate(test)
-
-	assert.False(t, got.Success)
-    assert.Len(t, got.Properties, 1)
-}
-
-func Test_ValidateStdoutSuccess(t *testing.T) {
-	test := commander.TestCase{}
-	test.Stdout = "foo"
-	test.Result = commander.TestResult{
-		Stdout: "foo",
-	}
-
-	got := Validate(test)
-
-	assert.True(t, got.Success)
-    assert.Len(t, got.Properties, 0)
-}
-
-func Test_ValidateStderrFail(t *testing.T) {
-	test := commander.TestCase{Stdout: "foo"}
-	test.Result = commander.TestResult{
-		Stdout: "bar",
-	}
-
-	got := Validate(test)
-
-	assert.False(t, got.Success)
-    assert.Len(t, got.Properties, 1)
-}
-
-func Test_ValidateStderrSuccess(t *testing.T) {
-	test := commander.TestCase{Stdout: "foo"}
-	test.Result = commander.TestResult{
-		Stdout: "foo",
-	}
-
-	got := Validate(test)
-
-	assert.True(t, got.Success)
-    assert.Len(t, got.Properties, 0)
-}
-
-func Test_Validate(t *testing.T) {
-	test := commander.TestCase{
-		Stdout:   "foo",
-		Stderr:   "bar",
-		ExitCode: SuccessCode,
-	}
-	test.Result = commander.TestResult{
-		Stdout:   "foo",
-		Stderr:   "bar",
-		ExitCode: SuccessCode,
-	}
-
-	got := Validate(test)
-
-	assert.True(t, got.Success)
-	assert.Len(t, got.Properties, 0)
-}
-
-func Test_ValidateFail(t *testing.T) {
-	test := commander.TestCase{
-		Stdout:   "fail",
-		Stderr:   "fail",
-		ExitCode: ErrorCode,
-	}
-	test.Result = commander.TestResult{
-		Stdout:   "foo",
-		Stderr:   "bar",
-		ExitCode: SuccessCode,
-	}
-
-	got := Validate(test)
-
-	assert.False(t, got.Success)
-	assert.Equal(t, []string{"Stdout", "Stderr", "ExitCode"}, got.Properties)
-}
-
-func Test_ValidateContains(t *testing.T) {
-	test := commander.TestCase{
-		Stdout:   `
-✓ it should assert stderr
-`,
-		Stderr:   `
-this
-`,
-		ExitCode: 0,
-	}
-	test.Result = commander.TestResult{
-		Stdout:   `
-✓ it should exit with error code
-✓ it should assert stderr
-✓ it should assert stdout
-✓ it should fail'
-`,
-		Stderr:   `
-this
-is
-my
-stderr
-and
-more`,
-		ExitCode: SuccessCode,
-	}
-
-	got := Validate(test)
-
-	assert.True(t, got.Success)
-	assert.Len(t, got.Properties, 0)
-}
-
-func Test_ValidateWithEmptyStdoutAndStderr(t *testing.T) {
-    test := commander.TestCase{ExitCode: SuccessCode}
-    test.Result = commander.TestResult{
-        Stdout: "out",
-        Stderr: "err",
-        ExitCode: SuccessCode,
-    }
-
-    got := Validate(test)
-
-    assert.True(t, got.Success)
-    assert.Len(t, got.Properties, 0)
-}
\ No newline at end of file
diff --git a/pkg/yaml_suite_test.go b/pkg/yaml_suite_test.go
deleted file mode 100644
index aaf04c9..0000000
--- a/pkg/yaml_suite_test.go
+++ /dev/null
@@ -1,49 +0,0 @@
-package commander
-
-import (
-	"github.com/stretchr/testify/assert"
-	"testing"
-)
-
-var ExampleYaml = []byte(`
-tests:
-    "should print something":
-        command: echo something
-        exit-code: 0
-        stdout: something
-    `)
-
-func Test_ParseYamlFile(t *testing.T) {
-	got := ParseYAMLFile("./_fixtures/commander.yaml")
-
-	assert.Implements(t, (*Suite)(nil), got)
-}
-
-func Test_ParseYaml(t *testing.T) {
-	got := ParseYAML(ExampleYaml)
-
-	assert.Implements(t, (*Suite)(nil), got)
-	assert.Len(t, got.GetTestCases(), 1)
-
-	test := got.GetTestCases()[0]
-	assert.Equal(t, "should print something", test.Title)
-	assert.Equal(t, "echo something", test.Command)
-	assert.Equal(t, "something", test.Stdout)
-	assert.Equal(t, 0, test.ExitCode)
-}
-
-func Test_ParseMutlipleYamlTests(t *testing.T) {
-	var multiExampleYaml = []byte(`
-tests:
-    "should print something":
-        command: echo something
-        exit-code: 0
-    "should print hello":
-        command: echo hello
-        exit-code: 0
-    `)
-
-	got := ParseYAML(multiExampleYaml)
-
-	assert.Len(t, got.GetTestCases(), 2)
-}