DEV Community

Cover image for Essential Unit Testing for Go APIs – Build Code with Confidence
Neel Patel
Neel Patel

Posted on

Essential Unit Testing for Go APIs – Build Code with Confidence

As we’ve been building out this API, we’ve covered authentication, logging, Dockerization, and more. But one thing we haven’t discussed yet is testing! If you want your API to be production-ready, adding solid unit tests is crucial. In this post, we’ll go over the basics of unit testing in Go, so you can catch bugs early and ship high-quality code.

Why Unit Testing?

Unit tests help you verify that each part of your codebase works as expected. They’re your first line of defense against bugs, regressions, and other nasty surprises. With Go’s built-in testing library, you can quickly set up tests that:

  • Ensure consistent behavior of your functions.
  • Make it easier to refactor code without introducing new issues.
  • Improve your confidence that everything is working as it should.

Ready to get started? Let’s dive in! 🏊


Step 1: Setting Up a Basic Test

Go’s testing framework is simple and integrated right into the language. You can create a test file by naming it with the _test.go suffix. Let’s start by testing a simple function in main.go:

// main.go
package main

func Add(a, b int) int {
    return a + b
}
Enter fullscreen mode Exit fullscreen mode

Now, create a file named main_test.go and add the following code:

// main_test.go
package main

import "testing"

func TestAdd(t *testing.T) {
    result := Add(2, 3)
    expected := 5

    if result != expected {
        t.Errorf("Add(2, 3) = %d; want %d", result, expected)
    }
}
Enter fullscreen mode Exit fullscreen mode

How It Works:

  1. Test Function: Any test function in Go must start with Test and accept a *testing.T parameter.
  2. Assertion: We check if the result matches our expectation. If it doesn’t, we log an error using t.Errorf.

To run the test, just use:

go test
Enter fullscreen mode Exit fullscreen mode

If everything works, you’ll see an ok message. 🎉


Step 2: Testing HTTP Handlers

Now, let’s write a test for one of our HTTP handlers. We’ll use Go’s httptest package to create a mock HTTP request and response recorder.

// main_test.go
package main

import (
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestGetBooksHandler(t *testing.T) {
    req, err := http.NewRequest("GET", "/books", nil)
    if err != nil {
        t.Fatal(err)
    }

    rr := httptest.NewRecorder()
    handler := http.HandlerFunc(getBooks)

    handler.ServeHTTP(rr, req)

    if status := rr.Code; status != http.StatusOK {
        t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
    }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  1. httptest.NewRequest: Creates a new HTTP request. This mocks a request to your /books endpoint.
  2. httptest.NewRecorder: Mocks an HTTP response. We’ll check this later to see if it matches our expectations.
  3. ServeHTTP: Calls our getBooks handler with the mock request and recorder.

This way, you can isolate and test your handlers without having to spin up a full server. 🚀


Step 3: Running Tests with Coverage

Go has a built-in way to check test coverage. To see what percentage of your code is covered by tests, you can run:

go test -cover
Enter fullscreen mode Exit fullscreen mode

For more detailed coverage, generate an HTML report:

go test -coverprofile=coverage.out
go tool cover -html=coverage.out
Enter fullscreen mode Exit fullscreen mode

Open the generated HTML file to visualize which parts of your code are covered. It’s a fantastic way to see where you may need additional testing.


Step 4: Mocking External Dependencies

When testing functions that depend on external services (e.g., database or external API calls), you can use interfaces to mock those dependencies.

// Define a simple interface for our database
type Database interface {
    GetBooks() ([]Book, error)
}

// Implement a mock database
type MockDatabase struct{}

func (m MockDatabase) GetBooks() ([]Book, error) {
    return []Book{{Title: "Mock Book"}}, nil
}
Enter fullscreen mode Exit fullscreen mode

By using interfaces, you can replace the actual dependency with your mock during testing. This keeps your tests fast, isolated, and repeatable.


What’s Next?

Now that you’ve started building unit tests, try adding tests to other parts of your API! 🚀 Next week, we’ll look at integrating a CI/CD pipeline so these tests can run automatically with every change. Stay tuned!


Question for You: What’s your favorite testing tool or technique? Drop a comment below—I’d love to hear how other Go devs approach testing!


With these basics, you’re well on your way to writing solid tests that make your Go API more reliable. For more testing tips and advanced techniques, stay tuned for future posts. Happy testing! 🧪

Top comments (0)