Reputation: 685
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
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 tosynctest.Wait
.
However:
- This test is not slow. The time.Sleep calls use a fake clock, and execute immediately.
- This test is not flaky. The
synctest.Wait
ensures that all background goroutines have idled or exited before the test proceeds.- 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
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