diff --git a/.gitignore b/.gitignore
index 5f924b621b8924d90e43e0f57d12b3b3003efac2..cec487f155b85442477e62a03271809a062bbe7c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,4 +26,7 @@ _testmain.go
 
 vendor/*/
 .env
+
+coverage.*
+
 vaultenv
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 5bc29b5edbec90844618cc49c31f14b8e6ad4a20..0a720300837da7564ab7e0c0bbb0e1e3cfb442e2 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -9,3 +9,36 @@ include:
     file: 'SonarQube.gitlab-ci.yml'
   - project: 'gitlab/templates'
     file: 'Go-CLI.gitlab-ci.yml'
+
+go-test:
+  image: golang:$GOLANG_VERSION
+  stage: test
+  rules:
+    - if: $CI_COMMIT_BRANCH
+  cache:
+    key:
+      files:
+        - go.mod
+        - go.sum
+    paths:
+      - .cache
+    policy: pull
+  services:
+    - name: hashicorp/vault:latest
+      alias: vault
+      command: ["server","-dev-kv-v1","-dev-root-token-id","00000000-0000-0000-0000-000000000000"]
+  script:
+    - |
+      export VAULT_ADDR=http://vault:8200
+      export VAULT_TOKEN=00000000-0000-0000-0000-000000000000
+      export GOPATH=${CI_PROJECT_DIR}/.cache
+      export PATH=${PATH}:${GOPATH}/bin
+      go test -covermode=count -coverprofile=coverage.txt $(go list ./... | grep -v /vendor/)
+      (gocover-cobertura < coverage.txt > coverage.xml) || true
+  artifacts:
+    paths:
+        - coverage.txt
+    reports:
+      coverage_report:
+        coverage_format: cobertura
+        path: coverage.xml
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index a2883d2b5e481f7cb27d0c2fa8005c96122eaa47..ad8b6c72237e1bfa2e6b20478105c49e8fd3c6ed 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -40,5 +40,14 @@
         "reveal": "silent"
       }
     },
+    {
+      "label": "Start vault dev server",
+      "type": "shell",
+      "command": "docker run --rm -it -p 8200:8200 --name vault-dev hashicorp/vault:latest server -dev-kv-v1 -dev-root-token-id 00000000-0000-0000-0000-000000000000",
+      "group": {
+        "kind": "test",
+        "isDefault": false
+      }
+    },
   ]
 }
diff --git a/cmd/root.go b/cmd/root.go
index 6035e42138a767ec60707e0d09bface251c51010..1961d998af882d2a8326a7d32a1611d0cf8520a8 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -13,6 +13,7 @@ var RootCmd = &cobra.Command{
 	Short:             "Root Short",
 	Long:              `Root Long`,
 	DisableAutoGenTag: true,
+	SilenceUsage:      true,
 }
 
 func init() {
@@ -20,8 +21,8 @@ func init() {
 	RootCmd.PersistentFlags().StringP("addr", "a", "http://127.0.0.1:8200", "Address to the vault server")
 	RootCmd.PersistentFlags().StringP("token", "t", "", "Vault access token")
 	viper.SetEnvPrefix("VAULT")
-	viper.BindPFlag("ADDR", RootCmd.PersistentFlags().Lookup("addr"))
-	viper.BindPFlag("TOKEN", RootCmd.PersistentFlags().Lookup("token"))
+	_ = viper.BindPFlag("ADDR", RootCmd.PersistentFlags().Lookup("addr"))
+	_ = viper.BindPFlag("TOKEN", RootCmd.PersistentFlags().Lookup("token"))
 }
 
 func initEnv() {
diff --git a/cmd/root_test.go b/cmd/root_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..d1b92217b6badfd763f9c87ebf5898df947d9dd4
--- /dev/null
+++ b/cmd/root_test.go
@@ -0,0 +1,57 @@
+package cmd
+
+import (
+	"bytes"
+	"io"
+	"os"
+	"strings"
+	"sync"
+	"testing"
+
+	_ "gitlab.hedenstroem.com/go/vaultenv/testing"
+)
+
+func execute(t *testing.T, 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)
+		if err != nil {
+			t.Error(err)
+		}
+		wg.Done()
+	}()
+
+	RootCmd.SetIn(input)
+	RootCmd.SetOut(output) // usage messages
+	RootCmd.SetErr(output) // error messages
+	RootCmd.SetArgs(strings.Split(args, " "))
+	err := RootCmd.Execute()
+
+	w.Close()
+	wg.Wait()
+
+	return output.String(), err
+}
+
+func TestWriteCmd(t *testing.T) {
+	_, err := execute(t, "write secret/test hello world", nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	out, err := execute(t, "read secret/test", nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if strings.Contains(out, "\"hello\": \"world\"") == false {
+		t.Errorf("expected \"hello\": \"world\", got '%s'", out)
+	}
+}
diff --git a/main.go b/main.go
index ff9a66f5612aaada43c2e6d205808da1fb6a4ab0..f5b508dc95e12a4e388ac9c726f4039993745a42 100644
--- a/main.go
+++ b/main.go
@@ -20,8 +20,11 @@
 
 package main
 
-import "gitlab.hedenstroem.com/go/vaultenv/cmd"
+import (
+	"github.com/spf13/cobra"
+	"gitlab.hedenstroem.com/go/vaultenv/cmd"
+)
 
 func main() {
-	cmd.RootCmd.Execute()
+	cobra.CheckErr(cmd.RootCmd.Execute())
 }
diff --git a/sonar-project.properties b/sonar-project.properties
index ee63546e3b6e2ef8d5d6e39b2cacadb5a86e3887..ac9921a3acd7b783764edf4d37b8974559d265e9 100644
--- a/sonar-project.properties
+++ b/sonar-project.properties
@@ -1 +1,9 @@
 sonar.projectKey=go_vaultenv_AXyPJ7AsH35cfvcLwDFS
+
+sonar.sources=.
+sonar.exclusions=**/*_test.go
+
+sonar.tests=.
+sonar.test.inclusions=**/*_test.go
+
+sonar.go.coverage.reportPaths=coverage.txt
diff --git a/testing/testing.go b/testing/testing.go
new file mode 100644
index 0000000000000000000000000000000000000000..413cb4805906a1ee32f744bb5afbe42b3c857ecc
--- /dev/null
+++ b/testing/testing.go
@@ -0,0 +1,16 @@
+package testing
+
+import (
+	"os"
+	"path"
+	"runtime"
+)
+
+func init() {
+	_, filename, _, _ := runtime.Caller(0)
+	dir := path.Join(path.Dir(filename), "..")
+	err := os.Chdir(dir)
+	if err != nil {
+		panic(err)
+	}
+}
diff --git a/vault/http.go b/vault/http.go
index d3d76db7be1db3a782ef4ab95e5e3d596a386058..4628ab4fe22a4f368289a1ce9250aaadfecfc1da 100644
--- a/vault/http.go
+++ b/vault/http.go
@@ -40,7 +40,10 @@ func GetSecret(path string) (data map[string]interface{}, err error) {
 	case http.StatusOK:
 		var parsed map[string]interface{}
 		defer res.Body.Close()
-		json.NewDecoder(res.Body).Decode(&parsed)
+		err = json.NewDecoder(res.Body).Decode(&parsed)
+		if err != nil {
+			return
+		}
 		data = parsed["data"].(map[string]interface{})
 	case http.StatusNoContent:
 		data = make(map[string]interface{})
@@ -52,7 +55,10 @@ func GetSecret(path string) (data map[string]interface{}, err error) {
 	default:
 		var parsed map[string]interface{}
 		defer res.Body.Close()
-		json.NewDecoder(res.Body).Decode(&parsed)
+		err = json.NewDecoder(res.Body).Decode(&parsed)
+		if err != nil {
+			return
+		}
 		err = &Error{
 			Status:  res.StatusCode,
 			Message: fmt.Sprint(parsed["errors"]),