DEV Community

Cover image for Testing Golang Applications
Arsham Roshannejad
Arsham Roshannejad

Posted on

Testing Golang Applications

Testing applications is a critical aspect of the software development process. Whether you’re building a small utility or a large-scale enterprise system, investing time and effort into testing can save you from countless headaches down the line. In this article, we will explore why testing is crucial for application development and how it contributes to achieving higher quality, reliability, and customer satisfaction.


Reasons for testing application layers :

  • Detecting and Preventing Bugs: Bugs are inevitable in software development. Testing is your shield against them. Proper testing helps identify and eliminate defects at an early stage, minimizing their impact on the final product. By ensuring that code functions as intended, you can significantly reduce the chances of encountering unexpected issues in a production environment.

  • Improving Software Quality: Quality software provides a smooth user experience, meets business requirements, and delivers value to its users. Through comprehensive testing, you can verify that your application meets functional and non-functional requirements, adheres to design guidelines, and performs optimally in various scenarios. Testing helps you deliver a polished and reliable product that meets or exceeds user expectations.

  • Enabling Continuous Integration and Delivery: Testing lays the foundation for successful implementation of continuous integration and continuous delivery (CI/CD) practices. By automating tests and integrating them into your CI/CD pipeline, you can ensure that code changes don’t introduce regressions or break existing functionalities. Testing becomes an essential part of the development workflow, providing fast feedback and enabling frequent, reliable releases.

  • Identifying Performance Bottlenecks: Performance issues can be detrimental to an application’s success. Testing allows you to assess the performance and scalability of your application under various loads and stress conditions. By conducting performance tests, you can pinpoint bottlenecks, optimize resource utilization, and ensure that your application delivers a responsive experience to users, even during peak usage periods.

  • Enhancing Maintainability and Long-term Stability: As applications evolve and grow, maintaining their stability becomes a daunting task. Here, testing plays a crucial role. With a robust suite of tests, you can confidently refactor or modify code without introducing unintended consequences. Tests act as safety nets, allowing you to make changes with confidence, knowing that you have mechanisms in place to catch possible regressions.


Testing Package in Golang : https://pkg.go.dev/testing

The website is the official documentation for the Go programming language’s testing package. The testing package in Go provides support for writing automated tests for your code. It includes functions and utilities for writing test cases, running tests, and reporting test results.

So let’s talk about how to test applications.

Test file structure follows a specific convention to ensure proper test discovery and execution. Test files should have a name that ends with _test.go . This naming convention distinguishes them as test files and allows the Go tooling to automatically recognize and execute the tests.

We assume that the structure and files of our project are like this.

project/
├── main.go
├── utils.go
├── test_utils.go

in handlers/utils.go

package main

func SumNums(num1 int, num2 int) int {
    return num1 + num2
}

Enter fullscreen mode Exit fullscreen mode

in main.go

package main

import (
    "fmt"
)

func main() {
    var firstNum, secondNum int
    fmt.Printf("This is a simple calculate app\n")

    fmt.Printf("Please Enter first Number: ")
    fmt.Scan(&firstNum)

    fmt.Printf("Please Enter Second Number: ")
    fmt.Scan(&secondNum)

    sum := SumNums(firstNum, secondNum)
    fmt.Printf("The Result is %d\n", sum)
}
Enter fullscreen mode Exit fullscreen mode

Note:To execute the go test command, we must be in the path of the main package, that is, if the main file is inside the cmd package, we must execute the test command from within this package. so enter in terminal :

cd ~/go/src/github/username/projectname/

Now if we run go test . command:

Image description

Why is it saying that we don't have a test file? due to wrong naming. The test file should end with ـtest.go We change the name of the test file to utils_test.go and execute the go test . command again:

Image description

Excellent, now the test file is recognized and the tests we write inside the file will be evaluated.

Let's write a test for the SumNums function:

package main

import (
 "testing"
)

func TestSum(t *testing.T) {
 num1 := 12
 num2 := 100
 sum := SumNums(num1, num2)

 if sum != 110 {
  t.Errorf("This function adds numbers incorrectly. answer is : %d", sum)
 }
}

Enter fullscreen mode Exit fullscreen mode

run go test ../tests/ command:

Image description

Excellent, our test passed successfully. So how do you know that our test did not pass? If we change the value of the condition to another number that is not equal to the sum of the numbers of name one and two, our test will fail. Set the number opposite the condition to any number other than 112 and run the test again.

Image description

As you can see, I set the number against the condition to 110 and run the test and the test failed.

Note: In addition to naming the test file, the functions also have a series of conditions. If the function name does not start with Test, the tests will not be executed. example: TestSum , TestCRUD, TestMain .


