How to Unit Test and Mock a Function in Go
Whenever you deliver your code, you must deliver it with quality.
Introduction
During my professional experience, I usually written my code in Java. I have to admit Java ecosystems are mature enough. They provided lots of frameworks or libraries which are very useful and stable. One of the best frameworks which are very practical is their unit test framework. Assuming that you are a Java programmer, you have must heard JUnit or TestNG as a unit test tools.
Mocking
As you may notice, JUnit or TestNG itself sometimes are not enough to cover your test. Because one of the key aspect of unit testing is controlling your test environment. Let us assume that we need external dependency or third party APIs. We cannot rely on those APIs in order to make our test runs as expected. Because sometimes the latency is high or the service maybe not available. Next thing to happen is your test will fail and break the deployment.
One of the techniques to oversight this case is, by mocking the object or functionalities. That is why library such as Mockito are very handy to manage mocking objects. You can expect the behavior of your object as a simulation during the runtime.
Mocking in Go
Go itself already shipped with testing module. So we do not need to import another library just to execute the test. However, in my opinion, it does not suffice. It is good to execute standard testing. On the other hand, whenever you need to mock your functions/methods, you need an additional package.
I was searching several mocking package for Go, and there are two packages that I found interesting:
- HttpMock. This library is useful to simulate HTTP request to third party libraries.
- Testify. Useful for mocking functions/methods and provide
assert
functionality. The assert function is effective because we do not need to typet.Error()
Use Case
I want to see latest currency exchange rate on my application. I need exchange rate from six currencies, which are USD, CAD, AUD, IDR, NZD and SGD. Here is the requirement in the user story formula:
Story
As a user I want to see latest currency rates in order to exchange my current currency.
Acceptance Criteria
Given input USD then user will see rates from CAD, AUD, SGD, NZD and IDR
Step by Step
First of all, what I need is to get third party API for currency exchange. For this case, I choose fixer.io because it is free.
Notes: I will try to do this in TDD fashion.
Step 1: Define Request and Response
As usual, create structs to handle request and response.
1 2 3 4 5 6 7 8 9 10 11 |
// filename: xrates.go type RatesQuotedRequest struct { Base string `json:"base"` } type RatesQuotedResponse struct { Base string `json:"base"` Date string `json:"date"` Rates map[string] float32 `json:"rates"` } |
Step 2: Define an Interface
Create an interface to get rates and another struct to implement the interface.
1 2 3 4 5 6 7 8 9 10 11 |
// filename: xrates.go // interface type AccessRates interface { getRates() (RatesQuotedResponse,error) } // struct to implement the interface type Rates struct { Request RatesQuotedRequest } |
Step 3: Create a Unit Test for getRates Function
For this unit testing, I will use HttpMock functionality to simulate API request. Also there are three scenario to be aware of.
- Making request return 200 OK
- Making request return empty body
- Making request but invalid JSON.
Of course you can add another scenario. It depends on your requirements.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
// filename: xrates_test.go // Private Function // HTTP Mock func TestGetRatesReturn200(t *testing.T) { assert := assert.New(t) //activate http mock httpmock.Activate() defer httpmock.DeactivateAndReset() //simulate response responder := httpmock.NewStringResponder(200, `{ "base": "USD", "date": "2016-06-23", "rates": { "AUD": 1.3214, "CAD": 1.3231, "IDR": 13305.0, "NZD": 1.3734, "SGD": 1.3869 } }`) // create a request rates := Rates{ Request: RatesQuotedRequest{Base: "USD"}, } url := fmt.Sprintf("http://api.fixer.io/latest?base=%s", rates.Request.Base) httpmock.RegisterResponder("GET", url, responder) // get expected response resp, _ := rates.getRates() assert.Equal("USD", resp.Base, "Base Currency") assert.Equal("2016-06-23", resp.Date, "Exchange Date") assert.Equal(float32(1.3214), resp.Rates["AUD"], "Australian X-Rate") assert.Equal(float32(13305.0), resp.Rates["IDR"], "Indonesian X-Rate") } func TestGetRatesErrorJSONFormat(t *testing.T) { //implement here } func TestGetRatesEmptyBody(t *testing.T) { //implement here } |
When you try to run first time, it will return error because we have done nothing in getRates function. So next step is to implement that function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
// filename: xrates.go func (r Rates) getRates() (RatesQuotedResponse, error) { // Access the API url := fmt.Sprintf("http://api.fixer.io/latest?base=%s", r.Request.Base) // Build the request req, err := http.NewRequest("GET", url, nil) if err != nil { log.Fatal("NewRequest: ", err) return RatesQuotedResponse{},err } // Create an HTTP Client client := &http.Client{} // Send the request via a client resp, err := client.Do(req) if err != nil { log.Println(err) return RatesQuotedResponse{},err } // Defer the closing of the body defer resp.Body.Close() var rqResp RatesQuotedResponse; // Use json.Decode for reading streams of JSON data if err := json.NewDecoder(resp.Body).Decode(&rqResp); err != nil { log.Println(err) return RatesQuotedResponse{},err } return rqResp, nil } |
Now your test should be green or no error.
Step 4: Create Public Function to Get Current Rates
In this step, first we target the test file first. Three important points here, which are creates the mock struct and named it ratesMock
. Then ratesMock
implements functions in AccessRate
interface. Then simulate the behavior by using mock.On(..).Return(..)
functionalities.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
// Filename: xrates_test.go // Public Function // Create a Mock Struct type ratesMock struct { mock.Mock } // Implement AccessRates interface with Mock Object func (o *ratesMock) getRates() (RatesQuotedResponse, error) { args := o.Called() return args.Get(0).(RatesQuotedResponse), args.Error(1) } // Testing the function func TestFunctionGetCurrentRates(t *testing.T) { assert := assert.New(t) ex_rates := map[string]float32{ "AUD": 1.0000, "NZD": 1.0000, "IDR": 13000.0000, "CAD": 1.0000, "SGD": 1.0000, } ar := RatesQuotedResponse{ Base: "USD", Rates: ex_rates, } // mock the behavior of getRates function rmock := new(ratesMock) rmock.On("getRates").Return(ar, nil) resp, err := GetCurrentRates(rmock) assert.Nil(err, "no error") assert.Equal("USD", resp.Base, "The currency should be USD") assert.Equal(5, len(resp.Rates), "Total exchange currency") rmock.AssertExpectations(t) } |
Next implement the functions.
1 2 3 4 5 |
// filename: xrates.go func GetCurrentRates(rates AccessRates) (RatesQuotedResponse, error) { return rates.getRates() } |
Step 5: Run it
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// test it cd $GOPATH/src/github.com/ru-rocker/go-testing go test -v xrates/*.go // output === RUN TestGetRatesReturn200 --- PASS: TestGetRatesReturn200 (0.00s) === RUN TestGetRatesErrorJSONFormat 2017/07/10 15:00:54 invalid character '}' looking for beginning of object key string --- PASS: TestGetRatesErrorJSONFormat (0.00s) === RUN TestGetRatesEmptyBody 2017/07/10 15:00:54 EOF --- PASS: TestGetRatesEmptyBody (0.00s) === RUN TestFunctionGetCurrentRates --- PASS: TestFunctionGetCurrentRates (0.00s) mock.go:387: ✅ getRates() PASS ok command-line-arguments 0.071s |
If you are using Gogland IDE, it offers some cool features like coverage test. So here is the sample result:
Advance: Behavior Driven Development (BDD)
Another testing technique is Behavior Driven Development (BDD). To support this technique there are third party library for Go. One of the library is Gucumber. This library inspired by Cucumber, which widely known in Java and Ruby ecosystem.
I will slightly highlight the BDD here. If you want to know about the details, you can visit their website.
We will create a feature file first based on our user story.
1 2 3 4 5 6 7 8 |
// location: internal/features/xrates.feature @xrates Feature: Exchange Currency Scenario: User selects exchange rate Given I select base rates "USD" And retrieve rates from REST endpoint Then retrieve five rates "NZD", "AUD", "SGD", "CAD" and "IDR" |
Then create a step definition for this feature.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
package features // step definition func init() { var rates xrates.Rates var resp xrates.RatesQuotedResponse Before("@xrates", func() { // runs before every feature or scenario tagged with @xrates rates = xrates.Rates{} }) Given(`^I select base rates "(.+?)"$`, func(base string) { req := xrates.RatesQuotedRequest{Base: base} rates.Request = req }) // mock the request And(`^retrieve rates from REST endpoint$`, func() { httpmock.Activate() defer httpmock.DeactivateAndReset() responder := httpmock.NewStringResponder(200, `{ "base": "USD", "date": "2016-06-23", "rates": { "AUD": 1.3214, "CAD": 1.3231, "IDR": 13305.0, "NZD": 1.3734, "SGD": 1.3869 } }`) url := fmt.Sprintf("http://api.fixer.io/latest?base=%s", rates.Request.Base) httpmock.RegisterResponder("GET", url, responder) resp, _ = xrates.GetCurrentRates(rates) }) Then(`^retrieve five rates "(.+?)", "(.+?)", "(.+?)", "(.+?)" and "(.+?)"`, func(nzd, aud, sgd, cad, idr string) { rates := resp.Rates assert.NotEmpty(T, rates[nzd], fmt.Sprintf("%s currency is %f", nzd, rates[nzd])) assert.NotEmpty(T, rates[aud], fmt.Sprintf("%s currency is %f", aud, rates[aud])) assert.NotEmpty(T, rates[sgd], fmt.Sprintf("%s currency is %f", sgd, rates[sgd])) assert.NotEmpty(T, rates[cad], fmt.Sprintf("%s currency is %f", cad, rates[cad])) assert.NotEmpty(T, rates[idr], fmt.Sprintf("%s currency is %f", idr, rates[idr])) }) } |
Then execute it by typing
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// test it! cd $GOPATH/src/github.com/ru-rocker/go-testing gucumber // output @xrates Feature: Exchange Currency Scenario: User selects exchange rate # internal/features/xrates.feature:3 Given I select base rates "USD" # internal/features/xrates.feature:4 And retrieve rates from REST endpoint # internal/features/xrates.feature:5 Then retrieve five rates "NZD", "AUD", "SGD", "CAD" and "IDR" # internal/features/xrates.feature:9 Finished (3 passed, 0 failed, 0 skipped). |
Conclusion
Go has already shipped with testing modules. By using its modules, we can create unit tests based on standard scenario. However, whenever we want to leverage our code coverage, it would become complicated because it does not provide the mocking object. That is why we need additional libraries such as httpmock and testify in order to make our testing environment controllable.
Maybe one side note, it would be nicer if testify provides a function which are similar to doThrow() in Mockito. Because at present, I found it is hard to simulate the runtime error.
That’s all from me. You can see whole sample on my repository. Thank you and cheerio!
Hi Ru
Just wanted to thank you for the golang unit test and mocking article. I also come from a java/mockito background but my current job requires me to use golang and my biggest struggle has been unit testing because i just dont understand how it works in golang. There are more unit test tutorials online, but they rarely touch on mocking and more advanced functions than Add or Substract. I think i’ve got a better grip thanks to your article.
Wondering if you have more sources/books etc to get more info on mocking in golang??
Thanks again.