Tests are stories we tell the next generation of programmers on a project. — Roy osherove
Unit test is the first most essential part of software testing, which focuses on small elements of software design.
We are what we repeatedly do. Excellence, then, is not an act, but a habit. Try out Justly and start building your habits today!
Suppose you are developing a music app, your APIs are in golang and you want to test those APIs. Let’s understand the flow of the unit test in development.
The prerequisites for the article are that you have a basic knowledge of golang and unit tests.
Today, we will write a simple unit test for music API which is written in gin and uses sqlx for database interaction.
We will be using the repository structure for avoiding global variables. If you are not familiar with it, please have a look at the below article.
Here is the sample code of the music API
//music.go
func (repo *MusicRepository) GetMusic(c *gin.Context) {
music := []Music{}
err := repo.db.Select(&music, "SELECT id, name, music_url FROM music")
if err != nil{
log.Fatal(err)
}
c.JSON(http.StatusOK, music)
}
This API returns JSON objects of music from the MySQL database.
Now let's write a unit test to check if music API is returning correct data. We are using go’s native testing package and assert library for unit testing.
Before that, please consider some common key points for unit tests in golang.
I have divided the process of writing the unit test into some small parts for easy understanding.
In this step, we will initialize all required things for our module(here it is music) like databases, loggers, etc...
For mocking the database, we are creating a music table in database test_db and inserting some fake data into it using the following methods.
//music_test.go
func createMusicTable(db *sqlx.DB) error {
music := "CREATE TABLE IF NOT EXISTS `music` (`id` int(10) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(191) NOT NULL, PRIMARY KEY (`id`))"
_, err = db.Exec(music)
if err != nil {
return err
}
return nil
}
func insertDataToMusicTable(db *sqlx.DB){
db.MustExec("INSERT INTO music(`id`, `name`, `music_url`) VALUES(1,'cheap thrills', 'https://music/cheap-thrills'),(2, 'steaches', 'https://music/steaches')")
}
func truncateTables(db *sqlx.DB) {
db.MustExec("TRUNCATE TABLE music")
}
Let us initialize the database and repository for the music module using the above methods.
//music_test.go
var musicRepo *MusicRepository
func initializeTest() error {
sqlxDb := db.NewSql()
err := createMusicTable(sqlxDb)
if err != nil {
return err
}
truncateTables(sqlxDb)
insertDataToMusicTable(sqlxDb)
musicRepo = music.New(sqlxDb)
return nil
}
musicRepo
variable that will be used within the test. Here musicRepo is only accessible within music_test.go
file.db.NewSql()
contains a method that will initialize the test_db
database instead of the original production database using sqlx
.test_db
and inserted fake data.sqlxDb
for music module.//music_test.go
func setUpRouter(router *gin.Engine) {
router.GET("/music", musicRepo.GetMusic)
}
we have to generate data that we are expecting from music API. Golang parses JSON in the map so we have to write expected data using maps.
//music_test.go
func expectedMusicData() []interface{}{
music1 := map[string]interface{}{
"id" : 1.0,
"name" : "cheap thrills",
"music_url" : "https://music/cheap-thrills"
}
music2 := map[string]interface{}{
"id" : 2.0,
"name" : "steaches",
"music_url" : "https://music/steaches"
}
music := []interface{}{music1, music2}
return music
}
var requestTests = []struct {
Url string
Method string
Headers map[string]interface{}
Body interface{}
ResponseCode int
ExpectedData interface{}
}{
{
"/music",
"GET",
nil,
nil,
http.StatusOK,
expectedMusicData(),
},
{
"/music",
"POST",
nil,
nil,
http.StatusNotFound,
nil,
},
}
Here we have defined the structure of the requests including all required fields for testing APIs. We will write a common method for testing all APIs from this structure at once.
It is the simplified version of the unit test. In the future, if you want to add more APIs then by adding your API request in the requests struct, you can test your APIs easily.
Let’s the go-ahead to our next part of the unit test.
We have to write a method, that will parse our JSON response to a map.
func parseActualData(w *httptest.ResponseRecorder, t *testing.T) []interface{} {
var gotData []interface{}
if len(w.Body.Bytes()) != 0 {
err := json.Unmarshal(w.Body.Bytes(), &gotData)
if err != nil {
t.Fatal(err)
}
}
return gotData
}
Unit tests for all APIs.
func TestAllAPIs(t *testing.T) {
// Initialize data before test
err = initializeTest()
assert.Nil(t, err)
asserts := assert.New(t)
// Setup router for apis
router := gin.New()
setUpRouter(router)
for _, testData := range requestTests {
//initialize new recorder for recording api data
w := httptest.NewRecorder()
var req *http.Request
var actualData interface{}
// Make http api request. We can also pass body here.
req, err = http.NewRequest(testData.Method, testData.Url, nil)
// assert err is nil
asserts.NoError(err)
/** if headers are not nil in testData,
then set headers to request **/
setRequestHeaders(req, testData.Headers)
router.ServeHTTP(w, req)
// assert expected and actual response code
assert.Equal(t, testData.ResponseCode, w.Code)
if testData.ExpectedData != nil{
/** If got data from api request then,
parse response to map for
comparing it to expected data **/
actualData = parseActualData(w, t)
}
// assert expected and actual response data
assert.Equal(t, testData.ExpectedData, actualData)
}
}
setRequestHeaders method
func setRequestHeaders(req *http.Request, headers map[string]interface{}) {
if len(headers) > 0 {
for key, value := range headers {
req.Header.Add(key, value.(string))
}
}
}
go clean -testcache && go test .
The above unit test gave me results like below. Time may vary for your APIs.
ok music 0.026s
Yeah, we have tested it... and that is the end of the matter. Now you can be sure music API is working fine if the automation test passes, no need to check it manually ever!!
To measure the coverage of the unit test in percentage, run the following command in your terminal.
go test -cover
Also if you want to see the lines covered or not covered by tests, you can use the below commands.
go test -coverprofile=coverage.out
go tool cover -html=coverage.out -o coverage.html
Always put testing as a priority in the queue. After all, QUALITY is everyone’s responsibility.
If you are also interested in unit testing on android, please have a look at the Unit testing ViewModels.
We’re Grateful to have you with us on this journey!
Suggestions and feedback are more than welcome!
Please reach us at Canopas Twitter handle @canopas_eng with your content or feedback. Your input enriches our content and fuels our motivation to create more valuable and informative articles for you.
Let's Work Together
Not sure where to start? We also offer code and architecture reviews, strategic planning, and more.