The testing package in Go provides a built-in coverage feature that allows you to measure the code coverage of your tests. Code coverage is a metric that indicates the percentage of your codebase that is covered by tests.

To enable code coverage, you can use the --cover flag with the go test command. For example:

Image description

When you run the tests with the --cover flag, Go will execute your tests and generate a coverage report. The report will include the percentage of code covered, along with detailed information about which lines of code were covered and which were not. By default, the coverage report will be displayed in the terminal. However, you can also generate an HTML coverage report by using the --coverprofile flag followed by a file name. For example:

Image description

After executing the second command, we will be directed to the HTML page. The red parts mean the untested lines of code and the green parts mean our tests cover these parts.

Image description

Image description

As you can see, our main function is without test and it is in red color.


The -v flag in the go test command stands for "verbose" and is used to enable verbose output during test execution. When you run go test without the -v flag, it provides minimal output, only showing the final test results. However, when you include the -v flag, it provides more detailed information about the tests being executed.

Image description

The -run flag in the go test command is used to specify a regular expression pattern to select specific tests to run. By default, when you run go test without the -run flag, it runs all the tests in the package. However, if you want to run only specific tests that match a particular pattern, you can use the -run flag followed by the regular expression pattern.

Image description

Here's an example of how you can use the -run flag to run tests with names starting with "Test_Alpha_" in your repository:

shell
go test -run ^Test_Alpha_

the regular expression ^Test_Alpha_ matches test function names that start with "Test_Alpha_".

The ^ character in the regular expression denotes the start of the string. By using this command, only the tests with names starting with "Test_Alpha_" will be executed, and all other tests will be skipped.

This approach allows you to selectively run tests within your repository based on their names or patterns, providing more focused testing when needed.


If you have many test files in one package and you want to avoid testing a test file in this package for the time being, you can enter //go:build integration tag before the package name in test file. From now on, this file will not be tested.

//go:build integration

package main

import (
    "testing"
)

func TestSum(t *testing.T) {
    num1 := 12
    num2 := 100
    sum := SumNums(num1, num2)

    if sum != 112 {
       t.Errorf("This function adds numbers incorrectly. answer is : %d", sum)
    }
}
Enter fullscreen mode Exit fullscreen mode

Image description

The testing.M type in Go's testing package is used to control the execution of tests and perform setup and teardown operations at the package level. It allows you to define functions that run before and after all the tests in a package.

Here's an example of how to use testing.M :

package main

import (
    "fmt"
    "os"
    "testing"
)

func TestMain(m *testing.M) {
    fmt.Printf("This is a simple calculate app\n")
    code := m.Run()
    os.Exit(code)
}
Enter fullscreen mode Exit fullscreen mode

Image description

The following code is a simple test function that prints a message and then runs the tests specified in the test suite. It also exits the program with the code returned by the test suite.

Here are a few scenarios where the TestMain function can be helpful:

  • Setting up shared resources: If your tests require shared resources such as databases, network connections, or file handles, you can set them up once in the TestMain function before running the tests. This allows you to avoid duplicating setup code in each test function.
  • Cleaning up resources: After running the tests, you may need to clean up or release the resources that were set up during the test execution. The TestMain function provides a convenient place to perform these cleanup operations.
  • Initializing test environment: In some cases, you might need to initialize the test environment or configure global settings before running the tests. The TestMain function can be used to perform these initialization tasks.
  • Custom test reporting: You can use the TestMain function to customize the reporting or logging of test results. For example, you might want to generate custom reports, send notifications, or log test results to a specific location.

The last thing that comes to my mind using structs in test functions allows you to write more flexible and reusable test cases. By defining a struct and using it as a parameter in a test function, you can easily test multiple scenarios or variations of the same functionality without duplicating the test code. example:

package main

import (
    "testing"
)

func TestSum(t *testing.T) {
    var tests = []struct {
       nameTest    string
       input1      int
       input2      int
       expectedSum int
    }{
       {nameTest: "Test A", input1: 12, input2: 13, expectedSum: 25},
       {nameTest: "Test A", input1: -12, input2: 20, expectedSum: 8},
       {nameTest: "Test A", input1: 1, input2: -1, expectedSum: 0},
    }
    for _, e := range tests {
       sum := SumNums(e.input1, e.input2)
       if sum != e.expectedSum {
          t.Errorf("in %s expected sum is %d but got %d", e.nameTest, e.expectedSum, sum)
       }
    }
}
Enter fullscreen mode Exit fullscreen mode

Image description

gooooodbye :)

Top comments (0)