Download as pdf or txt
Download as pdf or txt
You are on page 1of 25

Utilizing the Power of Interfaces when Mocking and Testing External APIs in Golang.

md 1/17/2023

Utilizing the Power of Interfaces when Mocking and


Testing External APIs in Golang

It is not good practice to call an actual external API endpoint in your test files, especially in unit tests.

These are some of the reasons:

The API server may be down.


The API can have a rate limit (you might need to hit it several times in your test)

It is best we mock the external API when testing.

We are going to be building a microservice that calls the darksky api.

Darksky is a weather API. We will need to input the location in latitude and longitude to get the weather
details based on the above parameters.

You can signup to Darksky API to get a free API key that you will use to get weather data.

Lets Begin
This is an idea of what we wish to achieve in this article:

Image from Federico’s course

Step 1: Basic Setup


a. Create a root directory: In your terminal, create a directory called interface-testing

mkdir interface-testing

b. Initialize go modules: For dependency management, we need a file that will hold all dependencies we will
use.

Inside our interface-testing directory, initialize go modules:

go mod init interface-testing

We gave the model name as the name of our root directory. You can call it anything you wish.

c. Create the api directory in the root project directory:

1 / 25
Utilizing the Power of Interfaces when Mocking and Testing External APIs in Golang.md 1/17/2023

mkdir api

Also, create the main.go file in the root project directory:

touch main.go

Your initial setup should look like this:

interface-testing
├── api
├── main.go
└── go.mod

Step 2: The Client


We will need a client that can enable us to talk to the API. We will be defining a function that performs a GET
request to the API.

Inside the **api** directory, create the clients directory:

cd api && mkdir clients

Then create the **restclient** directory inside the **clients** directory:

cd clients && mkdir restclient

Create the **restclient.go** file inside the **restclient** directory:

cd restclient && touch restclient.go

The content of the file:

package restclient

import (
"net/http"
)

type clientStruct struct{}

2 / 25
Utilizing the Power of Interfaces when Mocking and Testing External APIs in Golang.md 1/17/2023

type ClientInterface interface {


Get(string) (*http.Response, error)
}
var (
ClientStruct ClientInterface = &clientStruct{}
)

func (ci *clientStruct) Get(url string) (*http.Response, error) {


request, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
client := http.Client{}

return client.Do(request)
}

From the above file, The “clientStruct” is implementing the ClientInterface. Hence any method defined in the
interface, the struct must also define that method. Interfaces are simply awesome in testing; because they
help us create fake methods.

Step 3: Domains
The scope of this app doesn’t require a database. But we will need to define a schema that will resemble the
API request and response data that we will be sending and receiving.

In the **api** directory, create the **domain** directory:

mkdir domain

Since we are building a weather API, we will create the **weather_domain** directory:

cd domain && mkdir weather_domain

a. The Weather Schema

To get the weather information about a place, we will need to provide:

Darksky authentication key


Latitude of the place
Longitude of the place

We will provide these details in the GET request, we will receive a response from the API with weather result.

In the **weather_domain** directory, create the **weather_domain.go** file.

3 / 25
Utilizing the Power of Interfaces when Mocking and Testing External APIs in Golang.md 1/17/2023

cd weather_domain && touch weather_domain.go

package weather_domain

type Weather struct {


Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
TimeZone string `json:"timezone"`
Currently CurrentlyInfo `json:"currently"`
}

type CurrentlyInfo struct {


Temperature float64 `json:"temperature"`
Summary string `json:"summary"`
DewPoint float64 `json:"dewPoint"`
Pressure float64 `json:"pressure"`
Humidity float64 `json:"humidity"`
}

