1. Posts
  2. Test driven development in Go

Test driven development in Go

Nulab

Nulab

July 29, 2016

You may have read our recent announcement that we are currently rewriting Cacoo Editor UI to HTML 5, but that’s not all we have in the works; we have also decided to migrate our current API backend written in Java to Go.

Switching from Java to Go is surprisingly easy because the languages have a lot in common. However, without the wealth of online resources typically available for Java, we’ve had to dive into more Go documentation, source code, and open projects to answer some of our questions. We decided to share some things we’ve learned.

This post will focus on how to implement a simple REST API for managing tasks using Test Driven Development (TDD). We will create an API that provides two services: saveTask and getPendingTasks. Then, we will implement our server and API services.

Test Driven Development

TDD is a simple concept: you define a test for a function you haven’t implemented yet and then write the minimal code to achieve the expected behavior. We’ll create examples for these first few steps.

First, create two files in the package store: task.go and task_test.go. Then, create your first test.

package store

import (
  "reflect"
  "testing"
)

func TestGetPendingTasks(t *testing.T) {
  t.Log("GetPendingTasks")

  ds := Datastore{
    tasks: []Task{
      {1, "Do housework", true},
            {2, "Buy milk", false},
    },
  }

  want := []Task{ds.tasks[1]}

  t.Log("should return the tasks which need to be completed")

  if got := ds.GetPendingTasks(); !reflect.DeepEqual(got, want) {
    t.Errorf("Got %v wanted %v", got, want)
  }
}

The test fails because task and datastore are undefined. We must define our task model, datastore, and function, and our datastore must keep a list of all the tasks. The GetPendingTasks function should return the list of the tasks which are not completed.

package store

// Task job to be done or completed
type Task struct {
  ID    int    // identifier of the task
  Title string // Title of the task
  Done  bool   // If task is completed or not
}

// Datastore manages a list of tasks stored in memory
type Datastore struct {
  tasks  []Task
}

// GetPendingTasks returns all the tasks which need to be done
func (ds *Datastore) GetPendingTasks() []Task {
  var pendingTasks []Task
  for _, task := range ds.tasks {
    if !task.Done {
      pendingTasks = append(pendingTasks, task)
    }
  }
  return pendingTasks
}

Run the test again, and this time, it passes.

Next, let’s save a task. The task can be a new task or an existing task. Let’s begin with a test for saving a new task.

func TestSaveNewTask(t *testing.T) {
    t.Log("SaveTask")

  ds := Datastore{}

  task := Task{Title: "Buy milk"}

  want := []Task{
    {1, "Buy milk", false},
  }

    t.Log("should save the new task in the store")
  ds.SaveTask(task)

  if !reflect.DeepEqual(ds.tasks, want) {
    t.Errorf("=> Got %v wanted %v", ds.tasks, want)
  }
}

The test fails. The SaveTask function must be implemented. Our Datastore will attribute a new ID to each new task, so the last given ID should be stored in a field.

// Datastore manages a list of task in memory
type Datastore struct {
  tasks  []Task
  lastID int // lastID is incremented for each new stored task
}

// SaveTask should store the task in the datastore
func (ds *Datastore) SaveTask(task Task) {
  ds.lastID++
  task.ID = ds.lastID
  ds.tasks = append(ds.tasks, task)
}

Now, all the tests pass.

If a task already exists, it should be updated. Let’s define a test for this feature.

func TestSaveAndUpdateExistingTask(t *testing.T) {
    t.Log("SaveTask")
  ds := Datastore{
    tasks: []Task{
      {1, "Buy milk", false},
    },
  }

  want := []Task{
    {1, "Buy milk", true},
  }

  task := Task{1, "Buy milk", true}

    t.Log("should update the existing task in the store")
  ds.SaveTask(task)

  if !reflect.DeepEqual(ds.tasks, want) {
    t.Errorf("=> Got %v wanted %v", ds.tasks, want)
  }
}

This feature is not yet implemented, so the test fails. We therefore need to modify the SaveTask function, and check if the task to be saved is a new task or an existing task. If the task is already present in our datastore, it will be replaced by its new version.

// SaveTask should store the task in the datastore if the task
// does not exist else update it
func (ds *Datastore) SaveTask(task Task) {
  if task.ID == 0 {
    ds.lastID++
    task.ID = ds.lastID
    ds.tasks = append(ds.tasks, task)
    return
  }

  for i, t := range ds.tasks {
    if t.ID == task.ID {
      ds.tasks[i] = task
      return
    }
  }
}

This time, all tests pass.

Table-Driven Test

You might notice that the tests for saving a new task and updating an existing task are very similar. Both are testing the SaveTask function. How can we avoid code duplication?

We could create a helper function, but another solution exists in Go: refactoring our tests to a Table-Driven Test.

First, create table entries that contain all the data needed for our test cases.

var saveTaskTests = []struct {
  name string
  ds   *Datastore
  task Task
  want []Task
}{
  {
    name: "should save the new task in the datastore",
    ds:   &Datastore{},
    task: Task{Title: "Buy milk"},
    want: []Task{
      {1, "Buy milk", false},
    },
  },
  {
    name: "should update the existing task in the store",
    ds: &Datastore{
      tasks: []Task{
        {1, "Buy milk", false},
      },
    },
    task: Task{1, "Buy milk", true},
    want: []Task{
      {1, "Buy milk", true},
    },
  },
}

Then, add a new test. The test should iterate through all table entries and perform the test cases.

func TestSaveTask(t *testing.T) {
  t.Log("SaveTask")

  for _, testcase := range saveTaskTests {
    t.Log(testcase.name)
    testcase.ds.SaveTask(testcase.task)

    if !reflect.DeepEqual(testcase.ds.tasks, testcase.want) {
      t.Errorf("=> Got %v wanted %v", testcase.ds.tasks, testcase.want)
    }
  }
}

Once everything looks good, both TestSaveNewTask and TestSaveAndUpdateTask can be removed. We don’t need them anymore.

One more test case still needs to be tested. If the task ID doesn’t exist an error will be returned. Let’s create a new entry for our table and add an error field to our anonymous struct.

var saveTaskTests = []struct {
  name string
  ds   *Datastore
  task Task
  want []Task
  err  error
}{
  {
    name: "should save the new task in the datastore",
    ds:   &Datastore{},
    task: Task{Title: "Buy milk"},
    want: []Task{
      {1, "Buy milk", false},
    },
  },
  {
    name: "should update the existing task in the store",
    ds: &Datastore{
      tasks: []Task{
        {1, "Buy milk", false},
      },
    },
    task: Task{1, "Buy milk", true},
    want: []Task{
      {1, "Buy milk", true},
    },
  },
  {
    name: "should return an error when task ID does not exist",
    ds:   &Datastore{},
    task: Task{1, "Buy milk", true},
    err:  ErrTaskNotFound,
  },
}

The test fails. The SaveTask function should have returned an error when the task could not be found in the datastore.

import "errors"

// ErrTaskNotFound is returned when a Task ID is not found
var ErrTaskNotFound = errors.New("Task was not found")

// SaveTask should store the task in the datastore if the task
// is new else update it. A Task Not Found error is returned
// when the task ID does not exist
func (ds *Datastore) SaveTask(task Task) error {
  if task.ID == 0 {
    ds.lastID++
    task.ID = ds.lastID
    ds.tasks = append(ds.tasks, task)
    return nil
  }

  for i, t := range ds.tasks {
    if t.ID == task.ID {
      ds.tasks[i] = task
      return nil
    }
  }

  return ErrTaskNotFound
}

This time, it works! All tests have passed.

Create a Server

Go comes with a built-in HTTP server and a simple HTTP request multiplexer, so we just need to define a route, a handler, and start the server.

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {       // HandleFunc from http
        w.Write([]byte("Hello World!"))
    })
    http.ListenAndServe(":8080", nil) // Start the server
}

HTTP Request Handlers

We will develop three HTTP request handlers:

  • Get pending tasks,
  • Add a task, and
  • Update a task.

Get Pending tasks

First, we need to create a test for getting the pending tasks from our datastore and returning them as a JSON response. As we write our test, we find out that we need to access our datastore and insert our test tasks. Let’s define a global variable for our datastore:

var ds = &store.Datastore{}

The tasks slice of our datastore is private. We should change its visibility to public so our testing tasks can be directly inserted:

// Datastore manages a list of task in memory
type Datastore struct {
  Tasks  []Task
  lastID int // lastID is incremented for each new stored task
}

Finally, our test can be completed:

func TestGetPendingTasks(t *testing.T) {

  t.Log("GetPendingTasks")

  t.Log("should return pending tasks as JSON")

  rec := httptest.NewRecorder()

  req, _ := http.NewRequest(http.MethodGet, "/tasks/pending", nil)

  ds = &store.Datastore{
    Tasks: []store.Task{
      {1, "Do housework", false},
      {2, "Buy milk", false},
    },
  }

  GetPendingTasks(rec, req)

  if rec.Code != http.StatusOK {
    t.Errorf("KO => Got %d wanted %d", rec.Code, http.StatusOK)
  }

  want := "[{\"id\":1,\"title\":\"Do housework\",\"done\":false},{\"id\":2,\"title\":\"Buy milk\",\"done\":false}]"
  if got := rec.Body.String(); got != want {
    t.Errorf("KO => Got %s wanted %s", got, want)
  }
}

Our handler retrieves the pending tasks from our datastore, encodes, and returns them in the HTTP response body.

// GetPendingTasks returns pending tasks as a JSON response
func GetPendingTasks(w http.ResponseWriter, r *http.Request) {

  t := ds.GetPendingTasks()

  j, _ := json.Marshal(t)

  w.Header().Set("Content-Type", "application/json")
  w.Write(j)
}

Run the test, and you’ll see it fails. Our field names are not in lowercase. We didn’t add field tags to our Task struct, so the marshaller used the same letter case as our struct. Let’s fix it.

// Task job to be done or completed
type Task struct {
  ID    int    `json:"id"`    // identifier of the task
  Title string `json:"title"` // Title of the task
  Done  bool   `json:"done"`  // If task is completed or not
}

Now, the test passes.

Interface

Our Datastore struct should not have been modified for the purpose of testing only. The task slice should remain private and not be accessed directly. Our handler should be tested by isolating it from our data store implementation. At first, let’s revert our Datastore changes.

// Datastore manages a list of task in memory
type Datastore struct {
  tasks  []Task
  lastID int // lastID is incremented for each new stored task
}

A better solution is to create an interface.

// Store defines the datastore services
type Store interface {
  GetPendingTasks() []store.Task
}

And force our global variable to implement our Store interface.

var ds Store = &store.Datastore{}

Now our GetPendingTasks can be mocked by creating a new struct that implements our interface.

type mockedStore struct{}

func (ms *mockedStore) GetPendingTasks() []store.Task {
  return []store.Task{
    {1, "Do housework", false},
    {2, "Buy milk", false},
  }
}

Finally, our test should be modified and should use our newly created mock.

func TestGetPendingTasks(t *testing.T) {

  t.Log("GetPendingTasks")

  t.Log("should return pending tasks as JSON")

  rec := httptest.NewRecorder()

  req, _ := http.NewRequest(http.MethodGet, "/tasks/pending", nil)

  // The datastore is restored at the end of the test
  defer func() { ds = &store.Datastore{} }()

  ds = &mockedStore{}

  GetPendingTasks(rec, req)

  if rec.Code != http.StatusOK {
    t.Errorf("KO => Got %d wanted %d", rec.Code, http.StatusOK)
  }

  want := "[{\"id\":1,\"title\":\"Do housework\",\"done\":false},{\"id\":2,\"title\":\"Buy milk\",\"done\":false}]"
  if got := rec.Body.String(); got != want {
    t.Errorf("KO => Got %s wanted %s", got, want)
  }
}

Nice! The tests still pass.

Add a task

Now we implement our second handler for adding a task.  If the task is added, our REST service should return the HTTP status 201 Created:

func TestAddTask(t *testing.T) {

  t.Log("AddTask")

  t.Log("should add new task from JSON")

  rec := httptest.NewRecorder()
  req, _ := http.NewRequest(http.MethodPost, "/tasks", bytes.NewBuffer([]byte(`{"Title":"Buy bread for breakfast."}`)))

  defer func() { ds = &store.Datastore{} }()

  ds = &mockedStore{}

  AddTask(rec, req)

  wantCode := http.StatusCreated
  if rec.Code != wantCode {
    t.Errorf("KO => Got %d wanted %d", rec.Code, wantCode)
  }
}

And then create our handler. The JSON in the request body is decoded and the status 201 Created is written to the response header:

// AddTask handles POST requests on /tasks.
// Return 201 if the task could be created
func AddTask(w http.ResponseWriter, r *http.Request) {
  var t store.Task

  json.NewDecoder(r.Body).Decode(&t)

  w.WriteHeader(http.StatusCreated)
}

All tests pass. Some error checks need to be added now. If the JSON cannot be decoded, the status 400 Bad Request should be answered. Previously, we refactored our saveTask test to a Table-Driven Test to avoid code duplication. Let’s do same before implementing our new test case.

var addTaskTests = []struct {
  name string
  body []byte
  want int
}{
  {
    name: "should add new task from JSON",
    body: []byte(`{"Title":"Buy bread for breakfast."}`),
    want: http.StatusCreated,
  },
}

func TestAddTask(t *testing.T) {

  t.Log("AddTask")

  for _, testcase := range addTaskTests {

    t.Log(testcase.name)

    rec := httptest.NewRecorder()
    req, _ := http.NewRequest(http.MethodPost, "/tasks", bytes.NewBuffer(testcase.body))

    defer func() { ds = &store.Datastore{} }()

    ds = &mockedStore{}

    AddTask(rec, req)

    if rec.Code != testcase.want {
      t.Errorf("KO => Got %d wanted %d", rec.Code, testcase.want)
    }
  }
}

We run the tests again to be sure they still pass, and they do. Next, we add our test for handling decoding errors.

var addTaskTests = []struct {
  name string
  body []byte
  want int
}{
  {
    name: "should add new task from JSON",
    body: []byte(`{"Title":"Buy bread for breakfast."}`),
    want: http.StatusCreated,
  },
  {
    name: "should return bad argument when JSON could not be handled",
    body: []byte(""),
    want: http.StatusBadRequest,
  },
}

And add our error check to our implementation.

// AddTask handles requests for adding a new task.
// Return 201 if the task could be created
// Return 400 when JSON could not be decoded into a task
func AddTask(w http.ResponseWriter, r *http.Request) {
  var t store.Task

  if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
    http.Error(w, err.Error(), http.StatusBadRequest)
    return
  }

  w.WriteHeader(http.StatusCreated)
}

Another check should be added when an error is returned from our datastore. Our service should return the status 400 Bad Request. To be able to test this case, our mock should implement the Datastore SaveTask function. At first, our SaveTask function signature is added to our Store interface.

// Store defines the datastore services
type Store interface {
  GetPendingTasks() []store.Task
  SaveTask(task store.Task) error
}

Then we implement it in our mock struct. The SaveTask should return an error for our new test case only and not for the previous ones. We add a function field to our mock struct. The test will call this function only if the function was implemented in our mock.

type mockedStore struct {
  SaveTaskFunc func(task store.Task) error
}

func (ms *mockedStore) SaveTask(task store.Task) error {
  if ms.SaveTaskFunc != nil {
    return ms.SaveTaskFunc(task)
  }
  return nil
}

The saveFunc is added to our anonymous struct table and implemented in our datastore error test case.

var addTaskTests = []struct {
  name     string
  saveFunc func(task store.Task) error
  body     []byte
  want     int
}{
  {
    name: "should add new task from JSON",
    body: []byte(`{"Title":"Buy bread for breakfast."}`),
    want: http.StatusCreated,
  },
  {
    name: "should response bad argument when JSON could not be handled",
    body: []byte(""),
    want: http.StatusBadRequest,
  },
  {
    name: "should response bad argument when datastore returns an error",
    saveFunc: func(task store.Task) error {
      return errors.New("datastore error")
    },
    body: []byte(`{"Title":"Buy bread for breakfast."}`),
    want: http.StatusBadRequest,
  },
}

Our mock should use the saveFunc from the test case.

func TestAddTask(t *testing.T) {

  t.Log("AddTask")

  for _, testcase := range addTaskTests {

    t.Log(testcase.name)

    rec := httptest.NewRecorder()
    req, _ := http.NewRequest(http.MethodPost, "/tasks", bytes.NewBuffer(testcase.body))

    defer func() { ds = &store.Datastore{} }()

    ds = &mockedStore{
      SaveTaskFunc: testcase.saveFunc,
    }

    AddTask(rec, req)

    if rec.Code != testcase.want {
      t.Errorf("KO => Got %d wanted %d", rec.Code, testcase.want)
    }
  }
}

Modify the handler and return a bad request answer when an error is returned from the data store.

// AddTask handles requests for adding a new task.
// Return 201 if the task could be created
// Return 400 when JSON could not be decoded into a task or
// datastore returned an error
func AddTask(w http.ResponseWriter, r *http.Request) {
  var t store.Task

  if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
    http.Error(w, "JSON malformed", http.StatusBadRequest)
    return
  }

  if err := ds.SaveTask(t); err != nil {
    http.Error(w, err.Error(), http.StatusBadRequest)
    return
  }

  w.WriteHeader(http.StatusCreated)
}

