DEV Community

Nilesh Prasad
Nilesh Prasad

Posted on • Edited on

Effective Unit Testing in Golang and Gin based projects

Unit testing is a critical part of building robust applications in the world of software development. Golang is known for its simplicity and efficiency, making it an ideal language for writing clean, maintainable code. In this article, we'll explore effective unit testing in Golang for projects built with Gin, Gorm, and PostgreSQL. We'll cover structuring a Golang project, testing against a real PostgreSQL database, and using Testify for testing utility.

In Golang, you could group tests per package using TestMain feature.

TestMain for Setup and Teardown

The TestMain function is a global setup and teardown mechanism provided by testify. It allows us to initialize and migrate resources before running tests and clean up afterward. This is particularly useful when there's a need for global setup or teardown actions that should be applied across all tests.

package main
import (
    "testing"
    "github.com/stretchr/testify/suite"
)

// TestMain sets up and tears down resources for all tests.
func TestMain(m *testing.M) {
    // Additional setup code here

    // Run all tests
    code := m.Run()

    // Additional teardown code here

    // Exit with the test result code
    os.Exit(code)
}
Enter fullscreen mode Exit fullscreen mode

"Testify" library in Golang

The Testify (https://github.com/stretchr/testify) package extends the functionality of the standard Go testing library, making testing in Go more expressive and efficient.

Test Suites with Testify

Testify introduces the concept of test suites, allowing us to group tests into logical units. The suite package from Testify simplifies the management of test suites. This is beneficial when tests can be logically grouped together, sharing common setup or teardown logic.

package mypackage

import (
    "testing"
    "github.com/stretchr/testify/suite"
)

// MySuite is a test suite containing multiple related tests.
type MySuite struct {
    suite.Suite
}

func (suite *MySuite) SetupTest() {
    // Setup code before each test
}

func (suite *MySuite) TestSomething() {
    // Test code
}

func TestSuite(t *testing.T) {
    // Run the test suite
    suite.Run(t, new(MySuite))
}

Enter fullscreen mode Exit fullscreen mode

When to Use TestMain and Test Suites

TestMain: In Go, you can have only one TestMain function per package. The TestMain function is a special function that allows you to perform setup and teardown tasks for your tests, and it is meant to be defined only once in the entire package. Use TestMain when there's a need for global setup or teardown actions that should be applied across all tests. This is especially relevant for scenarios like initializing and migrating databases or setting up external services.

Test Suites: The concept of test suites is typically associated with external testing frameworks like Testify. While you can define multiple test suites in a package using Testify, it's important to note that each test suite is essentially a separate testing entity and doesn't interact with other test suites. Each test suite may have its own setup, teardown, and test functions. Use test suites when you want to logically group tests that share common setup or teardown logic. This helps in maintaining a clean and organized test structure, making it easier to manage and execute related tests.

Project Structure and tests location

When structuring your Go projects, it's crucial to establish a well-organized layout that promotes readability, maintainability, and testability. Let's explore a sample project structure and discuss how to integrate unit tests effectively.

Sample Project Structure
Consider the following simplified project structure:

/myproject
|-- /handlers
|   |-- handler.go
|   |-- handler_test.go
|
|-- /db
|   |-- database.go
|   |-- database_test.go
|
|-- main.go
|-- go.mod
|-- go.sum
Enter fullscreen mode Exit fullscreen mode

The application is structured as follows:

  • handlers: This contains the HTTP request handlers for the application.
  • db: This manages the database connections and queries.
  • main.go: This is the primary entry point for the application.
  • go.mod and go.sum: These files manage the dependencies.

When writing unit tests in Go, it's common practice to place them in the same package as the code being tested. This allows you to test internal or unexported functions, ensuring that the testing scope aligns with the implementation details. By placing the test file (handler_test.go) in the same package, you can easily access unexported functions, which can enhance the thoroughness of your tests.

Writing Unit Tests in a Global Package
While colocating tests with packages provides access to internal functions, it might be beneficial to create a global test package for testing public APIs and behaviors from a user's perspective. Here's an example with a global test package:

/myproject
|-- /tests
|   |-- main_test.go
|
|-- ...
Enter fullscreen mode Exit fullscreen mode
// tests/main_test.go
package tests

import (
    "testing"

    "myproject/handlers"
)

func TestMain(t *testing.T) {
    // Your test logic here
}
Enter fullscreen mode Exit fullscreen mode

This approach ensures that you validate the external APIs and behaviors as users would interact with them.

Testing PostgreSQL

When testing interactions with a PostgreSQL database, it's crucial to ensure that your data access layer functions correctly. By setting up a dedicated test database, you can conduct tests against a real PostgreSQL instance, providing more realistic scenarios.

Example.

// db_test.go
package db

import (
    "fmt"
    "os"
    "testing"

    "github.com/stretchr/testify/suite"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

// User represents a simple user model.
type User struct {
    ID   uint
    Name string
}

// DatabaseTestSuite is the test suite.
type DatabaseTestSuite struct {
    suite.Suite
    db *gorm.DB
}

// SetupSuite is called once before the test suite runs.
func (suite *DatabaseTestSuite) SetupSuite() {
    // Set up a PostgreSQL database for testing
    dsn := "user=testuser password=testpassword dbname=testdb sslmode=disable"
    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    suite.Require().NoError(err, "Error connecting to the test database")

    // Enable logging for Gorm during tests
    suite.db = db.Debug()

    // Auto-migrate tables
    err = suite.db.AutoMigrate(&User{})
    suite.Require().NoError(err, "Error auto-migrating database tables")
}

// TestUserInsertion tests inserting a user record.
func (suite *DatabaseTestSuite) TestUserInsertion() {
    // Create a user
    user := User{Name: "John Doe"}
    err := suite.db.Create(&user).Error
    suite.Require().NoError(err, "Error creating user record")

    // Retrieve the inserted user
    var retrievedUser User
    err = suite.db.First(&retrievedUser, "name = ?", "John Doe").Error
    suite.Require().NoError(err, "Error retrieving user record")

    // Verify that the retrieved user matches the inserted user
    suite.Equal(user.Name, retrievedUser.Name, "Names should match")
}

// TearDownSuite is called once after the test suite runs.
func (suite *DatabaseTestSuite) TearDownSuite() {
    // Clean up: Close the database connection
    err := suite.db.Exec("DROP TABLE users;").Error
    suite.Require().NoError(err, "Error dropping test table")

    err = suite.db.Close()
    suite.Require().NoError(err, "Error closing the test database")
}

// TestSuite runs the test suite.
func TestSuite(t *testing.T) {
    // Skip the tests if the PostgreSQL connection details are not provided
    if os.Getenv("POSTGRES_DSN") == "" {
        t.Skip("Skipping PostgreSQL tests; provide POSTGRES_DSN environment variable.")
    }

    suite.Run(t, new(DatabaseTestSuite))
}

Enter fullscreen mode Exit fullscreen mode

Conclusion

In this guide, we covered the basics of writing unit tests in Golang. The Testify library is a valuable addition to our testing toolkit, providing additional features like test suites, mocks and assertions that help create robust and reliable test suites for Golang applications. Happy coding!

Resources for Further Learning

To delve deeper into testing with Golang and Testify, consider exploring the following resources:

Remember, writing effective tests is crucial for maintaining code quality and ensuring robust software. By embracing testing best practices and leveraging powerful tools like Testify, you can enhance the reliability of your Golang projects.

Top comments (1)

Collapse
 
geniot profile image
geniot

And where does Gin come in? :)