06Jan, 2021
ภาษา :
English
Share blog : 
06 January, 2021
English

Building a finance tracking REST API using Go with TDD - Part 2

By

4 mins read
Building a finance tracking REST API using Go with TDD - Part 2

Hola amigos! In this part we're going to complete budgets API, also I'm going to tell you in recent days I've come up with a great combination of tools to build REST APIs much more MVC-like than this that I'm going to write about in incoming posts.

I've just finished with just one single acceptance test for budgets API but here I going to declare various test cases should cover almost 100% of budgets API behaviors. So let's get started:

// budgets_test.go
package main
import (    "net/http"    "os"    "testing"    "github.com/azbshiri/common/test"    "github.com/go-pg/pg"    "github.com/go-pg/pg/orm"    "github.com/gorilla/mux"    "github.com/pquerna/ffjson/ffjson"    "github.com/stretchr/testify/assert")
var testServer *server
var badServer *server
func TestMain(m *testing.M) {    testServer = newServer(        pg.Connect(&pg.Options{            User:     "alireza",            Password: "alireza",            Database: "alireza_test",        }),        mux.NewRouter(),    )    badServer = newServer(        pg.Connect(&pg.Options{            User:     "not_found",            Password: "alireza",            Database: "alireza",        }),        mux.NewRouter(),    )        // Here we create a temporary table to store each test case        // data and follow isolation which would be dropped after.    testServer.db.CreateTable(&budget{}, &orm.CreateTableOptions{        Temp: true,    })    os.Exit(m.Run())}
func TestGetBudgets_EmptyResponse(t *testing.T) {    var body []budget    res, err := test.DoRequest(testServer, "GET", BudgetPath, nil)    ffjson.NewDecoder().DecodeReader(res.Body, &body)    assert.NoError(t, err)    assert.Len(t, body, 0)    assert.Equal(t, res.Code, http.StatusOK)}
func TestGetBudgets_NormalResponse(t *testing.T) {    var body []budget    budgets, err := CreateBudgetListFactory(testServer.db, 10)    assert.NoError(t, err)    res, err := test.DoRequest(testServer, "GET", BudgetPath, nil)    assert.Equal(t, http.StatusOK, res.Code)    assert.NoError(t, err)    ffjson.NewDecoder().DecodeReader(res.Body, &body)    assert.Len(t, body, 10)    assert.Equal(t, budgets, &body)}
func TestGetBudgets_DatabaseError(t *testing.T) {    var body Error    res, err := test.DoRequest(badServer, "GET", BudgetPath, nil)    ffjson.NewDecoder().DecodeReader(res.Body, &body)    assert.NoError(t, err)    assert.Equal(t, DatabaseError, &body)    assert.Equal(t, http.StatusInternalServerError, res.Code)}

I've defined three test cases normal response, empty response and when there's a database error that we've simulated using a non-existing database username to make database unavailable. Also as you noticed there's a DatabaseErrorconstant which assert to make sure errors are readable. I've created a errors.go file in which there are a new error type called Error as below:

package main
import "net/http"
type Error struct {    Message string `json:"message"`    Status  int    `json:"status"`}
func Err(message string, status int) *Error {    return &Error{message, status}}
var DatabaseError = Err("Your request failed due to a database error.", http.StatusInternalServerError)

It carries a message and a corresponding HTTP code to make dealing with errors easier. Now that everything is in the right place we should pass or test which are failing in the current state.

So first we go by creating an HTTP handler function in budgets.go file:

// budgets.go
package main
import (    "net/http"    "github.com/azbshiri/common/db")
type budget struct {    db.Model    Amount float64 `json:"amount"`}
func (s *server) getBudgets(w http.ResponseWriter, r *http.Request) {}

Then registering the HTTP handle in routes.go:

// routes.go
package main
const BudgetPath = "/api/budgets"
func (s *server) routes() {    s.mux.HandleFunc(BudgetPath, s.getBudgets).Methods("GET")}

Note: I defined BudgetPath constant to be able to use budgets API path in various places including tests. Tests are still failing so we should get budgets from the database and return an acceptable response to consumers. Also, I created a CreateBudgetListFactory factory in factories.gofile to making database populations easier.

// factories.go
package main
import (    "math/rand"    "time"    "github.com/go-pg/pg")
func CreateBudgetListFactory(db *pg.DB, length int) (*[]budget, error) {    budgets := make([]budget, length)    for _, budget := range budgets {        budget.Amount = rand.Float64()        budget.CreatedAt = time.Now()    }    err := db.Insert(&budgets)    if err != nil {        return nil, err    }    return &budgets, nil}// budgets.go
package main
import (    "encoding/json"    "net/http"    "github.com/azbshiri/common/db"    "github.com/pquerna/ffjson/ffjson")
type budget struct {    db.Model    Amount float64 `json:"amount"`}
func (s *server) getBudgets(w http.ResponseWriter, r *http.Request) {    var budgets []*budget    err := s.db.Model(&budgets).Select()    if err != nil {        w.WriteHeader(http.StatusInternalServerError)        json.NewEncoder(w).Encode(DatabaseError)        return    }    ffjson.NewEncoder(w).Encode(budgets)}

Now we're green:

=== RUN   TestGetBudgets_EmptyResponse--- PASS: TestGetBudgets_EmptyResponse (0.00s)=== RUN   TestGetBudgets_NormalResponse--- PASS: TestGetBudgets_NormalResponse (0.00s)=== RUN   TestGetBudgets_DatabaseError--- PASS: TestGetBudgets_DatabaseError (0.00s)
PASS
ok      github.com/azbshiri/budget-api  0.040s

Okay, now we repeating the same steps for creating a new budget API:

// budgets_test.go
package main
import (    "bytes"    "net/http"    "os"    "testing"    "github.com/azbshiri/common/test"    "github.com/go-pg/pg"    "github.com/go-pg/pg/orm"    "github.com/gorilla/mux"    "github.com/pquerna/ffjson/ffjson"    "github.com/stretchr/testify/assert")
var testServer *server
var badServer *server
func TestMain(m *testing.M) {    testServer = newServer(        pg.Connect(&pg.Options{            User:     "alireza",            Password: "alireza",            Database: "alireza_test",        }),        mux.NewRouter(),    )    badServer = newServer(        pg.Connect(&pg.Options{            User:     "not_found",            Password: "alireza",            Database: "alireza",        }),        mux.NewRouter(),    )    testServer.db.CreateTable(&budget{}, &orm.CreateTableOptions{        Temp: true,    })    os.Exit(m.Run())}
func TestGetBudgets_EmptyResponse(t *testing.T) {    var body []budget    res, err := test.DoRequest(testServer, "GET", BudgetPath, nil)    ffjson.NewDecoder().DecodeReader(res.Body, &body)    assert.NoError(t, err)    assert.Len(t, body, 0)    assert.Equal(t, res.Code, http.StatusOK)}
func TestGetBudgets_NormalResponse(t *testing.T) {    var body []budget    budgets, err := CreateBudgetListFactory(testServer.db, 10)    assert.NoError(t, err)    res, err := test.DoRequest(testServer, "GET", BudgetPath, nil)    assert.Equal(t, http.StatusOK, res.Code)    assert.NoError(t, err)    ffjson.NewDecoder().DecodeReader(res.Body, &body)    assert.Len(t, body, 10)    assert.Equal(t, budgets, &body)}
func TestGetBudgets_DatabaseError(t *testing.T) {    var body Error    res, err := test.DoRequest(badServer, "GET", BudgetPath, nil)    ffjson.NewDecoder().DecodeReader(res.Body, &body)    assert.NoError(t, err)    assert.Equal(t, DatabaseError, &body)    assert.Equal(t, http.StatusInternalServerError, res.Code)}
func TestCreateBudget(t *testing.T) {    var body budget    byt, err := ffjson.Marshal(&budget{Amount: 1000.4})    rdr := bytes.NewReader(byt)    res, err := test.DoRequest(testServer, "POST", BudgetPath, rdr)    ffjson.NewDecoder().DecodeReader(res.Body, &body)    assert.NoError(t, err)    assert.Equal(t, 1000.4, body.Amount)    assert.Equal(t, http.StatusOK, res.Code)}
func TestCreateBudget_BadParamError(t *testing.T) {    var body Error    res, err := test.DoRequest(testServer, "POST", BudgetPath,        bytes.NewReader([]byte{}))    ffjson.NewDecoder().DecodeReader(res.Body, &body)    assert.NoError(t, err)    assert.Equal(t, BadParamError, &body)    assert.Equal(t, http.StatusBadRequest, res.Code)}
func TestCreateBudget_DatabaseError(t *testing.T) {    var body Error    byt, err := ffjson.Marshal(&budget{Amount: 1000.4})    rdr := bytes.NewReader(byt)    res, err := test.DoRequest(badServer, "POST", BudgetPath, rdr)    ffjson.NewDecoder().DecodeReader(res.Body, &body)    assert.NoError(t, err)    assert.Equal(t, DatabaseError, &body)    assert.Equal(t, http.StatusInternalServerError, res.Code)}

Creating a new HTTP handle function for new API:

// budgets.go
package main
import (    "encoding/json"    "net/http"    "github.com/azbshiri/common/db"    "github.com/pquerna/ffjson/ffjson")
type budget struct {    db.Model    Amount float64 `json:"amount"`}
func (s *server) getBudgets(w http.ResponseWriter, r *http.Request) {    var budgets []*budget    err := s.db.Model(&budgets).Select()    if err != nil {        w.WriteHeader(http.StatusInternalServerError)        json.NewEncoder(w).Encode(DatabaseError)        return    }    ffjson.NewEncoder(w).Encode(budgets)}
func (s *server) createBudget(w http.ResponseWriter, r *http.Request) {    var param struct {        Amount float64 `json:"amount"`    }    err := ffjson.NewDecoder().DecodeReader(r.Body, &param)    if err != nil {        w.WriteHeader(http.StatusBadRequest)        json.NewEncoder(w).Encode(BadParamError)        return    }    budget := budget{Amount: param.Amount}    err = s.db.Insert(&budget)    if err != nil {        w.WriteHeader(http.StatusInternalServerError)        json.NewEncoder(w).Encode(DatabaseError)        return    }    ffjson.NewEncoder(w).Encode(budget)}

Note: I used DTO concept to decode parameters that sent to budget creation API, which is a great way to do so.

And we're done:

=== RUN   TestGetBudgets_EmptyResponse--- PASS: TestGetBudgets_EmptyResponse (0.01s)=== RUN   TestGetBudgets_NormalResponse--- PASS: TestGetBudgets_NormalResponse (0.00s)=== RUN   TestGetBudgets_DatabaseError--- PASS: TestGetBudgets_DatabaseError (0.00s)=== RUN   TestCreateBudget--- PASS: TestCreateBudget (0.00s)=== RUN   TestCreateBudget_BadParamError--- PASS: TestCreateBudget_BadParamError (0.00s)=== RUN   TestCreateBudget_DatabaseError--- PASS: TestCreateBudget_DatabaseError (0.00s)
PASS
ok      github.com/azbshiri/budget-api  (cached)
Written by
Senna Labs
Senna Labs

Subscribe to follow product news, latest in technology, solutions, and updates

- More than 120,000 people/day visit to read our blogs

บทความอื่นๆ

13
November, 2024
JS class syntax
13 November, 2024
JS class syntax
เชื่อว่าหลายๆคนที่เขียน javascript กันมา คงต้องเคยสงสัยกันบ้าง ว่า class ที่อยู่ใน js เนี่ย มันคืออะไร แล้วมันมีหน้าที่ต่างกับการประกาศ function อย่างไร? เรามารู้จักกับ class ให้มากขึ้นกันดีกว่า class เปรียบเสมือนกับ blueprint หรือแบบพิมพ์เขียว ที่สามารถนำไปสร้างเป็นสิ่งของ( object ) ตาม blueprint หรือแบบพิมพ์เขียว( class ) นั้นๆได้ โดยภายใน class

By

4 mins read
Thai
13
November, 2024
15 สิ่งที่ทุกธุรกิจต้องรู้เกี่ยวกับ 5G
13 November, 2024
15 สิ่งที่ทุกธุรกิจต้องรู้เกี่ยวกับ 5G
ผู้ให้บริการเครือข่ายในสหรัฐฯ ได้เปิดตัว 5G ในหลายรูปแบบ และเช่นเดียวกับผู้ให้บริการเครือข่ายในยุโรปหลายราย แต่… 5G มันคืออะไร และทำไมเราต้องให้ความสนใจ บทความนี้ได้รวบรวม 15 สิ่งที่ทุกธุรกิจต้องรู้เกี่ยวกับ 5G เพราะเราปฏิเสธไม่ได้เลยว่ามันกำลังจะถูกใช้งานอย่างกว้างขวางขึ้น 1. 5G หรือ Fifth-Generation คือยุคใหม่ของเทคโนโลยีเครือข่ายไร้สายที่จะมาแทนที่ระบบ 4G ที่เราใช้อยู่ในปัจจุบัน ซึ่งมันไม่ได้ถูกจำกัดแค่มือถือเท่านั้น แต่รวมถึงอุปกรณ์ทุกชนิดที่เชื่อมต่ออินเตอร์เน็ตได้ 2. 5G คือการพัฒนา 3 ส่วนที่สำคัญที่จะนำมาสู่การเชื่อมต่ออุปกรณ์ไร้สายต่างๆ ขยายช่องสัญญาณขนาดใหญ่ขึ้นเพื่อเพิ่มความเร็วในการเชื่อมต่อ การตอบสนองที่รวดเร็วขึ้นในระยะเวลาที่น้อยลง ความสามารถในการเชื่อมต่ออุปกรณ์มากกว่า 1 ในเวลาเดียวกัน 3. สัญญาณ 5G นั้นแตกต่างจากระบบ

By

4 mins read
Thai
13
November, 2024
จัดการ Array ด้วย Javascript (Clone Deep)
13 November, 2024
จัดการ Array ด้วย Javascript (Clone Deep)
ในปัจจุบันนี้ ปฏิเสธไม่ได้เลยว่าภาษาที่ถูกใช้ในการเขียนเว็บต่าง ๆ นั้น คงหนีไม่พ้นภาษา Javascript ซึ่งเป็นภาษาที่ถูกนำไปพัฒนาเป็น framework หรือ library ต่าง ๆ มากมาย ผู้พัฒนาหลายคนก็มีรูปแบบการเขียนภาษา Javascript ที่แตกต่างกัน เราเลยมีแนวทางการเขียนที่หลากหลาย มาแบ่งปันเพื่อน ๆ เกี่ยวกับการจัดการ Array ด้วยภาษา Javascript กัน เรามาดูตัวอย่างกันเลยดีกว่า โดยปกติแล้วการ copy ค่าจาก value type ธรรมดา สามารถเขียนได้ดังนี้

By

4 mins read
Thai

Let’s build digital products that are
simply awesome !

We will get back to you within 24 hours!ติดต่อเรา
Please tell us your ideas.
- Senna Labsmake it happy
Contact ball
Contact us bg 2
Contact us bg 4
Contact us bg 1
Ball leftBall rightBall leftBall right
Sennalabs gray logo28/11 Soi Ruamrudee, Lumphini, Pathumwan, Bangkok 10330+66 62 389 4599hello@sennalabs.com© 2022 Senna Labs Co., Ltd.All rights reserved.