Advanced Golang

API Development Project

Testing API

In this article, we will demonstrate how to write test cases for our API endpoints while making necessary changes to our application code. These improvements improve flexibility, testability, and ensure that our endpoints return the correct HTTP status codes.


Update Application Initialization

Before testing, we need to modify the application initialization method to dynamically accept database credentials (DB user, DB password, and DB name) instead of relying on hardcoded constants. This change allows us to specify different credentials during testing.

Below is the original initialization method:

func (app *App) Initialise() error {
	connectionString := fmt.Sprintf(":%v@tcp(127.0.0.1:3306)/%v", DbUser, DbPassword, DbName)
	var err error
	app.DB, err = sql.Open("mysql", connectionString)
	if err != nil {
		return err
	}

	app.Router = mux.NewRouter().StrictSlash(true)
	app.handleRoutes()
	return nil
}

func (app *App) Run(address string) {
	log.Fatal(http.ListenAndServe(address, app.Router))
}

To enable passing credentials via parameters, modify the Initialise method as follows:

type App struct {
	Router *mux.Router
	DB     *sql.DB
}

func (app *App) Initialise(DbUser string, DbPassword string, DbName string) error {
	connectionString := fmt.Sprintf(":%s@tcp(127.0.0.1:3306)/%s", DbUser, DbPassword, DbName)
	var err error
	app.DB, err = sql.Open("mysql", connectionString)
	if err != nil {
		return err
	}

	app.Router = mux.NewRouter().StrictSlash(true)
	app.handleRoutes()
	return nil
}

func (app *App) Run(address string) {
	log.Fatal(http.ListenAndServe(address, app.Router))
}

In your main.go file, initialize the application by passing the database constants:

package main

func main() {
	app := App{}
	app.Initialise(DbUser, DbPassword, DbName)
	app.Run("localhost:10000")
}

Note

Using dynamic credentials improves flexibility by letting you create separate configurations for production and testing environments.


Update Route Handlers

Modify Create Product Handler

The createProduct handler originally returns an HTTP status code of 200 (OK) when a product is successfully created. However, according to RESTful best practices, a 201 (Created) response is more appropriate for resource creation. Below is the existing implementation:

func (app *App) createProduct(w http.ResponseWriter, r *http.Request) {
	var p product
	err := json.NewDecoder(r.Body).Decode(&p)
	if err != nil {
		sendError(w, http.StatusBadRequest, "Invalid request payload")
		return
	}
	err = p.createProduct(app.DB)
	if err != nil {
		sendError(w, http.StatusInternalServerError, err.Error())
		return
	}
	sendResponse(w, http.StatusOK, p)
}

func (app *App) updateProduct(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	_, err := strconv.Atoi(vars["id"])
	if err != nil {
		// handle error appropriately
	}
}

Update the createProduct handler to return a 201 (Created) status code:

func (app *App) createProduct(w http.ResponseWriter, r *http.Request) {
	var p product
	err := json.NewDecoder(r.Body).Decode(&p)
	if err != nil {
		sendError(w, http.StatusBadRequest, "Invalid request payload")
		return
	}
	err = p.createProduct(app.DB)
	if err != nil {
		sendError(w, http.StatusInternalServerError, err.Error())
		return
	}
	sendResponse(w, http.StatusCreated, p)
}

Additional Route Handler Implementations

Below are implementations for other route handlers used to retrieve products:

func (app *App) getProducts(w http.ResponseWriter, r *http.Request) {
	products, err := getProducts(app.DB)
	if err != nil {
		sendError(w, http.StatusInternalServerError, err.Error())
		return
	}
	sendResponse(w, http.StatusOK, products)
}

func (app *App) getProduct(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	key, err := strconv.Atoi(vars["id"])
	if err != nil {
		sendError(w, http.StatusBadRequest, "invalid product ID")
		return
	}

	p := product{ID: key}
	err = p.getProduct(app.DB)
	if err != nil {
		switch err {
		// include switch cases as needed to handle different error types
		}
	}
}

Testing Setup

To test the API, create a separate test file (e.g., app_test.go). In this file, declare your application variable and use the test main function to initialize the application for testing. The test main function, introduced in Go 1.4, is executed to set up and run all tests within the package. Note that the test main function should appear only once per package.

Below is an example of initializing your app variable in the test main function:

package main

import (
	"log"
	"testing"
)

var a App

func TestMain(m *testing.M) {
	err := a.Initialise(DbUser, DbPassword, "test")
	if err != nil {
		log.Fatal("Error occurred while initialising the database")
	}
	// Additional setup (e.g., creating a test table) can be done here

	m.Run()
}

In this testing setup, notice how we use the same constants for DB user and DB password but specify a different database name ("test") to prevent interference with production data. For improved code organization, consider isolating test table creation into a dedicated function.

Testing Best Practices

Using a dedicated test database ensures that your testing operations do not affect live production data. Always isolate your testing environment from production.


Summary

This article explained:

  • How to modify the application initialization to dynamically accept database credentials.
  • Updating the createProduct handler to return an HTTP 201 status code for resource creation.
  • Setting up a dedicated test environment to safely run API tests.

By following this improved structure and flow, you ensure that your API is more flexible, testable, and follows best practices for HTTP response codes.

For additional details, you can refer to these useful resources:

Watch Video

Watch video content

Previous
Demo Delete method