type WeatherRequest struct {


ApiKey string `json:"api_key"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
}

b. Error Schema

When the wrong details are inputted, the API will throw an error. We will define an error interface and wire up
some methods we will use throughout the app.

In the **weather_domain** directory, create the **weather_error.go** file

touch weather_error.go

package weather_domain

import (
"encoding/json"
"net/http"
)

type WeatherErrorInterface interface {


Status() int
Message() string
}
type WeatherError struct {

4 / 25
Utilizing the Power of Interfaces when Mocking and Testing External APIs in Golang.md 1/17/2023

Code int `json:"code"`


ErrorMessage string `json:"error"`
}

func (w *WeatherError) Status() int {


return w.Code
}
func (w *WeatherError) Message() string {
return w.ErrorMessage
}

func NewWeatherError(statusCode int, message string) WeatherErrorInterface {


return &WeatherError{
Code: statusCode,
ErrorMessage: message,
}
}
func NewBadRequestError(message string) WeatherErrorInterface {
return &WeatherError{
Code: http.StatusBadRequest,
ErrorMessage: message,
}
}

func NewForbiddenError(message string) WeatherErrorInterface {


return &WeatherError{
Code: http.StatusForbidden,
ErrorMessage: message,
}
}

func NewApiErrFromBytes(body []byte) (WeatherErrorInterface, error) {


var result WeatherError
if err := json.Unmarshal(body, &result); err != nil {
return nil, err
}
return &result, nil
}

c. Test the Schemas

We can test the schemas we have defined above. Still, in the **weather_domain** directory, create
**weather_domain_test.go** file.

touch weather_domain_test.go

package weather_domain

import (

5 / 25
Utilizing the Power of Interfaces when Mocking and Testing External APIs in Golang.md 1/17/2023

"encoding/json"
"github.com/stretchr/testify/assert"
"testing"
)

func TestWeather(t *testing.T) {


request := Weather{
Latitude: 12.33,
Longitude: 90.34,
TimeZone: "America/New_York",
Currently: CurrentlyInfo{
Temperature: 10,
Summary: "Clear",
DewPoint: 20.433,
Pressure: 95.33,
Humidity: 71.34,
},
}
bytes, err := json.Marshal(request)
assert.Nil(t, err)
assert.NotNil(t, bytes)

var result Weather


err = json.Unmarshal(bytes, &result)

assert.Nil(t, err)
assert.EqualValues(t, result.Latitude, request.Latitude)
assert.EqualValues(t, result.TimeZone, request.TimeZone)
assert.EqualValues(t, result.Longitude, request.Longitude)
assert.EqualValues(t, result.Currently.Summary, request.Currently.Summary)
assert.EqualValues(t, result.Currently.Humidity, request.Currently.Humidity)
assert.EqualValues(t, result.Currently.DewPoint, request.Currently.DewPoint)
assert.EqualValues(t, result.Currently.Pressure, request.Currently.Pressure)
assert.EqualValues(t, result.Currently.Temperature,
request.Currently.Temperature)
}

func TestWeatherError(t *testing.T) {


request := WeatherError{
Code: 400,
ErrorMessage: "Bad Request Error",
}
bytes, err := json.Marshal(request)
assert.Nil(t, err)
assert.NotNil(t, bytes)

var errResult WeatherError


err = json.Unmarshal(bytes, &errResult)
assert.Nil(t, err)
assert.EqualValues(t, errResult.Code, request.Code)
assert.EqualValues(t, errResult.ErrorMessage, request.ErrorMessage)

6 / 25
Utilizing the Power of Interfaces when Mocking and Testing External APIs in Golang.md 1/17/2023

You can run the test using:

go test -v

The **v** flag is for verbose output.

You can run a specific test using:

go test -v --run TestWeather

Step 4: Providers
The Get function defined in the clients, need to be called in the providers.

From the API directory, create the **providers** directory

mkdir providers

Then, create the **weather_providers** directory, inside the **providers** directory.

cd providers && mkdir weather_providers

a. Weather Provider file:

Inside the **weather_provider** directory, create the **weather_provider.go** file

touch weather_provider.go

package weather_provider

import (
"encoding/json"
"fmt"
"interface-testing/api/clients/restclient"
"interface-testing/api/domain/weather_domain"
"io/ioutil"
"log"
"net/http"
)

const (
7 / 25
Utilizing the Power of Interfaces when Mocking and Testing External APIs in Golang.md 1/17/2023

weatherUrl = "https://api.darksky.net/forecast/%s/%v,%v"
)
type weatherProvider struct {}

type weatherServiceInterface interface {


GetWeather(request weather_domain.WeatherRequest) (*weather_domain.Weather,
*weather_domain.WeatherError)
}
var (
WeatherProvider weatherServiceInterface = &weatherProvider{}
)

func (p *weatherProvider) GetWeather(request weather_domain.WeatherRequest)


(*weather_domain.Weather, *weather_domain.WeatherError) {
url := fmt.Sprintf(weatherUrl, request.ApiKey, request.Latitude,
request.Longitude)
response, err := restclient.ClientStruct.Get(url)
if err != nil {
log.Println(fmt.Sprintf("error when trying to get weather from dark sky
api %s", err.Error()))
return nil, &weather_domain.WeatherError{
Code: http.StatusBadRequest,
ErrorMessage: err.Error(),
}
}
bytes, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, &weather_domain.WeatherError{
Code: http.StatusBadRequest,
ErrorMessage: err.Error(),
}
}
defer response.Body.Close()

//The api owner can decide to change datatypes, etc. When this happen, it
might affect the error format returned
if response.StatusCode > 299 {
var errResponse weather_domain.WeatherError
if err := json.Unmarshal(bytes, &errResponse); err != nil {
return nil, &weather_domain.WeatherError{
Code: http.StatusInternalServerError,
ErrorMessage: "invalid json response body",
}
}
errResponse.Code = response.StatusCode
return nil, &errResponse
}
var result weather_domain.Weather
if err := json.Unmarshal(bytes, &result); err != nil {
log.Println(fmt.Sprintf("error when trying to unmarshal weather
successful response: %s", err.Error()))
return nil, &weather_domain.WeatherError{Code:
http.StatusInternalServerError, ErrorMessage: "error unmarshaling weather fetch
response"}
8 / 25
Utilizing the Power of Interfaces when Mocking and Testing External APIs in Golang.md 1/17/2023

}
return &result, nil
}

Observe that the Get function from the rest client is called.

b. Weather Provider Test Cases

We will test what happens when we call the Get function from the restclient, with both good and bad data.

In the **weather_provider** directory, create the **weather_provider_test.go** file.

touch weather_provider_test.go

package weather_provider

import (
"interface-testing/api/clients/restclient"
"interface-testing/api/domain/weather_domain"
"io/ioutil"
"net/http"
"os"
"strings"
"testing"

"github.com/stretchr/testify/assert"
)

var (
getRequestFunc func(url string) (*http.Response, error)
)

type getClientMock struct{}

//We are mocking the client method "Get"


func (cm *getClientMock) Get(request string) (*http.Response, error) {
return getRequestFunc(request)
}

//When the everything is good


func TestGetWeatherNoError(t *testing.T) {
// The error we will get is from the "response" so we make the second
parameter of the function is nil
getRequestFunc = func(url string) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(strings.NewReader(`{"latitude": 44.3601,
"longitude": -71.0589, "timezone": "America/New_York", "currently": {"summary":
"Clear", "temperature": 40.22, "dewPoint": 50.22, "pressure": 12.90, "humidity":
16.54}}`)),
9 / 25
Utilizing the Power of Interfaces when Mocking and Testing External APIs in Golang.md 1/17/2023

}, nil
}
restclient.ClientStruct = &getClientMock{} //without this line, the real api
is fired

response, err :=
WeatherProvider.GetWeather(weather_domain.WeatherRequest{"anything", 44.3601,
-71.0589})
assert.NotNil(t, response)
assert.Nil(t, err)
assert.EqualValues(t, 44.3601, response.Latitude)
assert.EqualValues(t, -71.0589, response.Longitude)
assert.EqualValues(t, "America/New_York", response.TimeZone)
assert.EqualValues(t, "Clear", response.Currently.Summary)
assert.EqualValues(t, 40.22, response.Currently.Temperature)
assert.EqualValues(t, 50.22, response.Currently.DewPoint)
assert.EqualValues(t, 16.54, response.Currently.Humidity)
assert.EqualValues(t, 12.90, response.Currently.Pressure)
}

func TestGetWeatherInvalidApiKey(t *testing.T) {


getRequestFunc = func(url string) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusForbidden,
Body: ioutil.NopCloser(strings.NewReader(`{"code": 403, "error":
"permission denied"}`)),
}, nil
}
restclient.ClientStruct = &getClientMock{} //without this line, the real api
is fired

response, err :=
WeatherProvider.GetWeather(weather_domain.WeatherRequest{"wrong_anything",
44.3601, -71.0589})
assert.NotNil(t, err)
assert.Nil(t, response)
assert.EqualValues(t, http.StatusForbidden, err.Code)
assert.EqualValues(t, "permission denied", err.ErrorMessage)
}

func TestGetWeatherInvalidLatitude(t *testing.T) {


getRequestFunc = func(url string) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusBadRequest,
Body: ioutil.NopCloser(strings.NewReader(`{"code": 400, "error":
"The given location is invalid"}`)),
}, nil
}
restclient.ClientStruct = &getClientMock{} //without this line, the real api
is fired

response, err :=
WeatherProvider.GetWeather(weather_domain.WeatherRequest{"anything", 34223.3445,
-71.0589})
10 / 25
Utilizing the Power of Interfaces when Mocking and Testing External APIs in Golang.md 1/17/2023

assert.NotNil(t, err)
assert.Nil(t, response)
assert.EqualValues(t, http.StatusBadRequest, err.Code)
assert.EqualValues(t, "The given location is invalid", err.ErrorMessage)
}

func TestGetWeatherInvalidLongitude(t *testing.T) {


getRequestFunc = func(url string) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusBadRequest,
Body: ioutil.NopCloser(strings.NewReader(`{"code": 400, "error":
"The given location is invalid"}`)),
}, nil
}
restclient.ClientStruct = &getClientMock{} //without this line, the real api
is fired

response, err :=
WeatherProvider.GetWeather(weather_domain.WeatherRequest{"anything", 44.3601,
-74331.0589})

assert.NotNil(t, err)
assert.Nil(t, response)
assert.EqualValues(t, http.StatusBadRequest, err.Code)
assert.EqualValues(t, "The given location is invalid", err.ErrorMessage)
}

func TestGetWeatherInvalidFormat(t *testing.T) {


getRequestFunc = func(url string) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusBadRequest,
Body: ioutil.NopCloser(strings.NewReader(`{"code": 400, "error":
"Poorly formatted request"}`)),
}, nil
}
restclient.ClientStruct = &getClientMock{} //without this line, the real api
is fired

response, err :=
WeatherProvider.GetWeather(weather_domain.WeatherRequest{"anything", 0,
-74331.0589})

assert.NotNil(t, err)
assert.Nil(t, response)
assert.EqualValues(t, http.StatusBadRequest, err.Code)
assert.EqualValues(t, "Poorly formatted request", err.ErrorMessage)
}

//When no body is provided


func TestGetWeatherInvalidRestClient(t *testing.T) {
getRequestFunc = func(url string) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusBadRequest,
11 / 25
Utilizing the Power of Interfaces when Mocking and Testing External APIs in Golang.md 1/17/2023

Body: ioutil.NopCloser(strings.NewReader(`{"code": 400, "error":


"invalid rest client response"}`)),
}, nil
}
restclient.ClientStruct = &getClientMock{} //without this line, the real api
is fired

response, err :=
WeatherProvider.GetWeather(weather_domain.WeatherRequest{"anything", 0,
-74331.0589})
assert.NotNil(t, err)
assert.Nil(t, response)
assert.EqualValues(t, http.StatusBadRequest, err.Code)
assert.EqualValues(t, "invalid rest client response", err.ErrorMessage)
}

func TestGetWeatherInvalidResponseBody(t *testing.T) {


getRequestFunc = func(url string) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusBadRequest,
Body: ioutil.NopCloser(strings.NewReader(`{"code": 400, "error":
"Invalid response body"}`)),
}, nil
}
restclient.ClientStruct = &getClientMock{} //without this line, the real api
is fired

response, err :=
WeatherProvider.GetWeather(weather_domain.WeatherRequest{"wrong_anything",
44.3601, -71.0589})
assert.NotNil(t, err)
assert.Nil(t, response)
assert.EqualValues(t, http.StatusBadRequest, err.Code)
assert.EqualValues(t, "Invalid response body", err.ErrorMessage)
}

func TestGetWeatherInvalidRequest(t *testing.T) {


getRequestFunc = func(url string) (*http.Response, error) {
invalidCloser, _ := os.Open("-asf3")
return &http.Response{
StatusCode: http.StatusBadRequest,
Body: invalidCloser,
}, nil
}
restclient.ClientStruct = &getClientMock{} //without this line, the real api
is fired

response, err :=
WeatherProvider.GetWeather(weather_domain.WeatherRequest{"wrong_anything",
44.3601, -71.0589})
assert.Nil(t, response)
assert.NotNil(t, err)
assert.EqualValues(t, http.StatusBadRequest, err.Code)
assert.EqualValues(t, "invalid argument", err.ErrorMessage)
12 / 25
Utilizing the Power of Interfaces when Mocking and Testing External APIs in Golang.md 1/17/2023

//When the error response is invalid, here the code is supposed to be an integer,
but a string was given.
//This can happen when the api owner changes some data types in the api
func TestGetWeatherInvalidErrorInterface(t *testing.T) {
getRequestFunc = func(url string) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusBadRequest,
Body: ioutil.NopCloser(strings.NewReader(`{"code": "string
code"}`)),
}, nil
}
restclient.ClientStruct = &getClientMock{} //without this line, the real api
is fired

