Testing is a critical aspect of software development, ensuring code reliability, maintainability, and performance. In the Golang ecosystem, a variety of testing frameworks have emerged, each offering unique features and capabilities that cater to different needs. As a backend engineer who relies heavily on robust testing practices, I decided to explore and compare the most popular Golang testing frameworks available today.

In this blog, I’ll walk you through my experience trying out different testing libraries, including their pros, cons, and when to use them. From built-in testing tools to powerful third-party frameworks, we’ll dive into what makes each option stand out. Whether you’re new to testing in Go or looking to refine your approach, this guide will provide insights to help you choose the right tools for your projects. Let’s get started!

Application Overview

One of Go’s most popular use cases is for building web applications. Therefore, knowing how to build and test web applications is important. We will learn through the TodoList application
This simple application allows users to sign up and create their Todolist. Users can create, and update the todo list from UNDONE DONE. The TodoList application then generates a notification and sends it to the notification service for notifying to other devices

Components Overview
TodoList ServiceUser ServiceNotification Service
Creating todoCreating UserExternal service
Updating todo statusUpdating UserHandling all the notification functionality
Listing all todos by UserIDFetching User by ID and their todos
The responsibilities of the main components of the TodoList application

Each of the services has its own specialization and separate responsibilities:
* TodoService is in charge of all todos management. This service implements the functionality of creating, updating, listing, and filtering todos. As this application is quite limited in functionality, todos are only filtered by the owner’s user ID
* UserService is in charge of all user management aspects. This service implements functionality for creating and updating user profiles. It can also fetch a given user by ID and relies on TodoService to receive a list of all todos
* Notification Service is an external service to the TodoList application, which is in charge of notifying the todo which have been just DONE to all other devices of user.

User journeys

A user journey is a sequence of steps or interactions that a user follows to achieve a goal

Chat GPT

Persistent storage

The next change we will make is to add persistent storage to the TodoList application, allowing us to save the state once the application shuts down. As SQL databases are still the most popular persistent storage solutions, we will use an SQL database in the demo application.

We will use a PostgreSQL database, an open-source relational database widely used in production. It also has excellent cloud support across the public cloud providers, so it is a natural choice for our technology stack.

We will make use of an object-relational mapping (ORM) library, which allows us to create a bridge between our Go custom types and our PostgreSQL database.
There are a few ORM solutions in the Go ecosystem. One of the most popular ones is GORM (https://github.com/go-gorm/gorm), which is an open source easy-to-use Go library. This library will make it easy for us to interact with our database solution, removing the need to manage SQL as raw strings in our source code.

golang-migrate

The golang-migrate project (https://github.com/golang-migrate/migrate) allows us to write migrations and run them as part of our application startup. Once installed, we can initialize the table directly in the code:

postgresURL, ok := os.LookupEnv("TODOLIST_DB_URL")
if !ok {
	log.Fatal("$TODOLIST_DB_URL not found")
}
m, err := migrate.New("file://db/migrations", postgresURL)
if err != nil {
	log.Fatalf("migrate:%v", err)
}
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
	log.Fatalf("migration up:%v", err)
}

Implement Source Code

The source code for the application can be found at https://github.com/PLHieu/todolist

Setup Dockerfile

FROM golang:1.22-alpine

WORKDIR /app

COPY go.mod ./
COPY go.sum ./
COPY . .

RUN go mod download
RUN go build -o app ./cmd
RUN apk --no-cache add curl
EXPOSE ${TODOLIST_PORT}

CMD [ "./app" ]

* The FROM statement indicates the base image of this build stage. In Docker terminology, alpine images are lightweight and run on the Linux BusyBox distribution.
* The WORKDIR statement creates and sets the working directory of the Docker container. All further commands in our file execute in this directory.
* The COPY statement copies all the source files from our local directory to the container’s working directory. Remember that containers are isolated from the underlying local directories, so these files must be copied to the container.
* The RUN statement executes the commands needed to build our Go executable by first downloading its dependencies and then specifying the directory that contains our application entry point. The Dockerfile is placed alongside the go.mod file in the root directory, so we need to explicitly state which of our chapter entry points to build from.
* The EXPOSE statement instructs the Docker container to listen to network requests on the given port, as indicated by the TODOLIST_PORT environment variable. This variable is needed for the BookSwap application, so ensure that it is set in your terminal session before you run the application. The instructions of how to set environment variables will be different according to your operating system. If you want to run with the default setup, set the TODOLIST_PORT environment variable to 3000.
* The CMD statement specifies the command that should be run once the container starts. We run the executable from the go build step.

Running all components with docker-compose

Docker Compose is a powerful tool that allows you to define and manage multi-container Docker applications. By using a simple YAML file, Docker Compose enables you to specify the services, networks, and volumes that your application needs, streamlining the process of building and orchestrating complex environments.

version: '3'

services:
  todos:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "${TODOLIST_PORT}:${TODOLIST_PORT}"
    depends_on:
      db:
        condition: service_healthy
    env_file:
      - docker.env
    restart: on-failure
  db:
    image: postgres:15.0-alpine
    ports:
      - "5432:5432"
    expose:
      - "5432"
    env_file:
      - docker.env
    restart: on-failure
    healthcheck:
      test: [ "CMD-SHELL", "pg_isready -U root -d books" ]
      interval: 1s
      timeout: 1s
      retries: 5
POSTGRES_USER=root
POSTGRES_PASSWORD=root
POSTGRES_DB=todos
TODOLIST_DB_URL=postgres://root:root@db:5432/todos?sslmode=disable
TODOLIST_PORT=3000

Setup env variable TODOLIST_PORT=3000 & Run docker compose up --build command from the project root directory to start all components.

Testing APIs with Postman

The postman requests collection can be found at https://github.com/PLHieu/todolist/blob/main/TodoList.postman_collection.json

Next Steps

So now we already finish setting up a simple TodoList Application. In the next step, we will try using Testify, a very popular golang testing framework to create Unit Tests, Intergration Tests and E2E Tests

References

https://github.com/PacktPublishing/Test-Driven-Development-in-Go/tree/main

Categorized in: