abdel
abdel

Reputation: 685

Unit testing in golang

I'm currently looking into creating some unit tests for my service in Go, as well as other functions that build up on top of that functionality, and I'm wondering what is the best way to unit test that in Go? My code looks like:

type BBPeripheral struct {
    client   *http.Client
    endpoint string
}

type BBQuery struct {
    Name string `json:"name"`
}

type BBResponse struct {
    Brand          string `json:"brand"`
    Model          string `json:"model"`
    ...
}

type Peripheral struct {
    Brand          string 
    Model          string 
    ...
}

type Service interface {
    Get(name string) (*Peripheral, error)
}

func NewBBPeripheral(config *peripheralConfig) (*BBPeripheral, error) {
    transport, err := setTransport(config)
    if err != nil {
        return nil, err
    }

    BB := &BBPeripheral{
        client:   &http.Client{Transport: transport},
        endpoint: config.Endpoint[0],
    }

    return BB, nil
}


func (this *BBPeripheral) Get(name string) (*Peripheral, error) {

    data, err := json.Marshal(BBQuery{Name: name})
    if err != nil {
        return nil, fmt.Errorf("BBPeripheral.Get Marshal: %s", err)
    }

    resp, err := this.client.Post(this.endpoint, "application/json", bytes.NewBuffer(data))
    if resp != nil {
        defer resp.Body.Close()
    }
    if err != nil {
        return nil, err
    }
    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf(resp.StatusCode)
    }

    var BBResponse BBResponse

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }

    err = json.Unmarshal(body, &BBResponse)
    if err != nil {
        return nil, err
    }

    peripheral := &Peripheral{}

    peripheral.Model = BBResponse.Model
    if peripheral.Model == "" {
        peripheral.Model = NA
    }

    peripheral.Brand = BBResponse.Brand
    if peripheral.Brand == "" {
        peripheral.Brand = NA
    }

    return peripheral, nil
}

Is the most efficient way of testing this code and the code that uses these functions to spin up a separate goroutine to act like the server, use http.httptest package, or something else? that's the first time that i try to write a test then i don't realy know how.

Upvotes: 2

Views: 2410

Answers (2)

VonC
VonC

Reputation: 1329662

Go makes it so easy to write concurrent code, and makes it just as easy to test it.

Not quite. There is a Q3 2024 proposal "testing/synctest: new package for testing concurrent code" for that, mentioned in 2024-08-27 Go compiler and runtime meeting notes.

It is on hold pending accepted now that the second proposal detailed in the next section has been completed.

This package has two main features:

  • It permits using a fake clock to test code which uses timers. The test can control the passage of time as observed by the code under test.
  • It permits a test to wait until an asynchronous operation has completed.

We can inject a fake clock to give us control over time in tests. When advancing the fake clock, we will need some mechanism to ensure that any timers that fire have executed before progressing the test. These changes come at the expense of additional code complexity: We can no longer use time.Timer, but must use a testable wrapper. Background goroutines need additional synchronization points.

Example:

func TestCacheEntryExpires(t *testing.T) {
        synctest.Run(func() {
                count := 0
                        c := NewCache(2 * time.Second, func(key string) int {
                        count++
                        return fmt.Sprintf("%v:%v", key, count)
                })

                // Get an entry from the cache.
                if got, want := c.Get("k"), "k:1"; got != want {
                        t.Errorf("c.Get(k) = %q, want %q", got, want)
                }

                // Verify that we get the same entry when accessing it before the expiry.
                time.Sleep(1 * time.Second)
                synctest.Wait()
                if got, want := c.Get("k"), "k:1"; got != want {
                        t.Errorf("c.Get(k) = %q, want %q", got, want)
                }

                // Wait for the entry to expire and verify that we now get a new one.
                time.Sleep(3 * time.Second)
                synctest.Wait()
                if got, want := c.Get("k"), "k:2"; got != want {
                        t.Errorf("c.Get(k) = %q, want %q", got, want)
                }
        })
}

This is identical to the naive test above, wrapped in synctest.Run and with the addition of two calls to synctest.Wait.
However:

  1. This test is not slow. The time.Sleep calls use a fake clock, and execute immediately.
  2. This test is not flaky. The synctest.Wait ensures that all background goroutines have idled or exited before the test proceeds.
  3. This test requires no additional instrumentation of the code under test. It can use standard time package timers, and it does not need to provide any mechanism for tests to synchronize with it.

Proposal #69687 "testing/synctest: experimental package for testing concurrent code" is now (Nov. 2024) likely accept, with the following package API, completed with commit 76e4efd:

// Package synctest provides support for testing concurrent code.
package synctest

// Run executes f in a new goroutine.
//
// The new goroutine and any goroutines transitively started by it form
// an isolated "bubble".
// Run waits for all goroutines in the bubble to exit before returning.
//
// Goroutines in the bubble use a fake time implementation.
// The initial time is midnight UTC 2000-01-01.
//
// A goroutine in the bubble is idle if it is blocked on:
//   - a send or receive on a channel created within the bubble
//   - a select statement where every case is a channel within the bubble
//   - sync.Cond.Wait
//   - time.Sleep
//
// The above cases are the only times a goroutine is idle.
// In particular, a goroutine is NOT idle when blocked on:
//   - system calls
//   - cgo calls
//   - I/O, such as reading from a network connection with no data
//   - sync.Mutex.Lock or sync.Mutex.RLock
//
// Time advances when every goroutine in the bubble is idle.
// For example, a call to time.Sleep will block until all goroutines
// are idle, and return after the bubble's clock has advanced
// by the sleep duration.
//
// If every goroutine is idle and there are no timers scheduled,
// Run panics.
//
// Channels, time.Timers, and time.Tickers created within the bubble
// are associated with it. Operating on a bubbled channel, timer, or ticker
// from outside the bubble panics.
func Run(f func())

// Wait blocks until every goroutine within the current bubble,
// other than the current goroutine, is idle.
func Wait()

Upvotes: 0

dm03514
dm03514

Reputation: 55972

It really completely depends. Go provides pretty much all the tools you need to test your application at every single level.

Unit Tests

Design is important because there aren't many tricks to dynamically provide mock/stub objects. You can override variables for tests, but it unlocks all sorts of problems with cleanup. I would focus on IO free unit tests to check that your specific logic works.

For example, you could test BBPeripheral.Get method by making client an interface, requiring it during instantiation, and providing a stub one for the test.

func Test_BBPeripheral_Get_Success(*testing.T) {
  bb := BBPeripheral{client: &StubSuccessClient, ...}
  p, err := bb.Get(...) 
  if err != nil {
     t.Fail()
  }
}

Then you could create a stub error client that exercises error handling in the Get method:

func Test_BBPeripheral_Get_Success(*testing.T) {
  bb := BBPeripheral{client: &StubErrClient, ...}
  _, err := bb.Get(...) 
  if err == nil {
     t.Fail()
  }
}

Component/Integration Tests

These tests can help exercise that each individual unit in your package can work together in unison. Since your code talks over http, Go provides the httptest package that could be used.

To do this the test could create an httptest server with a handler registered to provide the response that this.endpoint expects. You could then exercise your code using its public interface by requesting a NewBBPeripheral, passing in this.endpoint corresponding to the Server.URL property.

This allows you to simulate your code talking to a real server.

Go Routine Tests

Go makes it so easy to write concurrent code, and makes it just as easy to test it. Testing the top level code that spawns a go routine that exercises NewBBPeripheral could look very much like the test above. In addition to starting up a test server your test will have to wait your your asynchronous code to complete. If you don't have a service wide way to cancel/shutdown/signal complete then one may be required to test it using go routines.

RaceCondition/Load Testing

Using go's built in bechmark test combined with -race flag, you can easily exercise your code, and profile it for race conditions, leveraging the tests you wrote above.


One thing to keep in mind, if the implementation of your application is still in flux, writing unit tests may cost a large amount of time. Creating a couple tests, which exercise the public interface of your code, should allow you to easily verify that your application is working, while allowing the implementation to change.

Upvotes: 2

Related Questions