response, err :=
WeatherProvider.GetWeather(weather_domain.WeatherRequest{"anything", 44.3601,
-71.0589})
assert.Nil(t, response)
assert.NotNil(t, err)
assert.EqualValues(t, http.StatusInternalServerError, err.Code)
assert.EqualValues(t, "invalid json response body", err.ErrorMessage)
}

//We are getting a postive response from the api, but, the datatype of the
response returned does not match the struct datatype we have defined (does not
match the struct type we want to unmarshal this response into).
func TestGetWeatherInvalidResponseInterface(t *testing.T) {
getRequestFunc = func(url string) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(strings.NewReader(`{"latitude": "string
latitude", "longitude": -71.0589, "timezone": "America/New_York"}`)), //when we
use string for latitude instead of float
}, nil
}
restclient.ClientStruct = &getClientMock{} //without this line, the real api
is fired

response, err :=
WeatherProvider.GetWeather(weather_domain.WeatherRequest{"anything", 44.3601,
-71.0589})
assert.Nil(t, response)
assert.NotNil(t, err)
assert.EqualValues(t, http.StatusInternalServerError, err.Code)
assert.EqualValues(t, "error unmarshaling weather fetch response",
err.ErrorMessage)
}

From the above test file, you could see how using interface saved us from hitting the actual API. We simulate
the request and response. The good thing about these tests is, you can visit the actual URLs provider in each

