Search code examples
unit-testinggo

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.


Solution

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