Setting up handlers with the Repository Pattern: A Test-driven approach in Go.
Tags: go, backenddevelopment, backend, repositorypattern
Published: 2024-04-28 19:43:02

### Introduction
Writing tests are essential in software development to ensure the reliability, maintainability, and quality of software products. In this article, I will be delving into a good approach to setting up and testing handlers in Go called the Repository pattern. The Repository pattern is a developmental approach that allows you to separate your database logic from your application logic to make it easy to test your code.

### Prerequisite 
- Basic understanding of the Go programming language

### Setting up a DB repo for the database logic
Before we begin setting up our handlers, we need to set up a basic connection to the database and we'd be doing that by implementing the repository pattern for a user model. 
We start by initializing the app as a go module using the command
```bash
go mod init github.com/orololuwa/reimagined-robot
```
Typically, you would use a repository from your GitHub as the module name.

In a `models` package, we create a simple user model like;
```go
package models

import "time"

type User struct {
    ID int
    FirstName string
    LastName  string
    Email     string
    Password string
    CreatedAt time.Time
    UpdatedAt time.Time
}
```
In a `repository` folder and package, we would create two files, `repository.go` and `users.go` as thus;

```go
package repository

import "github.com/orololuwa/reimagined-robot/models"

type UserRepo interface {
    CreateAUser(user models.User) (int, error)
    GetAUser(id int) (models.User, error)
}
```
```go
package repository

import (
	"context"
	"database/sql"
	"time"

	"github.com/orololuwa/reimagined-robot/models"
)

type user struct {
    DB *sql.DB
}

func NewUserRepo(conn *sql.DB) UserRepo {
    return &user{
        DB: conn,
    }
}
func (m *user) CreateAUser(user models.User) (int, error){
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    var newId int

    query := `
            INSERT into users 
                (first_name, last_name, email, password, created_at, updated_at)
            values 
                ($1, $2, $3, $4, $5, $6)
            returning id`

    err := m.DB.QueryRowContext(ctx, query, 
        user.FirstName, 
        user.LastName, 
        user.Email, 
        user.Password,
        time.Now(),
        time.Now(),
    ).Scan(&newId)

    if err != nil {
        return 0, err
    }

    return newId, nil
}


func (m *user) GetAUser(id int) (models.User, error){
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    var user models.User

    query := `
            SELECT (id, first_name, last_name, email, password, created_at, updated_at)
            from users
            WHERE
            id=$1
    `

    err := m.DB.QueryRowContext(ctx, query, id).Scan(
        &user.ID,
        &user.FirstName,
        &user.LastName,
        &user.Email,
        &user.Password,
        &user.CreatedAt,
        &user.UpdatedAt,
    )

    if err != nil {
        return user, err
    }

    return user, nil
}
```
In the above snippets, we create a user struct, a NewUserRepo function to initialize the struct, and receiver functions to the struct to handle the database logic. If you want to learn more about setting up a database repo, you can check [this](https://dev.to/orololuwa/setting-up-a-database-driver-and-repository-and-implementation-of-a-transaction-function-for-your-go-app-545b) out.

### Creating the handlers for the application logic
Now we create a `handlers.go` package where all the application logic will be done.

1) Paste the package declaration at the top of the file and import the packages that would be used in the handler.
```go
package handlers

import (
	"database/sql"
	"encoding/json"
	"net/http"
	"strconv"

	"github.com/go-chi/chi/v5"
	"github.com/orololuwa/reimagined-robot/models"
	"github.com/orololuwa/reimagined-robot/repository"
)
```

2) We create a struct and an initialization function to hold the db repo and any other configs that we might need in our handlers:
```go
type Repository struct {
	User repository.UserRepo
}

var Repo *Repository

func NewHandler(db *sql.DB) {
	r := &Repository{
		User: repository.NewUserRepo(db),
	}
	Repo = r
}
```
we call the struct Repository because it's going to hold references to the database repositories and any other repository we might create and make available to it, like an in-memory db repository or even an app config. We can see that the initialization function initializes the User field with the `repository.NewUserRepo` function from the repository package.

3) Create the `CreateAUser` handler function to create a user:
```go
func (m *Repository) CreateAUser(w http.ResponseWriter, r *http.Request) {
	type userBody struct {
		FirstName string `json:"firstName"`
		LastName string `json:"lastName"`
		Email string `json:"email"`
		Password string `json:"password"`
	}

	var body userBody
	
	err := json.NewDecoder(r.Body).Decode(&body)
	if err != nil {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusInternalServerError)
		responseMap := map[string]interface{}{"message": "error decoding requset body",}

		jsonData, err := json.Marshal(responseMap)
		if err != nil {
			return
		}

		w.Write(jsonData)
		return
	}

	user := models.User{
		FirstName: body.FirstName,
		LastName: body.LastName,
		Email: body.Email,
		Password: body.Password,
	}
	
	id, err := m.User.CreateAUser(user)
	if err != nil {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusBadRequest)		
		responseMap := map[string]interface{}{"message": "error creating user",}

		jsonData, err := json.Marshal(responseMap)
		if err != nil {
			return
		}

		w.Write(jsonData)
		return
	}

	response := map[string]interface{}{"message": "user created successfully", "data": id}
	jsonResponse, err := json.Marshal(response)
	if err != nil {
		http.Error(w, "Failed to marshal response", http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	w.Write(jsonResponse)
}
```
the handler function above receives the HTTP response and a pointer to the request. First, we decode the body of the request and pass it to the CreateAUser function made available to us from the User field. We write error messages to the client if there are any or we write the data.

4) Create the `GetAUser` handler to get a user by id:
```go
func (m *Repository) GetAUser(w http.ResponseWriter, r *http.Request){
	exploded := strings.Split(r.RequestURI, "/")
	id, err := strconv.Atoi(exploded[2])
	if err != nil {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusInternalServerError)
		responseMap := map[string]interface{}{"message": "error decoding id",}

		jsonData, err := json.Marshal(responseMap)
		if err != nil {
			return
		}

		w.Write(jsonData)
		return
	}
	
	user, err := m.User.GetAUser(id)
	if err != nil {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusNotFound)
		responseMap := map[string]interface{}{"message": "error getting user",}

		jsonData, err := json.Marshal(responseMap)
		if err != nil {
			return
		}

		w.Write(jsonData)
		return
	}

	response := map[string]interface{}{"message": "user created successfully", "data": user}
	jsonResponse, err := json.Marshal(response)
	if err != nil {
		http.Error(w, "Failed to marshal response", http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	w.Write(jsonResponse)
}
```
In the handler function above, we decode the id of the user from the URL param, using the function from the chi router. You can install chi router with `go get github.com/go-chi/chi/v5`. We pass the id to the GetAUser function made available to us from the User field and write the data to the client.

### The `main.go` file.
1) First we create a `main.go` file, declare the package main, and import the other packages that would be used in the file.
```go
package main

import (
	"database/sql"
	"fmt"
	"log"
	"net/http"

	"github.com/go-chi/chi/v5"
	"github.com/go-chi/chi/v5/middleware"
	_ "github.com/jackc/pgx/v5/stdlib"
	"github.com/orololuwa/reimagined-robot/handlers"
)

```
2) We declare a run function where we create a connection to the database, initialize our handlers package with the NewHandler function and declare our routes.To open a connection to the database, I'm using a pgx driver called jackc and you can install it using `go get github.com/jackc/pgx/v5`
At the top of the file, we import the router, the pgx driver with a blank import, and, the other packages used in the main.go file.
```go
func run()(*sql.DB, *chi.Mux, error){
	dbHost := "localhost"
	dbPort := "5432"
	dbName := "bookings"
	dbUser := "orololuwa"
	dbPassword := ""
	dbSSL := "disable"

	// Connecto to DB
	log.Println("Connecting to dabase")
	dsn := fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s sslmode=%s", dbHost, dbPort, dbName, dbUser, dbPassword, dbSSL)

	db, err := sql.Open("pgx", dsn)
	if err != nil {
		log.Fatal("Cannot conect to database: Dying!", err)
	}
	if err = db.Ping(); err != nil {
		panic(err)
	}
	log.Println("Connected to database")

	handlers.NewHandler(db)
	router := chi.NewRouter()

	router.Use(middleware.Logger)

	router.Post("/user", handlers.Repo.CreateAUser)
	router.Get("/user/{id}", handlers.Repo.GetAUser)
	return db, router, nil
}
```
Ideally, you should store your database connection details as environment variables.
3) Lastly, we create the main function, call the run function inside of it, and serve our application.
```go
const portNumber = ":8080"
func main(){
	db, route, err := run()
	if (err != nil){
		log.Fatal(err)
	}
	defer db.Close()

	fmt.Println(fmt.Sprintf("Staring application on port %s", portNumber))

	srv := &http.Server{
		Addr: portNumber,
		Handler: route,
	}

	err = srv.ListenAndServe()
	if err != nil {
		log.Fatal(err)
	}
}
```
Now, when we run `go run main.go` and test on Postman, we get the following result:

![Post request to create a user](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/131hggtb8d7o91913luc.png) 
![Get Request to get a user by Id](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zc7bd1s1ye7r6u78nrbw.png)

### Writing Tests
Here comes the fun part 🤩.
The first thing we want to do is create mock implementations of our database logic functions, the UserRepo functions in this case. So, we would create a new file `test-repo.go` under the package repository with the following steps.
1) Declare the package repository and import the necessary packages.
```go
package repository

import (
	"database/sql"
	"errors"

	"github.com/orololuwa/reimagined-robot/models"
)
```
2) Next, we create a test repo struct and an initialization function to implement the UserRepo interface we previously had.
```go
type testUserDBRepo struct {
	DB *sql.DB
}

func NewUserTestingDBRepo() UserRepo {
	return &testUserDBRepo{
	}
}
```
3) Next, we create the mock implementation of the UserRepo functions as thus;
```go
func (m *testUserDBRepo) CreateAUser(user models.User) (int, error){
	var newId int

	if user.Password == "invalid"{
		return newId, errors.New("CreateAUser: DB repo fail")
	}

	return newId, nil
}

func (m *testUserDBRepo) GetAUser(id int)(models.User, error){
	var user models.User

	if id == 0{
		return user, errors.New("GetAUser: DB repo fail")
	}

	return user, nil
}
```
In the CreateAUser function above, we create a scenario where the function returns an error. Likewise, the GetAUser function.
Now that we've mocked our UserRepo functions, we would go ahead and write tests for the handlers.

**The Handlers**
In the handlers package/folder, we would create a file called `setup_test.go` to set up what we would need in the actual test. In this file, we would go ahead and initialize a test db repo with the mocked functions. Your test files have to end with the suffix `_test.go` to allow the go compiler to see it in the testing environment. In the file, we would put the following code;
```go
package handlers

import (
	"os"
	"testing"

	"github.com/orololuwa/reimagined-robot/repository"
)

func NewTestingHandler() {
	r := &Repository{
		User: repository.NewUserTestingDBRepo(),
	}
	Repo = r
}

func TestMain(m *testing.M){
	NewTestingHandler()
	os.Exit(m.Run())
}
```
In the code above, the TestMain function is going to run before any other test functions we'd create later. It receives a pointer to testing.M, which makes available to us, a Run function.
Next, we would go ahead and install an external package for creating mocking data.
```bash
go get -u github.com/go-faker/faker/v4
```

Now we create a new file; `handlers_test.go` where we write the tests for the handlers as thus:
1) Declare the package and import every other package that would be used.
```go
package handlers

import (
	"bytes"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/go-faker/faker/v4"
)
```
2) Next, we create a TestCreateAUser function to test the CreateAUser handler.
```go
func TestCreateAUser(t *testing.T){
	type UserBody struct {
		FirstName string `json:"firstName" faker:"first_name"`
		LastName string `json:"lastName" faker:"last_name"`
		Email string `json:"email" faker:"email"`
		Password string `json:"password" faker:"password"`
	}

	body := UserBody{}
	err := faker.FakeData(&body)
	if err != nil {
		t.Log(err)
	}

	jsonBody, err := json.Marshal(body)
    if err != nil {
        t.Log("Error:", err)
        return
    }

	// Test for success
	req, _ := http.NewRequest("POST", "/user", bytes.NewBuffer(jsonBody))
	rr := httptest.NewRecorder()

	handler := http.HandlerFunc(Repo.CreateAUser)
	handler.ServeHTTP(rr, req)

	if rr.Code != http.StatusCreated {
		t.Errorf("CreateAUser handler returned wrong response code: got %d, wanted %d", rr.Code, http.StatusCreated)
	}

	// Test for missing request body
	req, _ = http.NewRequest("POST", "/user", bytes.NewBuffer([]byte(``)))
	rr = httptest.NewRecorder()

	handler = http.HandlerFunc(Repo.CreateAUser)
	handler.ServeHTTP(rr, req)

	if rr.Code != http.StatusInternalServerError {
		t.Errorf("CreateAUser handler returned wrong response code for missing body: got %d, wanted %d", rr.Code, http.StatusInternalServerError)
	}

	// Test for failed DB insert
	body.Password = "invalid"
	jsonBody, err = json.Marshal(body)
    if err != nil {
        t.Log("Error:", err)
        return
    }

	req, _ = http.NewRequest("POST", "/user", bytes.NewBuffer(jsonBody))
	rr = httptest.NewRecorder()

	handler = http.HandlerFunc(Repo.CreateAUser)
	handler.ServeHTTP(rr, req)

	if rr.Code != http.StatusBadRequest {
		t.Errorf("CreateAUser handler returned wrong response code for failed UserRepo db function: got %d, wanted %d", rr.Code, http.StatusBadRequest)
	}
}
```
In the snippet above, we declare the Userbody struct with struct tags including the JSON and Faker metadata. We create mock data into the body variable with the Faker package and encode it into JSON into the jsonBody variable. We create instances of the HTTP Request and ResponseRecorder and test them according to different cases. In this, we tested for cases of a successful request, missing request body, and failed DB operation.
3) Still in the same file, we create a TestGetAUser function to test the GetAUser handler
```go
func TestGetAUser(t *testing.T){
	// test for success
	req, _ := http.NewRequest("GET", "/user/1", nil)
	req.Header.Set("Content-Type", "application/json")
	req.RequestURI = "/user/1"	
	res := httptest.NewRecorder()

	handler := http.HandlerFunc(Repo.GetAUser)	
	handler.ServeHTTP(res, req)

	if res.Code != http.StatusOK {
		t.Errorf("GetAUser handler returned wrong response code: got %d, wanted %d", res.Code, http.StatusOK)
	}

	// test valid id in the path variable
	req, _ = http.NewRequest("GET", "/room", nil)
	req.Header.Set("Content-Type", "application/json")
	req.RequestURI = "/room/one"

	res = httptest.NewRecorder()

	handler = http.HandlerFunc(Repo.GetAUser)

	handler.ServeHTTP(res, req)

	if res.Code != http.StatusInternalServerError {
		t.Errorf("GetAUser handler returned wrong response code for invalid query param 'id': got %d, wanted %d", res.Code, http.StatusInternalServerError)
	}

	// test for failed db operation
	req, _ = http.NewRequest("GET", "/room", nil)
	req.Header.Set("Content-Type", "application/json")	
	req.RequestURI = "/room/0"


	res = httptest.NewRecorder()

	handler = http.HandlerFunc(Repo.GetAUser)

	handler.ServeHTTP(res, req)

	if res.Code != http.StatusNotFound {
		t.Errorf("GetAUser handler returned wrong response code for failed UserRepo function: got %d, wanted %d", res.Code, http.StatusNotFound)
	}
}
```
In the snippet above, we create instances of the HTTP Request and ResponseRecorder and test for cases where the request is successful, the id is missing or invalid, and a failed DB operation.

**The main file**
Testing the main file is quite easy. We create a `main_test.go` file in the root under the main package and try to run the "run" function as thus:
```go
package main

import "testing"

func TestRun(t *testing.T){
	_, _, err := run();
	if err != nil {
		t.Error("failed run")
	}
}
```

**Running the test commands**
There are different ways to run the test.
1) You can do so with a simple test command without any flags
```bash
go test ./...
```
![Result of running go test ./...](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e0rmd5g6oyipv16ug18q.png)

2) You can run it with the -v (verbose) flag to get more details about which test functions passed or failed.
```bash
go test -v ./...
```
![Result of running go test -v ./...](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/16da4340qhh1pgytazvr.png)

3) Running it with -cover flag to get the percentage of obvious test cases that were covered in the test.
```bash
go test -cover ./...
```
![Result of running go test -cover ./...](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/725ulz97kuh6en42y0hs.png)

4) Running it to get the coverage output as an HTML file.
```bash
go test -coverprofile=coverage.out  ./... && go tool cover -html=coverage.out
```
![Part result of running go test -coverprofile=coverage.out  ./... && go tool cover -html=coverage.out](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zft9uc9atfao9ihruaxf.png)

### Conclusion
In this article, we explored a basic way to set up handlers and write proper tests. You can build on it as your code gets robust and complicated. The source code is available on my [Github](https://github.com/Orololuwa/reimagined-robot)