13 / 25
Utilizing the Power of Interfaces when Mocking and Testing External APIs in Golang.md 1/17/2023

test, but this time, the real API key from Darksky and still get the same result as the response we provided in
our mock. Explanations are provided for test cases that need them in the above file.

Step 5: Services
Let's wire up our services. We will define the **GetWeather** method that will call the **GetWeather**
function we defined in our providers.

From the API directory, create the **services** directory

mkdir services

a. The Weather Service file:

Then create the **weather_service.go** file inside the **services** directory:

cd services && touch weather_service.go

package services

import (
"interface-testing/api/domain/weather_domain"
"interface-testing/api/providers/weather_provider"
)

type weatherService struct {}

type weatherServiceInterface interface {


GetWeather(input weather_domain.WeatherRequest) (*weather_domain.Weather,
weather_domain.WeatherErrorInterface)
}
var (
WeatherService weatherServiceInterface = &weatherService{}
)

func (w *weatherService) GetWeather(input weather_domain.WeatherRequest)


(*weather_domain.Weather, weather_domain.WeatherErrorInterface){
request := weather_domain.WeatherRequest{
ApiKey: input.ApiKey,
Latitude: input.Latitude,
Longitude: input.Longitude,
}
response, err := weather_provider.WeatherProvider.GetWeather(request)
if err != nil {
return nil, weather_domain.NewWeatherError(err.Code, err.ErrorMessage)
}
result := weather_domain.Weather{
Latitude: response.Latitude,
14 / 25
Utilizing the Power of Interfaces when Mocking and Testing External APIs in Golang.md 1/17/2023

Longitude: response.Longitude,
TimeZone: response.TimeZone,
Currently: weather_domain.CurrentlyInfo{
Temperature: response.Currently.Temperature,
Summary: response.Currently.Summary,
DewPoint: response.Currently.DewPoint,
Pressure: response.Currently.Pressure,
Humidity: response.Currently.Humidity,
},
}
return &result, nil
}