Another case needs to be verified: If our the title is empty, a 400 Bad Request should be returned.

var addTaskTests = []struct {
  name     string
  saveFunc func(task store.Task) error
  body     []byte
  want     int
}{
  {
    name: "should add new task from JSON",
    body: []byte(`{"Title":"Buy bread for breakfast."}`),
    want: http.StatusCreated,
  },
  {
    name: "should response bad argument when JSON could not be handled",
    body: []byte(""),
    want: http.StatusBadRequest,
  },
  {
    name: "should response bad argument when datastore returns an error",
    saveFunc: func(task store.Task) error {
      return errors.New("datastore error")
    },
    body: []byte(`{"Title":"Buy bread for breakfast."}`),
    want: http.StatusBadRequest,
  },
  {
    name: "should response bad argument when task title is emtpy",
    body: []byte(`{"Title":""}`),
    want: http.StatusBadRequest,
  },
}

Finally, we add it to our implementation.

// AddTask handles requests for adding a new task.
// Return 201 if the task could be created
// Return 400 when JSON could not be decoded into a task or
// datastore returned an error or task title is empty
func AddTask(w http.ResponseWriter, r *http.Request) {
  var t store.Task

  if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
    http.Error(w, err.Error(), http.StatusBadRequest)
    return
  }

  if t.Title == "" {
    http.Error(w, "Title is missing", http.StatusBadRequest)
    return
  }

  if err := ds.SaveTask(t); err != nil {
    http.Error(w, err.Error(), http.StatusBadRequest)
    return
  }

  w.WriteHeader(http.StatusCreated)
}

Update a task

This handler is quite similar to our AddTask handler, so the code snippets will be provided without explanation.

var updateTaskTests = []struct {
  name     string
  saveFunc func(task store.Task) error
  body     []byte
  want     int

}{
  {
    name: "should response with a status 200 OK when the task was updated",
    body: []byte(`{"ID":1, "Title":"Buy bread for breakfast.", "Done":true }`),
    want: http.StatusOK,
  },
  {
    name: "should response with a status 400 Bad Request when JSON body could not be handle",
    body: []byte(""),
    want: http.StatusBadRequest,
  },
  {
    name: "should response with a status 400 Bad Request when the datastore returned an error",
    saveFunc: func(task store.Task) error {
      return errors.New("datastore error")
    },
    body: []byte(`{"ID":1, "Title":"Buy bread for breakfast.", "Done":true }`),
    want: http.StatusBadRequest,
  },
  {
    name: "should response with a status 400 Bad Request when task title is emtpy",
    body: []byte(`{"Title":""}`),
    want: http.StatusBadRequest,
  },
}

func TestUpdateTask(t *testing.T) {

  t.Log("UpdateTask")

  for _, testcase := range updateTaskTests {
    t.Logf(testcase.name)

    rec := httptest.NewRecorder()
    req, _ := http.NewRequest(http.MethodPost, "/tasks/1", bytes.NewBuffer(testcase.body))

    defer func() { ds = &store.Datastore{} }()

    ds = &mockedStore{
      SaveTaskFunc: testcase.saveFunc,
    }

    UpdateTask(rec, req)

    if rec.Code != testcase.want {
      t.Errorf("KO => Got %d wanted %d", rec.Code, testcase.want)
    }
  }
}
// UpdateTask handles requests for updating an existing task.
// Return 200 if the task could be modified
// Return 400 when JSON could not be decoded into a task or
// datastore returned an error or task title is empty
func UpdateTask(w http.ResponseWriter, r *http.Request) {

  var t store.Task

  if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
    http.Error(w, err.Error(), http.StatusBadRequest)
    return
  }

  if err:= validateTask(t); err != nil {
    http.Error(w, err.Error(), http.StatusBadRequest)
    return
  }

  if err := ds.SaveTask(t); err != nil {
    http.Error(w, err.Error(), http.StatusBadRequest)
    return
  }

  w.WriteHeader(http.StatusOK)
}

A small refactor was done. The task title check was extracted to a function, so it can be used in our AddTask and UpdateTask handlers.

func validateTask(t store.Task) error {
  if t.Title == "" {
    return errors.New("Title is missing")
  }
  return nil
}

Router

Finally, let’s define our routes:

  • /tasks/pending GET Return the pending tasks
  • /tasks POST Add a new task
  • /tasks/{id} PUT Update an existing task

Unfortunately, the default Server Mux cannot handle dynamic routes such as /tasks/{id}, so we’ll need to implement our own router. First, we create a Table-Driven test with different test cases for our routes.

package router

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

var routeTests = []struct {
  name      string
  routMethod string
  routPatn   string
  reqMethod  string
  reqURL	  string
  want      int
}{
  {
    name: "should response with a status 200 OK when a route and method match",
    routPatn: "/tasks",
    routMethod:http.MethodGet,
    reqURL:  "/tasks",
    reqMethod:http.MethodGet,
    want:http.StatusOK,
  },
  {
    name: "should response with a status 404 Not Found when HTTP method is different",
    routPatn: "/tasks",
    routMethod:http.MethodGet,
    reqURL:  "/tasks",
    reqMethod:http.MethodPost,
    want: http.StatusNotFound,
  },
  {
    name: "should response with a status 200 OK when a route match regex and method",
    routPatn: `/tasks/\d`,
    routMethod:http.MethodGet,
    reqURL:  "/tasks/1",
    reqMethod:http.MethodGet,
    want: http.StatusOK,
  },
  {
    name: "should response with a status 404 Not Found when route could not be found",
    routPatn: `/tasks\d`,
    routMethod:http.MethodPost,
    reqURL:  "/tasks/a",
    reqMethod:http.MethodPost,
    want: http.StatusNotFound,
  },
}

func TestRoute(t *testing.T) {

  t.Log("Router")

  for _, testcase := range routeTests {
    t.Logf(testcase.name)

    rec := httptest.NewRecorder()
    req, _ := http.NewRequest(testcase.reqMethod, testcase.reqURL, nil)

    r := Router{}

    r.HandleFunc(testcase.routPatn, testcase.routMethod, func(w http.ResponseWriter, r *http.Request) {
      w.WriteHeader(http.StatusOK)
    })

    r.ServeHTTP(rec, req)

    if rec.Code != testcase.want {
      t.Errorf("KO => Got %d wanted %d", rec.Code, testcase.want)
    }
  }
}

Our router contains a slice of routes. Routes are added to the router by passing a regex pattern, a handler, and an HTTP method as parameters to the HandleFunc. When a request is received, the server multiplexer will call the ServeHTTP function. This function loops over the routes slice and checks if a route matches the HTTP method and the regex pattern. If a route is found, the route handler is called else a status 404 Not Found is returned.

package router

import (
  "net/http"
  "regexp"
)

type Route struct {
  Pattern    *regexp.Regexp
  Handler    http.Handler
  HTTPMethod string
}

type Router struct {
  routes []*Route
}


func (r *Router) HandleFunc(pattern string, httpMethod string, f func(http.ResponseWriter, *http.Request)) {
  r.routes = append(r.routes, &Route{
      HTTPMethod:httpMethod,
      Pattern: regexp.MustCompile(pattern + "$"),
      Handler: http.HandlerFunc(f),
    })
}

func (rer *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  for _, route := range rer.routes {
    if route.HTTPMethod == r.Method && route.Pattern.MatchString(r.URL.Path) {
      route.Handler.ServeHTTP(w, r)
      return
    }
  }
  http.NotFound(w, r)
}

Wrapping up

Finally, we should declare our router, our routes, and start our server in our main function.

func main() {
  r := &router.Router{}
  r.HandleFunc("/tasks/pending", http.MethodGet, server.GetPendingTasks)
  r.HandleFunc("/tasks", http.MethodPost, server.AddTask)
  r.HandleFunc(`/tasks/\d`, http.MethodPut, server.UpdateTask)

  log.Fatal(http.ListenAndServe(":8080", r))
}

Voilà, the server is started and ready to serve the routes that were defined above. Requests should be sent to localhost:8080.

 

Final Thoughts

This concludes the development of a simple REST API using Test Driven Development in Go.

Go already includes everything we need to test our functions without using any frameworks. Table-Driven Tests are useful for avoiding code duplication and performing many test cases for a function.

Our router implementation could be more efficient, but it’s good enough for our example. If more complex routing or performance are needed, an advanced router, such as Gorilla Mux or HttpRouter, can be used.

The final source code can be found on GitHub.

The featured image is based on the Go mascot designed by Renee French.

Keywords

Related

Subscribe to our newsletter

Learn with Nulab to bring your best ideas to life