Table of Contents
Introduction
In this tutorial, we will explore how to test microservices in Go. Microservices are a popular architectural pattern that allows us to create complex applications by dividing them into smaller, independent services. Testing these microservices ensures that they are functioning correctly and provides confidence in the overall system.
By the end of this tutorial, you will learn:
- How to write testable code in Go
- Different testing techniques for microservices
- Best practices for testing microservices
Let’s get started!
Prerequisites
Before starting this tutorial, you should have a basic understanding of Go programming language and familiarity with writing Go code. You should also have Go installed on your machine. If you haven’t installed Go yet, please visit the official Go website (https://golang.org/) and follow the installation instructions specific to your operating system.
Setup
To demonstrate the testing of microservices, we will create a simple microservice that provides an API endpoint to fetch weather information. The microservice will interact with an external weather API to fetch the weather data.
Create a new directory for our project and navigate to it in your terminal:
mkdir weather-microservice
cd weather-microservice
Initialize a new Go module:
go mod init github.com/your-username/weather-microservice
Create a new file called main.go
and open it in your favorite text editor.
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/weather", weatherHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
func weatherHandler(w http.ResponseWriter, r *http.Request) {
// Fetch weather info from external API and return as response
fmt.Fprint(w, "Today's weather: Sunny")
}
Save the file and run your microservice using the following command:
go run main.go
The microservice will start and listen on port 8080 for incoming HTTP requests.
Writing Testable Code
Before writing tests for our microservice, it’s important to write testable code. Testable code is structured in a way that allows us to easily write isolated unit tests.
In our example microservice, we have a weatherHandler
function that fetches weather information from an external API. To make it testable, we can separate the weather fetching logic into a separate function and make it a dependency of the weatherHandler
function.
Update the main.go
file as follows:
package main
import (
"fmt"
"log"
"net/http"
)
type WeatherAPI interface {
FetchWeather() (string, error)
}
type ExternalWeatherAPI struct{}
func (api *ExternalWeatherAPI) FetchWeather() (string, error) {
// Implementation to fetch weather info from external API
return "Sunny", nil
}
func main() {
api := &ExternalWeatherAPI{}
http.HandleFunc("/weather", weatherHandler(api))
log.Fatal(http.ListenAndServe(":8080", nil))
}
func weatherHandler(api WeatherAPI) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
weather, err := api.FetchWeather()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprint(w, "Today's weather:", weather)
}
}
In the updated code, we have introduced the WeatherAPI
interface and the ExternalWeatherAPI
struct that implements this interface. The FetchWeather
function now resides in the ExternalWeatherAPI
struct. By injecting the WeatherAPI
interface into the weatherHandler
function, we can easily write test cases by providing our own implementation of the WeatherAPI
interface.
Testing Techniques
Now that we have made our code testable, we can write tests for our microservice using various testing techniques. Let’s explore some common testing techniques for microservices:
Unit Testing
Unit testing involves testing individual units or components of our code in isolation. In Go, we can use the built-in testing
package to write unit tests. Create a new file called main_test.go
and open it in your text editor.
package main
import (
"net/http"
"net/http/httptest"
"testing"
)
type MockWeatherAPI struct{}
func (api *MockWeatherAPI) FetchWeather() (string, error) {
// Mock implementation to return fixed weather
return "Rainy", nil
}
func TestWeatherHandler(t *testing.T) {
handler := weatherHandler(&MockWeatherAPI{})
req, err := http.NewRequest("GET", "/weather", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
expected := "Today's weather: Rainy"
if rr.Body.String() != expected {
t.Errorf("unexpected response - got %s, want %s", rr.Body.String(), expected)
}
}
In this test, we create a mock implementation of the WeatherAPI
interface called MockWeatherAPI
to provide fixed weather information. We then initialize our weatherHandler
with the MockWeatherAPI
and send a test HTTP request to the handler using httptest.NewRecorder()
. Finally, we assert that the response matches our expected output.
To run the test, use the following command:
go test
The test should run successfully, and you should see the output PASS
.
Integration Testing
Integration testing involves testing the interaction between different components of our microservice. In our case, we want to ensure that our microservice can interact correctly with an external weather API.
Create a new file called integration_test.go
and open it in your text editor.
package main
import (
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
)
func TestWeatherAPIIntegration(t *testing.T) {
handler := weatherHandler(&ExternalWeatherAPI{})
req, err := http.NewRequest("GET", "/weather", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
resp := rr.Result()
body, _ := ioutil.ReadAll(resp.Body)
expected := "Today's weather: Sunny"
if string(body) != expected {
t.Errorf("unexpected response - got %s, want %s", string(body), expected)
}
}
In this test, we use the actual implementation of ExternalWeatherAPI
and send a request to our microservice. We then assert that the response matches the expected weather information.
To run the integration test, use the following command:
go test -tags=integration
Note that we use the -tags=integration
flag to differentiate integration tests from unit tests. This allows us to have finer control over which tests to run.
Conclusion
In this tutorial, we have explored how to test microservices in Go. We started by setting up a simple microservice and then learned how to write testable code by separating concerns and using interfaces. We then covered two common testing techniques: unit testing and integration testing.
By following the examples and best practices provided in this tutorial, you can confidently test your own microservices in Go. Remember to always aim for good code coverage and test all possible scenarios to ensure the reliability and correctness of your microservices.
Happy testing!