You might need to pay attention to the above file because we defined an interface that we will use will enable
us to create a fake **GetWeather** method when testing.

We made the **weatherService** struct implement the **weatherServiceInterface**. In our test, we


will define a fake struct that implements this same interface. Hence the struct must have the methods that
implement the interface. For example, the **GetWeather** method. This will be useful when testing our
controller file.

b. The Weather Service Test file

We will need to test the GetWeather method. In the **services** directory, create the
**weather_service_test.go** file. We will also use mock data in our tests.

touch weather_service_test.go

package services

import (
"interface-testing/api/domain/weather_domain"
"interface-testing/api/providers/weather_provider"
"net/http"
"testing"

"github.com/stretchr/testify/assert"
)

var (
getWeatherProviderFunc func(request weather_domain.WeatherRequest)
(*weather_domain.Weather, *weather_domain.WeatherError)
)
type getProviderMock struct{}

func (c *getProviderMock) GetWeather(request weather_domain.WeatherRequest)


(*weather_domain.Weather, *weather_domain.WeatherError) {
return getWeatherProviderFunc(request)
}
15 / 25
Utilizing the Power of Interfaces when Mocking and Testing External APIs in Golang.md 1/17/2023

func TestWeatherServiceNoAuthKey(t *testing.T) {


getWeatherProviderFunc = func(request weather_domain.WeatherRequest)
(*weather_domain.Weather, *weather_domain.WeatherError) {
return nil, &weather_domain.WeatherError{
Code: 403,
ErrorMessage: "permission denied",
}
}
weather_provider.WeatherProvider = &getProviderMock{} //without this line, the
real api is fired

request := weather_domain.WeatherRequest{ApiKey: "wrong_key", Latitude:


44.3601, Longitude: -71.0589}
result, err := WeatherService.GetWeather(request)
assert.Nil(t, result)
assert.NotNil(t, err)
assert.EqualValues(t, http.StatusForbidden, err.Status())
assert.EqualValues(t, "permission denied", err.Message())
}

func TestWeatherServiceWrongLatitude(t *testing.T) {


getWeatherProviderFunc = func(request weather_domain.WeatherRequest)
(*weather_domain.Weather, *weather_domain.WeatherError) {
return nil, &weather_domain.WeatherError{
Code: 400,
ErrorMessage: "The given location is invalid",
}
}
weather_provider.WeatherProvider = &getProviderMock{} //without this line, the
real api is fired

request := weather_domain.WeatherRequest{ApiKey: "api_key", Latitude: 123443,


Longitude: -71.0589}
result, err := WeatherService.GetWeather(request)
assert.Nil(t, result)
assert.NotNil(t, err)
assert.EqualValues(t, http.StatusBadRequest, err.Status())
assert.EqualValues(t, "The given location is invalid", err.Message())
}

func TestWeatherServiceWrongLongitude(t *testing.T) {


getWeatherProviderFunc = func(request weather_domain.WeatherRequest)
(*weather_domain.Weather, *weather_domain.WeatherError) {
return nil, &weather_domain.WeatherError{
Code: 400,
ErrorMessage: "The given location is invalid",
}
}
weather_provider.WeatherProvider = &getProviderMock{} //without this line, the
real api is fired

request := weather_domain.WeatherRequest{ApiKey: "api_key", Latitude: 39.12,


Longitude: 122332}
16 / 25
Utilizing the Power of Interfaces when Mocking and Testing External APIs in Golang.md 1/17/2023

result, err := WeatherService.GetWeather(request)


assert.Nil(t, result)
assert.NotNil(t, err)
assert.EqualValues(t, http.StatusBadRequest, err.Status())
assert.EqualValues(t, "The given location is invalid", err.Message())
}

func TestWeatherServiceSuccess(t *testing.T) {


getWeatherProviderFunc = func(request weather_domain.WeatherRequest)
(*weather_domain.Weather, *weather_domain.WeatherError) {
return &weather_domain.Weather{
Latitude: 39.12,
Longitude: 49.12,
TimeZone: "America/New_York",
Currently: weather_domain.CurrentlyInfo{
Temperature: 40.22,
Summary: "Clear",
DewPoint: 50.22,
Pressure: 12.90,
Humidity: 16.54,
},
}, nil
}
weather_provider.WeatherProvider = &getProviderMock{} //without this line, the
real api is fired

request := weather_domain.WeatherRequest{ApiKey: "api_key", Latitude: 39.12,


Longitude: 49.12}
result, err := WeatherService.GetWeather(request)
assert.NotNil(t, result)
assert.Nil(t, err)
assert.EqualValues(t, 39.12, result.Latitude)
assert.EqualValues(t, 49.12, result.Longitude)
assert.EqualValues(t, "America/New_York", result.TimeZone)
assert.EqualValues(t, "Clear", result.Currently.Summary)
assert.EqualValues(t, 40.22, result.Currently.Temperature)
assert.EqualValues(t, 50.22, result.Currently.DewPoint)
assert.EqualValues(t, 12.90, result.Currently.Pressure)
assert.EqualValues(t, 16.54, result.Currently.Humidity)
}

Step 7: Controllers
Create the weather controller that will call the **GetWeather** method we defined in the services.

From the **api** directory, create the **controllers** directory

mkdir controllers

Inside the **controllers** directory, create the **weather_controller** directory.

17 / 25
Utilizing the Power of Interfaces when Mocking and Testing External APIs in Golang.md 1/17/2023

cd controllers && mkdir weather_controller

a. The Weather Controller file

Then create the **weather_controller.go** file.

touch weather_controller.go

package weather_controller

import (
"github.com/gin-gonic/gin"
"interface-testing/api/domain/weather_domain"
"interface-testing/api/services"
"net/http"
"strconv"
)
func GetWeather(c *gin.Context){
long, _ := strconv.ParseFloat(c.Param("longitude"), 64)
lat, _ := strconv.ParseFloat(c.Param("latitude"), 64)
request := weather_domain.WeatherRequest{
ApiKey: c.Param("apiKey"),
Latitude: lat,
Longitude: long,
}
result, apiError := services.WeatherService.GetWeather(request)
if apiError != nil {
c.JSON(apiError.Status(), apiError)
return
}
c.JSON(http.StatusOK, result)
}

Remember, we are getting the api key, latitude, and the longitude from the URL. We used gin for routing
and handling JSON response.

b. The Weather Controller Test file

Remember how we used interface in our services? We did that so as to mock the GetWeather method from
services. From the controller above, we called that method. But we should not call the real method in our
controller tests. Hence, we will define a struct that will implement the **WeatherErrorInterface**. When
we do, we must define the GetWeather method(the fake one this time). See the test file below.

In the **weather_controller** directory, create the **weather_controller_test.go** file:

touch weather_controller_test.go
18 / 25
Utilizing the Power of Interfaces when Mocking and Testing External APIs in Golang.md 1/17/2023

package weather_controller

import (
"encoding/json"
"fmt"
"interface-testing/api/domain/weather_domain"
"interface-testing/api/services"
"net/http"
"net/http/httptest"
"testing"

"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)

var (
getWeatheFunc func(request weather_domain.WeatherRequest)
(*weather_domain.Weather, weather_domain.WeatherErrorInterface)
)

type weatherServiceMock struct{}

//We are mocking the service method "GetWeather"


func (w *weatherServiceMock) GetWeather(request weather_domain.WeatherRequest)
(*weather_domain.Weather, weather_domain.WeatherErrorInterface) {
return getWeatheFunc(request)
}

func TestGetWeatherLatitudeInvalid(t *testing.T) {


getWeatheFunc = func(request weather_domain.WeatherRequest)
(*weather_domain.Weather, weather_domain.WeatherErrorInterface) {
return nil, weather_domain.NewBadRequestError("invalid latitude body")
}
services.WeatherService = &weatherServiceMock{}
response := httptest.NewRecorder()
c, _ := gin.CreateTestContext(response)
c.Request, _ = http.NewRequest(http.MethodGet, "", nil)
c.Params = gin.Params{
{Key: "apiKey", Value: "right_api_key"},
{Key: "latitude", Value: "1rte4.78"},
{Key: "longitude", Value: fmt.Sprintf("%f", 42.78)},
}
GetWeather(c)
assert.EqualValues(t, http.StatusBadRequest, response.Code)
apiErr, err := weather_domain.NewApiErrFromBytes(response.Body.Bytes())
assert.Nil(t, err)
assert.NotNil(t, apiErr)
assert.EqualValues(t, http.StatusBadRequest, apiErr.Status())
assert.EqualValues(t, "invalid latitude body", apiErr.Message())
}

19 / 25
Utilizing the Power of Interfaces when Mocking and Testing External APIs in Golang.md 1/17/2023

func TestGetWeatherLongitudeInvalid(t *testing.T) {


getWeatheFunc = func(request weather_domain.WeatherRequest)
(*weather_domain.Weather, weather_domain.WeatherErrorInterface) {
return nil, weather_domain.NewBadRequestError("invalid longitude body")
}
services.WeatherService = &weatherServiceMock{}

response := httptest.NewRecorder()
c, _ := gin.CreateTestContext(response)
c.Request, _ = http.NewRequest(http.MethodGet, "", nil)
c.Params = gin.Params{
{Key: "apiKey", Value: "right_api_key"},
{Key: "latitude", Value: fmt.Sprintf("%f", 12.78)},
{Key: "longitude", Value: "23awe.78"},
}
GetWeather(c)
assert.EqualValues(t, http.StatusBadRequest, response.Code)
apiErr, err := weather_domain.NewApiErrFromBytes(response.Body.Bytes())
assert.Nil(t, err)
assert.NotNil(t, apiErr)
assert.EqualValues(t, http.StatusBadRequest, apiErr.Status())
assert.EqualValues(t, "invalid longitude body", apiErr.Message())
}

func TestGetWeatherLatitudeInvalidLocation(t *testing.T) {


getWeatheFunc = func(request weather_domain.WeatherRequest)
(*weather_domain.Weather, weather_domain.WeatherErrorInterface) {
return nil, weather_domain.NewBadRequestError("The given location is
invalid")
}
services.WeatherService = &weatherServiceMock{}
response := httptest.NewRecorder()
c, _ := gin.CreateTestContext(response)
c.Request, _ = http.NewRequest(http.MethodGet, "", nil)
c.Params = gin.Params{
{Key: "apiKey", Value: "right_api_key"},
{Key: "latitude", Value: fmt.Sprintf("%f", 122334.78)},
{Key: "longitude", Value: fmt.Sprintf("%f", 42.78)},
}
GetWeather(c)
assert.EqualValues(t, http.StatusBadRequest, response.Code)
apiErr, err := weather_domain.NewApiErrFromBytes(response.Body.Bytes())
assert.Nil(t, err)
assert.NotNil(t, apiErr)
assert.EqualValues(t, http.StatusBadRequest, apiErr.Status())
assert.EqualValues(t, "The given location is invalid", apiErr.Message())
}

func TestGetWeatherLongitudeInvalidLocation(t *testing.T) {


getWeatheFunc = func(request weather_domain.WeatherRequest)
(*weather_domain.Weather, weather_domain.WeatherErrorInterface) {
return nil, weather_domain.NewBadRequestError("The given location is
invalid")
}
20 / 25
Utilizing the Power of Interfaces when Mocking and Testing External APIs in Golang.md 1/17/2023

services.WeatherService = &weatherServiceMock{}

response := httptest.NewRecorder()
c, _ := gin.CreateTestContext(response)
c.Request, _ = http.NewRequest(http.MethodGet, "", nil)
c.Params = gin.Params{
{Key: "apiKey", Value: "right_api_key"},
{Key: "latitude", Value: fmt.Sprintf("%f", 12.78)},
{Key: "longitude", Value: fmt.Sprintf("%f", 423243.78)},
}
GetWeather(c)
assert.EqualValues(t, http.StatusBadRequest, response.Code)
apiErr, err := weather_domain.NewApiErrFromBytes(response.Body.Bytes())
assert.Nil(t, err)
assert.NotNil(t, apiErr)
assert.EqualValues(t, http.StatusBadRequest, apiErr.Status())
assert.EqualValues(t, "The given location is invalid", apiErr.Message())
}

func TestGetWeatherInvalidKey(t *testing.T) {


getWeatheFunc = func(request weather_domain.WeatherRequest)
(*weather_domain.Weather, weather_domain.WeatherErrorInterface) {
return nil, weather_domain.NewForbiddenError("permission denied")
}
services.WeatherService = &weatherServiceMock{}
response := httptest.NewRecorder()
c, _ := gin.CreateTestContext(response)
c.Request, _ = http.NewRequest(http.MethodGet, "", nil)
c.Params = gin.Params{
{Key: "apiKey", Value: "wrong_api_key"},
{Key: "latitude", Value: fmt.Sprintf("%f", 12.78)},
{Key: "longitude", Value: fmt.Sprintf("%f", 42.78)},
}
GetWeather(c)
var apiError weather_domain.WeatherError
err := json.Unmarshal(response.Body.Bytes(), &apiError)
assert.Nil(t, err)
assert.EqualValues(t, http.StatusForbidden, response.Code)
assert.EqualValues(t, "permission denied", apiError.ErrorMessage)
}

//
func TestGetWeatherSuccess(t *testing.T) {
getWeatheFunc = func(request weather_domain.WeatherRequest)
(*weather_domain.Weather, weather_domain.WeatherErrorInterface) {
return &weather_domain.Weather{
Latitude: 20.34,
Longitude: -12.44,
TimeZone: "Africa/Nouakchott",
Currently: weather_domain.CurrentlyInfo{
Temperature: 78.02,
Summary: "Overcast",
DewPoint: 32.37,
Pressure: 1014.1,
21 / 25
Utilizing the Power of Interfaces when Mocking and Testing External APIs in Golang.md 1/17/2023

Humidity: 0.19,
},
}, nil
}
services.WeatherService = &weatherServiceMock{}
response := httptest.NewRecorder()
c, _ := gin.CreateTestContext(response)
c.Request, _ = http.NewRequest(http.MethodGet, "", nil)
c.Params = gin.Params{
{Key: "apiKey", Value: "right_api_key"},
{Key: "latitude", Value: fmt.Sprintf("%f", 20.34)},
{Key: "longitude", Value: fmt.Sprintf("%f", -12.44)},
}
GetWeather(c)
var weather weather_domain.Weather
err := json.Unmarshal(response.Body.Bytes(), &weather)
assert.Nil(t, err)
assert.NotNil(t, weather)
assert.EqualValues(t, http.StatusOK, response.Code)
assert.EqualValues(t, 20.34, weather.Latitude)
assert.EqualValues(t, -12.44, weather.Longitude)
assert.EqualValues(t, "Africa/Nouakchott", weather.TimeZone)
assert.EqualValues(t, 78.02, weather.Currently.Temperature)
assert.EqualValues(t, 0.19, weather.Currently.Humidity)
assert.EqualValues(t, 1014.1, weather.Currently.Pressure)
assert.EqualValues(t, 32.37, weather.Currently.DewPoint)
assert.EqualValues(t, "Overcast", weather.Currently.Summary)
}

For all the above tests, none actually hit the real API. This is how an external API should be tested.

Step 8: Running the App


We are pretty much set to run this application.

From the **api** directory, create the **app** directory.

mkdir app

a. Routing

Create the **routes.go** file inside the **app** directory:

cd app && touch routes.go

package app

22 / 25
Utilizing the Power of Interfaces when Mocking and Testing External APIs in Golang.md 1/17/2023

import "interface-testing/api/controllers/weather_controller"

func routes() {
router.GET("/weather/:apiKey/:latitude/:longitude",
weather_controller.GetWeather)
}

b. Application file

Still inside the app directory, create the **app.go** file. The file will call the routes:

touch app.go

package app

import (
"github.com/gin-gonic/gin"
"log"
)

var (
router = gin.Default()
)

func RunApp(){

routes()

if err := router.Run(":8080"); err != nil {


log.Fatal(err)
}
}

c. main.go

Remember, earlier that we created the main.go file in the project root directory. We need to call the RunApp
function defined in the app.go file.

package main

import (
"interface-testing/api/app"
)

func main(){

app.RunApp()

23 / 25
Utilizing the Power of Interfaces when Mocking and Testing External APIs in Golang.md 1/17/2023

Your directory structure should look like this:

At this point it is safe to run:

go run main.go

24 / 25
Utilizing the Power of Interfaces when Mocking and Testing External APIs in Golang.md 1/17/2023

Your app should be running at port 8080.

Use curl, postman, or navigate to your browser and enter testing details (your API key, latitude, and
longitude). You should get a response of the weather based on the data supplied.

You can once again run your test suite. If you are using the GoLand editor, right-click on the api directory,
and you can choose to run the tests in the api directory with coverage. When you do, you will observe that all
the providers, services, and controllers packages have 100% coverage.

Get the code on github.

Cheers.

25 / 25

You might also like