diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..252e070 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,17 @@ +language: go +dist: xenial + +os: + - linux + +env: + - GO111MODULE=on + +go: + - 1.12.x + +script: + - go test -failfast -v -coverprofile=coverage.txt -covermode=atomic ./... + +after_success: + - bash <(curl -s https://codecov.io/bash) diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e5dd727 --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +.PHONY: test +test: + @echo "running tests on $(run). waiting for changes..." + @-zsh -c "go test ./...; repeat 100 printf '#'; echo" + @reflex -d none -r "(\.go$$)|(go.mod)" -- zsh -c "go test ./...; repeat 100 printf '#'" + +.PHONY: test_race +test_race: + @echo "running tests on $(run). waiting for changes..." + @-zsh -c "go test -race ./...; repeat 100 printf '#'; echo" + @reflex -d none -r "(\.go$$)|(go.mod)" -- zsh -c "go test -race ./...; repeat 100 printf '#'" + +.PHONY: third-party +third-party: + @go get -u github.com/cespare/reflex + +.PHONY: clean +clean: + go clean -cache -testcache diff --git a/README.md b/README.md index 9b26806..70b7eb5 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,70 @@ # dbtesting -Utility for using with go-sqlmock library. +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) +[![GoDoc](https://godoc.org/github.com/arsham/dbtesting?status.svg)](http://godoc.org/github.com/arsham/dbtesting) +[![Build Status](https://travis-ci.org/arsham/dbtesting.svg?branch=master)](https://travis-ci.org/arsham/dbtesting) +[![Coverage Status](https://codecov.io/gh/arsham/dbtesting/branch/master/graph/badge.svg)](https://codecov.io/gh/arsham/dbtesting) -This library can be used in the [go-sqlmock][go-sqlmock] test cases for cases -that values are random but it is important to check the values passed in +This library has a few helpers for using in tests. + +1. [Spec Reports](#spec-reports) + * [Usage](#usage) +2. [SQLMock Helpers](#sqlmock-helpers) + * [ValueRecorder](#valuerecorder) + * [OkValue](#okvalue) +3. [Testing](#testing) +4. [License](#license) + +## Spec Reports + +`Mocha` is a reporter for printing Mocha inspired reports when using +[spec BDD library][spec]. + +### Usage + +```go +import "github.com/arsham/dbtesting" + +func TestFoo(t *testing.T) { + spec.Run(t, "Foo", func(t *testing.T, when spec.G, it spec.S) { + // ... + }, spec.Report(&dbtesting.Mocha{})) +} + +``` + +You can set an `io.Writer` to `Mocha.Out` to redirect the output, otherwise it +prints to the `os.Stdout`. + +## SQLMock Helpers + +There a couple of helpers for using with [go-sqlmock][go-sqlmock] test cases for +cases that values are random but it is important to check the values passed in queries. -## ValueRecorder +### ValueRecorder -If you generate a UUID and use it in multiple queries and you want to make sure -the queries are passed with correct IDs. For instance if in your code you have: +If you have an value and use it in multiple queries, and you want to +make sure the queries are passed with correct values, you can use the +`ValueRecorder`. For example UUIDs, time and random values. + +For instance if the first query generates a random number but it is essential to +use the same value on next queries: ```go import "database/sql" -// ... - -// assume num has been generated randomly -num := 666 -_, err := tx.ExecContext(ctx, "INSERT INTO life (value) VALUE ($1)", num) -// error check -_, err := tx.ExecContext(ctx, "INSERT INTO reality (value) VALUE ($1)", num) -// error check -_, err := tx.ExecContext(ctx, "INSERT INTO everywhere (value) VALUE ($1)", num) -// error check +func TestFoo(t *testing.T) { + // ... + // assume num has been generated randomly + num := 666 + _, err := tx.ExecContext(ctx, "INSERT INTO life (value) VALUE ($1)", num) + // error check + _, err = tx.ExecContext(ctx, "INSERT INTO reality (value) VALUE ($1)", num) + // error check + _, err = tx.ExecContext(ctx, "INSERT INTO everywhere (value) VALUE ($1)", num) + // error check +} ``` Your tests can be checked easily like this: @@ -31,49 +72,80 @@ Your tests can be checked easily like this: import ( "github.com/arsham/dbtesting" "github.com/DATA-DOG/go-sqlmock" - // ... ) -rec := dbtesting.NewValueRecorder() -mock.ExpectExec("INSERT INTO life .+"). - WithArgs(rec.Record("truth")). - WillReturnResult(sqlmock.NewResult(1, 1)) -mock.ExpectExec("INSERT INTO reality .+"). - WithArgs(rec.For("truth")). - WillReturnResult(sqlmock.NewResult(1, 1)) -mock.ExpectExec("INSERT INTO everywhere .+"). - WithArgs(rec.For("truth")). - WillReturnResult(sqlmock.NewResult(1, 1)) +func TestFoo(t *testing.T) { + // ... + rec := dbtesting.NewValueRecorder() + mock.ExpectExec("INSERT INTO life .+"). + WithArgs(rec.Record("truth")). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectExec("INSERT INTO reality .+"). + WithArgs(rec.For("truth")). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectExec("INSERT INTO everywhere .+"). + WithArgs(rec.For("truth")). + WillReturnResult(sqlmock.NewResult(1, 1)) +} +``` + +Recorded values can be retrieved by casting to their types: +```go +rec.Value("true").(string) ``` -## OkValue +There are two rules for using the `ValueRecorder`: +1. You can only record for a value once. +2. You should record a value before you call `For` or `Value`. + +It will panic if these requirements are not met. -When you are only interested in checking some arguments passed to the Exec/Query +### OkValue + +If you are only interested in checking some arguments passed to the Exec/Query functions and you don't want to check everything (maybe because thy are not -relevant to the current test), you can use the `OkValue`. +relevant to the current test), you can use `OkValue`. ```go import ( "github.com/arsham/dbtesting" "github.com/DATA-DOG/go-sqlmock" - // ... ) +ok := dbtesting.OkValue mock.ExpectExec("INSERT INTO life .+"). WithArgs( - dbtesting.OkValue, - dbtesting.OkValue, - dbtesting.OkValue, - "import value" - dbtesting.OkValue, - dbtesting.OkValue, - dbtesting.OkValue, + ok, + ok, + ok, + "important value" + ok, + ok, + ok, ) ``` -## LICENSE +## Testing + +To run the tests: + +```bash +make +``` +or for with `-race` flag: +```bash +make test_race +``` + +If you don't have `reflex` installed, run the following once: +```bash +make third-party +``` + +## License Use of this source code is governed by the Apache 2.0 license. License can be found in the [LICENSE](./LICENSE) file. -[go-sqlmock]: github.com/DATA-DOG/go-sqlmock +[go-sqlmock]: https://github.com/DATA-DOG/go-sqlmock +[spec]: https://github.com/sclevine/spec diff --git a/dbtesting_test.go b/dbtesting_test.go index 1bf483d..76f208a 100644 --- a/dbtesting_test.go +++ b/dbtesting_test.go @@ -253,3 +253,30 @@ func ExampleValueRecorder() { // Output: // got recorded value: 666 } + +func ExampleValueRecorder_value() { + db, mock, err := sqlmock.New() + if err != nil { + panic(err) + } + defer db.Close() + defer func() { + if err := mock.ExpectationsWereMet(); err != nil { + fmt.Printf("there were unfulfilled expectations: %s", err) + } + }() + rec := dbtesting.NewValueRecorder() + mock.ExpectExec("INSERT INTO life .+"). + WithArgs(rec.Record("meaning")). + WillReturnResult(sqlmock.NewResult(1, 1)) + + _, err = db.Exec("INSERT INTO life (name) VALUE ($1)", 42) + if err != nil { + panic(err) + } + + fmt.Printf("Meaning of life: %d", rec.Value("meaning").(int64)) + + // Output: + // Meaning of life: 42 +} diff --git a/go.mod b/go.mod index 648963d..eb7c7e2 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,9 @@ module github.com/arsham/dbtesting go 1.12 -require github.com/DATA-DOG/go-sqlmock v1.3.3 +require ( + github.com/DATA-DOG/go-sqlmock v1.3.3 + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/sclevine/spec v1.2.0 + github.com/stretchr/testify v1.3.0 +) diff --git a/go.sum b/go.sum index e787bfc..eecc193 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,12 @@ github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08= github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sclevine/spec v1.2.0 h1:1Jwdf9jSfDl9NVmt8ndHqbTZ7XCCPbh1jI3hkDBHVYA= +github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= diff --git a/mocha.go b/mocha.go new file mode 100644 index 0000000..c4ae5e4 --- /dev/null +++ b/mocha.go @@ -0,0 +1,64 @@ +package dbtesting + +import ( + "fmt" + "io" + "os" + "strings" + "sync" + "testing" + + "github.com/sclevine/spec" +) + +// Mocha prints spec reports in terminal. +type Mocha struct { + once sync.Once + Out io.Writer // if not set it will print to stdout +} + +func (m *Mocha) setup() { + if m.Out == nil { + m.Out = os.Stdout + } +} + +// Start prints some information when the suite is started. +func (m *Mocha) Start(_ *testing.T, plan spec.Plan) { + m.once.Do(m.setup) + fmt.Fprintln(m.Out, "Suite:", plan.Text) + fmt.Fprintf(m.Out, "Total: %d | Focused: %d | Pending: %d\n", plan.Total, plan.Focused, plan.Pending) + if plan.HasRandom { + fmt.Fprintln(m.Out, "Random seed:", plan.Seed) + } + if plan.HasFocus { + fmt.Fprintln(m.Out, "Focus is active.") + } +} + +// Specs prints information about specs' results while suite is running. +func (m *Mocha) Specs(_ *testing.T, specs <-chan spec.Spec) { + m.once.Do(m.setup) + var passed, failed, skipped int + fs := "\033[31m" + "✘" + ps := "\033[32m" + "✔" + ss := "\033[32m" + "✱" + for s := range specs { + switch { + case s.Failed: + failed++ + fmt.Fprint(m.Out, fs) + case s.Skipped: + skipped++ + fmt.Fprint(m.Out, ss) + default: + passed++ + fmt.Fprint(m.Out, ps) + } + for i, txt := range s.Text { + fmt.Fprintln(m.Out, strings.Repeat(" ", i*3), " ", txt) + } + fmt.Fprint(m.Out, "\033[0m") + } + fmt.Fprintf(m.Out, "\nPassed: %d | Failed: %d | Skipped: %d\n\n", passed, failed, skipped) +} diff --git a/mocha_test.go b/mocha_test.go new file mode 100644 index 0000000..0af9596 --- /dev/null +++ b/mocha_test.go @@ -0,0 +1,129 @@ +package dbtesting_test + +import ( + "bufio" + "bytes" + "os" + "strconv" + "strings" + "testing" + + "github.com/arsham/dbtesting" + "github.com/sclevine/spec" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTerminal(t *testing.T) { + t.Run("Start", testTerminalStart) + t.Run("Specs", testTerminalSpecs) +} + +func testTerminalStart(t *testing.T) { + t.Run("Stdout", testTerminalStartStdout) + t.Run("Buffer", testTerminalStartBuffer) +} + +func testTerminalStartStdout(t *testing.T) { + // This test replaces os.Stdout and should not be run in parallel with other + // tests. + original := os.Stdout + r, w, err := os.Pipe() + require.NoError(t, err) + os.Stdout = w + defer func() { + os.Stdout = original + }() + + buf := &bytes.Buffer{} + done := make(chan struct{}) + go func() { + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := scanner.Text() + buf.WriteString(line) + } + close(done) + }() + p := spec.Plan{Text: "satan"} + m := &dbtesting.Mocha{} + m.Start(t, p) + w.Close() + <-done + content := buf.String() + assert.Contains(t, content, "satan") +} + +func testTerminalStartBuffer(t *testing.T) { + t.Parallel() + tcs := map[string]struct { + plan spec.Plan + want string + }{ + "suite name": { + spec.Plan{Text: "satan"}, + "satan", + }, + "has random": { + spec.Plan{HasRandom: true, Seed: 666}, + "666", + }, + "has focus": { + spec.Plan{HasFocus: true}, + "Focus", + }, + } + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + buf := &bytes.Buffer{} + m := &dbtesting.Mocha{ + Out: buf, + } + m.Start(t, tc.plan) + content := buf.String() + assert.Contains(t, content, tc.want) + }) + } +} + +func testTerminalSpecs(t *testing.T) { + t.Parallel() + buf := &bytes.Buffer{} + m := &dbtesting.Mocha{ + Out: buf, + } + specs := make(chan spec.Spec, 20) + getSpec := func(i int, failed, skipped bool) spec.Spec { + return spec.Spec{ + Text: []string{"passed " + strconv.Itoa(i)}, + Skipped: skipped, + Failed: failed, + } + } + // passed + specs <- getSpec(1, false, false) + specs <- getSpec(2, false, false) + // failed + specs <- getSpec(3, true, false) + specs <- getSpec(4, true, false) + specs <- getSpec(5, true, false) + // skipped + specs <- getSpec(6, false, true) + specs <- getSpec(7, false, true) + specs <- getSpec(8, false, true) + specs <- getSpec(9, false, true) + close(specs) + + m.Specs(t, specs) + content := strings.ToLower(buf.String()) + tcs := map[string]string{ + "passed": "passed: 2", + "failed": "failed: 3", + "skipped": "skipped: 4", + } + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + assert.Contains(t, content, tc) + }) + } +}