dbtools

package module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 17, 2019 License: Apache-2.0 Imports: 6 Imported by: 0

README

dbtools

License GoDoc Build Status Coverage Status

This library has a few helpers for using in production code and go-sqlmock tests. There is also a Mocha inspired reporter for spec BDD library.

  1. Transaction
  2. Spec Reports
  3. SQLMock Helpers
  4. Testing
  5. License

Transaction

WithTransaction

WithTransaction helps you reduce the amount of code you put in the logic by taking care of errors. For example instead of writing:

tx, err := db.Begin()
if err != nil {
    return errors.Wrap(err, "starting transaction")
}
err := firstQueryCall(tx)
if err != nil {
    e := errors.Wrap(tx.Rollback(), "rolling back transaction")
    return multierror.Append(err, e).ErrorOrNil()
}
err := secondQueryCall(tx)
if err != nil {
    e := errors.Wrap(tx.Rollback(), "rolling back transaction")
    return multierror.Append(err, e).ErrorOrNil()
}
err := thirdQueryCall(tx)
if err != nil {
    e := errors.Wrap(tx.Rollback(), "rolling back transaction")
    return multierror.Append(err, e).ErrorOrNil()
}

return errors.Wrap(tx.Commit(), "committing transaction")

You can write:

return dbtools.WithTransaction(db, firstQueryCall, secondQueryCall, thirdQueryCall)

Function types should be of func(*sql.Tx) error.

Retry

Retry calls your function, and if it errors it calls it again with a delay. Every time the function returns an error it increases the delay. Eventually it returns the last error or nil if one call is successful.

You can use this function in non-database situations too.

dbtools.Retry(10, time.Second. func(i int) error {
    logger.Debugf("%d iteration", i)
    return myFunctionCall()
})
RetryTransaction

RetryTransaction is a combination of WithTransaction and Retry. It stops the retry if the context is cancelled/done.

err := dbtools.RetryTransaction(ctx, db, 10, time.Millisecond * 10,
    firstQueryCall,
    secondQueryCall,
    thirdQueryCall,
)
// error check

Spec Reports

Mocha is a reporter for printing Mocha inspired reports when using spec BDD library.

Usage
import "github.com/arsham/dbtools/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 test cases for cases that values are random but it is important to check the values passed in queries.

ValueRecorder

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:

import "database/sql"

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:

import (
    "github.com/arsham/dbtools/dbtesting"
    "github.com/DATA-DOG/go-sqlmock"
)

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:

rec.Value("true").(string)

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.

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 OkValue.

import (
    "github.com/arsham/dbtools/dbtesting"
    "github.com/DATA-DOG/go-sqlmock"
)

ok := dbtesting.OkValue
mock.ExpectExec("INSERT INTO life .+").
    WithArgs(
        ok,
        ok,
        ok,
        "important value"
        ok,
        ok,
        ok,
    )

Testing

To run the tests:

make

test_race target runs tests with -race flag. third-party installs reflex task runner.

License

Use of this source code is governed by the Apache 2.0 license. License can be found in the LICENSE file.

Documentation

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Retry

func Retry(retries int, delay time.Duration, fn func(int) error) error

Retry calls fn for retries times until it returns nil. If retries is zero fn would not be called. It delays and retries if the function returns any errors. The fn function receives the current iteration as its argument.

Example
package main

import (
	"fmt"
	"time"

	"github.com/arsham/dbtools"
	"github.com/pkg/errors"
)

func main() {
	err := dbtools.Retry(100, time.Nanosecond, func(i int) error {
		fmt.Printf("Running iteration %d.\n", i+1)
		if i < 1 {
			return errors.New("ignored error")
		}
		return nil
	})
	fmt.Println("Error:", err)

}
Output:

Running iteration 1.
Running iteration 2.
Error: <nil>
Example (Two)
package main

import (
	"fmt"
	"time"

	"github.com/arsham/dbtools"
	"github.com/pkg/errors"
)

func main() {
	err := dbtools.Retry(2, time.Nanosecond, func(i int) error {
		fmt.Printf("Running iteration %d.\n", i+1)
		return errors.New("some error")
	})
	fmt.Println("Error:", err)

}
Output:

Running iteration 1.
Running iteration 2.
Error: some error

func RetryTransaction

func RetryTransaction(ctx context.Context, db *sql.DB, retries int, delay time.Duration, fn ...func(*sql.Tx) error) error

RetryTransaction combines WithTransaction and Retry calls. It stops the call if context is times out or cancelled.

Example
// For this example we are using sqlmock, but you can use an actual
// connection with this function.
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\n", err)
	}
}()

mock.ExpectBegin()
mock.ExpectRollback()
mock.ExpectBegin()
mock.ExpectRollback()
mock.ExpectBegin()
mock.ExpectCommit()

calls := 0
ctx := context.Background()
err = dbtools.RetryTransaction(ctx, db, 100, time.Millisecond*100, func(*sql.Tx) error {
	calls++
	fmt.Printf("Running iteration %d.\n", calls)
	if calls < 3 {
		return errors.New("some error")
	}
	return nil
})
fmt.Println("Error:", err)
Output:

Running iteration 1.
Running iteration 2.
Running iteration 3.
Error: <nil>

func WithTransaction

func WithTransaction(db *sql.DB, fn ...func(*sql.Tx) error) error

WithTransaction creates a transaction on db and uses it to call fn functions one by one. The first function that returns an error will cause the loop to stop and transaction to be rolled back.

Example
// For this example we are using sqlmock, but you can use an actual
// connection with this function.
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\n", err)
	}
}()
mock.ExpectBegin()
mock.ExpectCommit()
err = dbtools.WithTransaction(db, func(*sql.Tx) error {
	fmt.Println("Running first query.")
	return nil
}, func(*sql.Tx) error {
	fmt.Println("Running second query.")
	return nil
})
fmt.Println("Transaction has an error:", err != nil)
Output:

Running first query.
Running second query.
Transaction has an error: false
Example (Two)
// For this example we are using sqlmock, but you can use an actual
// connection with this function.
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\n", err)
	}
}()
mock.ExpectBegin()
mock.ExpectRollback()
err = dbtools.WithTransaction(db, func(*sql.Tx) error {
	fmt.Println("Running first query.")
	return nil
}, func(*sql.Tx) error {
	fmt.Println("Running second query.")
	return errors.New("something happened")
}, func(*sql.Tx) error {
	fmt.Println("Running third query.")
	return nil
})
fmt.Println("Transaction has an error:", err != nil)
Output:

Running first query.
Running second query.
Transaction has an error: true

Types

This section is empty.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL