Unit testing in go with MySQL

Unit test: must part for coding
Dec 30 2021 · 5 min read

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.

What is the flow of unit testing in a software application?

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.

1_jPgspkjoLd7GmufqZljLkQ.webp
Flow diagram of unit test

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

Unit test for music API

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.

  • Testing package considers a file named with suffix _test as a test file. Always create a test file with a name like filename_test.go.
  • Testing package tests all methods prefixed with Test word in filename_test.go file. So method’s name should be like TestMethodName.


I have divided the process of writing the unit test into some small parts for easy understanding.

1. Initialize unit test data

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
}
  • Declared 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.
  • Then created a music table in test_db and inserted fake data.
  • Initialized repository(musicRepo) with sqlxDb for music module.

2. Setup gin router for test APIs

//music_test.go
func setUpRouter(router *gin.Engine) {
    router.GET("/music", musicRepo.GetMusic)
}

3. Generate expected data

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    
}

4. Create a common API request structure that can be used for all APIs.

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.

5. Write a common test for all APIs.

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))
	 }
    }
}

6. Run the unit test using the below command.

go clean -testcache && go test .
  • The unit test returns the result from the cache if it had. So always run go clean -testcache before running tests in golang.
  • go test will run the TestAllAPIs method from our music_test.go file.

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!!

Coverage measurement

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

  • The first command runs the test and will create coverage.out file in your project’s root folder.
  • The second command creates a coverage.html file and converts coverage.out to an HTML, you can see it by opening it in the browser.

Final words

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.


sumita-k image
Sumita Kevat
Sumita is an experienced software developer with 5+ years in web development. Proficient in front-end and back-end technologies for creating scalable and efficient web applications. Passionate about staying current with emerging technologies to deliver.


sumita-k image
Sumita Kevat
Sumita is an experienced software developer with 5+ years in web development. Proficient in front-end and back-end technologies for creating scalable and efficient web applications. Passionate about staying current with emerging technologies to deliver.

Let's Work Together

Not sure where to start? We also offer code and architecture reviews, strategic planning, and more.

cta-image
Get Free Consultation
footer
Subscribe Here!
Follow us on
2024 Canopas Software LLP. All rights reserved.