简体   繁体   中英

Re-usable components and fixtures in Golang tests

I'm just getting started with Golang and writing my first test suite.

I have a background in Rails, which has fantastic support for testing tools (Rspec, Cucumber, etc..), so I'm approaching my golang tests with a similar mindset (not sure if that's the right or wrong thing to do)

I have a User data model (basically a struct) that reads records from a users table in postgres and stores an array of them. (Essentially a really simple version of what ActiveRecord does in the Rails world)

I'd like to write a test that checks if the routine correctly reads from the DB and builds the models.

  1. In almost every test suite I'll be connecting to the DB so I have a helper named establish_db_connection . Where can I place this so that it's centrally available to all my tests?

  2. Building off #1 - is there an equivalent of a before block or some setup/teardown method where I can establish a connection before every test?

  3. Lastly, how do I handle fixtures? Right now before each test I call a clear_db function that resets all tables and inserts some static data rows. I'd love to move away from fixtures and use factories to build data as needed (very similar to FactoryGirl in Rails), but not sure how common that is in Golang.

  4. Is the built-in go test framework the best approach, or are there better alternatives?

  1. Go is based on strong package management, meaning a namespace is treated as one single file. If establish_db_connection is used within a single test package, it can begin with a lowercase letter to signify a private instance and use it in the test file with the same package as the code being tested (Note that naming convention in Go is establishDBConnection ). However, most of the time, as in data/sql , you will want to obtain a DB connection once and keep that around until the test is finished (more like a factory and injection pattern).

  2. There is none in the standard testing package. If you like BDD, Goconvey use scopes to define fixtures and a reset function for teardown.

  3. You can use factory and dependency injections in your testing. I think that's pretty idiomatic.

  4. A few includes Goconvey , Ginkgo and Testify They all have pros and cons of their own. The first two often end up with too many nested scopes, but Goconvey has a great browser-based real-time testing server which can be used with Go standard testing.

Since there's no global variables/functions in Go, you might design your project in interface-delegate pattern to help with importing functions cross packages and avoiding cyclic imports when dealing with cross-package testing.

mypackage

type DBOptions struct {
        Name, Credentials string
}

func aFunc(db *sql.DB) error {
        // do something
        return nil
}

func bFunc(db *sql.DB) int, error {
        // do something
        return 0, nil
}

func establishConn(opts *DBOptions) (*sql.DB, error) {
        db, err := sql.Open(opts.Name, opts.Credentials)
        if err != nil {
                return nil, err
        }
        return db, nil
}

func destroyConn(conn *sql.DB) {
        conn.Close()
}

// test file
mypackage 

import "testing"

var myOpt = &DBOptions{
        Name: "mysql", 
        Credentials: "user:password@tcp(127.0.0.1:3306)/hello",
}

var conn, _ = establishConn(myOpt)

func TestAFunc(t *testing.T) {
        err := aFunc(conn)

        if err != nil  {
                t.Error(err)
        }
}

func TestBFunc(t *testing.T) {
        err := aFunc(conn)

        if err != nil  {
                t.Error(err)
        }
}

// use `conn` in other tests ...

destroyConn(conn)

About test fixture library similar to FactoryGirl in Rails, There are some choices in go.

those two libraries got stars most.

And I also implemented test-fixture library which is type-safe, DRY, and flexible compared to the above libraries!

on fixtures: consider passing functions in your testcases:

package main

import "testing"

type testcase struct {
    scenario  string
    before    func(string)
    after     func()
    input     string
    expOutput string
}

var state = ""

func setup(s string) {
    state = s
}

func nilSetup(s string) {}

func reset() {
    state = ""
}

func execute(s string) string {
    return state
}

func TestSetupTeardown(t *testing.T) {
    tcs := []testcase{
        {
            scenario:  "blank output when initial state is wrong",
            before:    nilSetup,
            after:     reset,
            input:     "foo",
            expOutput: "",
        },
        {
            scenario:  "correct output when initial state is right",
            before:    setup,
            after:     reset,
            input:     "foo",
            expOutput: "foo",
        },
    }

    for _, tc := range tcs {
        tc.before(tc.input)
        if out := execute(tc.input); out != tc.expOutput {
            t.Fatal(tc.scenario)
        }
        tc.after()
    }
}

I built a tiny utility library to make it easy to create reusable fixtures to tests in go. Check out https://github.com/houqp/gtest to see if it solves your problem.

Here is a quick example on how to create a database transaction fixture for each test in a test group:

type TransactionFixture struct{}

// Construct can take other fixtures as input parameter as well
func (s TransactionFixture) Construct(t *testing.T, fixtures struct{}) (*sqlx.Tx, *sqlx.Tx) {
    tx := // create db transaction here
    return tx, tx
}

func (s TransactionFixture) Destruct(t *testing.T, tx *sqlx.Tx) {
    tx.Rollback()
}

func init() {
    // register and make fixture available to all tests
    gtest.MustRegisterFixture(
        "Transaction", &TransactionFixture{}, gtest.ScopeSubTest)
}

// begin of test definition
type SampleTests struct{}

func (s *SampleTests) Setup(t *testing.T)      {
    // you can create/initialize DB in this method
    // DB instance can also be implemented as a fixture and get injected into Transanction fixture.
}
func (s *SampleTests) Teardown(t *testing.T)   {
    // you can clean up all DB resources in this method
}
func (s *SampleTests) BeforeEach(t *testing.T) {}
func (s *SampleTests) AfterEach(t *testing.T)  {}

func (s *SampleTests) SubTestFoo(t *testing.T, fixtures struct {
    Tx sqlx.Tx `fixture:"Transaction"`
}) {
    // transaction is available as fixtures.Tx in this test
}

func TestSampleTests(t *testing.T) {
    gtest.RunSubTests(t, &SampleTests{})
}

See https://godoc.org/github.com/houqp/gtest and https://github.com/houqp/gtest/blob/master/example_test.go for more advanced and uptodate examples.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM