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
TodoList Service | User Service | Notification Service |
---|---|---|
Creating todo | Creating User | External service |
Updating todo status | Updating User | Handling all the notification functionality |
Listing all todos by UserID | Fetching User by ID and their todos |
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
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