搜尋
首頁後端開發Golang在 Go 中測試 REST API:使用 Go 標準測試庫進行單元和整合測試的指南

Testing REST APIs in Go: A Guide to Unit and Integration Testing with Go

介紹

本文將帶您了解如何使用單元測試和整合測試來改善您在 golang 中建立 REST API 時的開發體驗。

  • 單元測試旨在驗證應用程式最小的各個部分的功能,通常側重於單一函數或方法。這些測試與程式碼的其他部分隔離進行,以確保每個組件都能按預期工作。

  • 另一方面,
  • 整合測試評估應用程式的不同模組或元件如何協同工作。在本文中,我們將重點放在 Go 應用程式的整合測試,特別是透過成功建立和執行 SQL 查詢來檢查它是否與 PostgreSQL 資料庫正確互動。

本文假設您熟悉golang 以及如何在golang 中建立Rest api,主要重點是為您的路由建立測試(單元測試)並測試您的sql 查詢函數(整合測試)以供參考,請造訪github來看看這個項目。

設定

假設您已設定與上面連結的項目類似的項目,您將擁有與此類似的資料夾結構

test_project
|__cmd
   |__api
      |__api.go
   |__main.go
|__db
   |___seed.go
|__internal
   |___db
       |___db.go
   |___services
       |___records
           |___routes_test.go
           |___routes.go
           |___store_test.go
           |___store.go
       |___user
           |___routes_test.go
           |___routes.go
           |___store_test.go
           |___store.go
|__test_data
|__docker-compose.yml
|__Dockerfile
|__Makefile

與您可能遇到的其他語言相比,golang 中的測試很容易,因為內建的測試包提供了編寫測試所需的工具。
測試檔以 _test.go 命名,此後綴允許 go 在執行 go test 指令時以該檔案為目標執行。

我們專案的入口點是位於 cmd 資料夾中的 main.go 檔案

// main.go

package main

import (
    "log"

    "finance-crud-app/cmd/api"
    "finance-crud-app/internal/db"

    "github.com/gorilla/mux"
    "github.com/jmoiron/sqlx"
    _ "github.com/lib/pq"
)

type Server struct {
    db  *sqlx.DB
    mux *mux.Router
}

func NewServer(db *sqlx.DB, mux *mux.Router) *Server {
    return &Server{
        db:  db,
        mux: mux,
    }
}

func main() {

    connStr := "postgres://postgres:Password123@localhost:5432/crud_db?sslmode=disable"

    dbconn, err := db.NewPGStorage(connStr)
    if err != nil {
        log.Fatal(err)
    }
    defer dbconn.Close()

    server := api.NewAPIServer(":8085", dbconn)
    if err := server.Run(); err != nil {
        log.Fatal(err)
    }
}

從程式碼中您可以看到我們正在透過傳遞資料庫連線和連接埠號碼來建立一個新的 api 伺服器。創建伺服器後,我們在指定的連接埠上運行它。

NewAPIServer 指令來自 api.go 文件,其中

// api.go
package api

import (
    "finance-crud-app/internal/services/records"
    "finance-crud-app/internal/services/user"
    "log"
    "net/http"

    "github.com/gorilla/mux"
    "github.com/jmoiron/sqlx"
)

type APIServer struct {
    addr string
    db   *sqlx.DB
}

func NewAPIServer(addr string, db *sqlx.DB) *APIServer {
    return &APIServer{
        addr: addr,
        db:   db,
    }
}

func (s *APIServer) Run() error {
    router := mux.NewRouter()
    subrouter := router.PathPrefix("/api/v1").Subrouter()

    userStore := user.NewStore(s.db)
    userHandler := user.NewHandler(userStore)
    userHandler.RegisterRoutes(subrouter)

    recordsStore := records.NewStore(s.db)
    recordsHandler := records.NewHandler(recordsStore, userStore)
    recordsHandler.RegisterRoutes(subrouter)

    log.Println("Listening on", s.addr)

    return http.ListenAndServe(s.addr, router)
}

對於這個 api,我們使用 mux 作為我們的 http 路由器。

整合測試

我們有一個使用者儲存結構,用於處理與使用者實體相關的 SQL 查詢。

// store.go
package user

import (
    "errors"
    "finance-crud-app/internal/types"
    "fmt"
    "log"

    "github.com/jmoiron/sqlx"
)

var (
    CreateUserError   = errors.New("cannot create user")
    RetrieveUserError = errors.New("cannot retrieve user")
    DeleteUserError   = errors.New("cannot delete user")
)

type Store struct {
    db *sqlx.DB
}

func NewStore(db *sqlx.DB) *Store {
    return &Store{db: db}
}

func (s *Store) CreateUser(user types.User) (user_id int, err error) {
    query := `
    INSERT INTO users
    (firstName, lastName, email, password)
    VALUES (, , , )
    RETURNING id`

    var userId int
    err = s.db.QueryRow(query, user.FirstName, user.LastName, user.Email, user.Password).Scan(&userId)
    if err != nil {
        return -1, CreateUserError
    }

    return userId, nil
}

func (s *Store) GetUserByEmail(email string) (types.User, error) {
    var user types.User

    err := s.db.Get(&user, "SELECT * FROM users WHERE email = ", email)
    if err != nil {
        return types.User{}, RetrieveUserError
    }

    if user.ID == 0 {
        log.Fatalf("user not found")
        return types.User{}, RetrieveUserError
    }

    return user, nil
}

func (s *Store) GetUserByID(id int) (*types.User, error) {
    var user types.User
    err := s.db.Get(&user, "SELECT * FROM users WHERE id = ", id)
    if err != nil {
        return nil, RetrieveUserError
    }

    if user.ID == 0 {
        return nil, fmt.Errorf("user not found")
    }

    return &user, nil
}

func (s *Store) DeleteUser(email string) error {

    user, err := s.GetUserByEmail(email)
    if err != nil {
        return DeleteUserError
    }
    // delete user records first
    _, err = s.db.Exec("DELETE FROM records WHERE userid = ", user.ID)
    if err != nil {
        return DeleteUserError
    }

    _, err = s.db.Exec("DELETE FROM users WHERE email = ", email)
    if err != nil {
        return DeleteUserError
    }
    return nil
}

在上面的文件中,我們有 3 個指標接收器方法:

  • 建立使用者
  • 透過電子郵件取得使用者
  • 取得UserById

為了讓這些方法執行其功能,它們必須與外部系統交互,在本例中為 Postgres DB .

為了測試這個方法,我們先建立一個 store_test.go 檔案。在 go 中,我們通常以要測試的文件命名測試文件,並添加後綴 _test.go .

// store_test.go

package user_test

import (
    "finance-crud-app/internal/db"
    "finance-crud-app/internal/services/user"
    "finance-crud-app/internal/types"
    "log"
    "os"
    "testing"

    "github.com/jmoiron/sqlx"
    _ "github.com/lib/pq"
)

var (
    userTestStore *user.Store
    testDB        *sqlx.DB
)

func TestMain(m *testing.M) {
    // database
    ConnStr := "postgres://postgres:Password123@localhost:5432/crud_db?sslmode=disable"
    testDB, err := db.NewPGStorage(ConnStr)
    if err != nil {
        log.Fatalf("could not connect %v", err)
    }
    defer testDB.Close()
    userTestStore = user.NewStore(testDB)

    code := m.Run()
    os.Exit(code)
}

func TestCreateUser(t *testing.T) {
    test_data := map[string]struct {
        user   types.User
        result any
    }{
        "should PASS valid user email used": {
            user: types.User{
                FirstName: "testfirsjjlkjt-1",
                LastName:  "testlastkjh-1",
                Email:     "validuser@email.com",
                Password:  "00000000",
            },
            result: nil,
        },
        "should FAIL invalid user email used": {
            user: types.User{
                FirstName: "testFirstName1",
                LastName:  "testLastName1",
                Email:     "test1@email.com",
                Password:  "800890",
            },
            result: user.CreateUserError,
        },
    }

    for name, tc := range test_data {
        t.Run(name, func(t *testing.T) {
            value, got := userTestStore.CreateUser(tc.user)
            if got != tc.result {
                t.Errorf("test fail expected %v got %v instead and value %v", tc.result, got, value)
            }
        })
    }

    t.Cleanup(func() {
        err := userTestStore.DeleteUser("validuser@email.com")
        if err != nil {
            t.Errorf("could not delete user %v got error %v", "validuser@email.com", err)
        }
    })
}

func TestGetUserByEmail(t *testing.T) {
    test_data := map[string]struct {
        email  string
        result any
    }{
        "should pass valid user email address used": {
            email:  "test1@email.com",
            result: nil,
        },
        "should fail invalid user email address used": {
            email:  "validuser@email.com",
            result: user.RetrieveUserError,
        },
    }

    for name, tc := range test_data {
        got, err := userTestStore.GetUserByEmail(tc.email)
        if err != tc.result {
            t.Errorf("test fail expected %v instead got %v", name, got)
        }
    }
}

func TestGetUserById(t *testing.T) {
    testUserId, err := userTestStore.CreateUser(types.User{
        FirstName: "userbyid",
        LastName:  "userbylast",
        Email:     "unique_email",
        Password:  "unique_password",
    })
    if err != nil {
        log.Panicf("got %v when creating testuser", testUserId)
    }

    test_data := map[string]struct {
        user_id int
        result  any
    }{
        "should pass valid user id used": {
            user_id: testUserId,
            result:  nil,
        },
        "should fail invalid user id used": {
            user_id: 0,
            result:  user.RetrieveUserError,
        },
    }

    for name, tc := range test_data {
        t.Run(name, func(t *testing.T) {
            _, got := userTestStore.GetUserByID(tc.user_id)
            if got != tc.result {
                t.Errorf("error retrieving user by id got %v want %v", got, tc.result)
            }
        })
    }

    t.Cleanup(func() {
        err := userTestStore.DeleteUser("unique_email")
        if err != nil {
            t.Errorf("could not delete user %v got error %v", "unique_email", err)
        }
    })
}

func TestDeleteUser(t *testing.T) {
    testUserId, err := userTestStore.CreateUser(types.User{
        FirstName: "userbyid",
        LastName:  "userbylast",
        Email:     "delete_user@email.com",
        Password:  "unique_password",
    })
    if err != nil {
        log.Panicf("got %v when creating testuser", testUserId)
    }

    test_data := map[string]struct {
        user_email string
        result     error
    }{
        "should pass user email address used": {
            user_email: "delete_user@email.com",
            result:     nil,
        },
    }

    for name, tc := range test_data {
        t.Run(name, func(t *testing.T) {
            err = userTestStore.DeleteUser(tc.user_email)
            if err != tc.result {
                t.Errorf("error deletig user got %v instead of %v", err, tc.result)
            }
        })
    }

    t.Cleanup(func() {
        err := userTestStore.DeleteUser("delete_user@email.com")
        if err != nil {
            log.Printf("could not delete user %v got error %v", "delete_user@email.com", err)
        }
    })
}

讓我們瀏覽一下文件,看看每個部分的作用。

第一個操作是宣告變數 userTestStore 和 testDB。這些變數將用於分別儲存指向用戶儲存和資料庫的指標。我們在全域檔案作用域中聲明它們的原因是因為我們希望測試檔案中的所有函數都可以存取指標。

TestMain 函數允許我們在執行主測試之前執行一些設定操作。我們最初連接到 postgres 儲存並將指標保存到我們的全域變數中。
我們已經使用該指標建立了一個 userTestStore,我們將使用它來執行我們嘗試連接的 sql 查詢。

defer testDB.Close() 測試完成後關閉資料庫連線

code := m.Run() 在返回和退出之前運行測試函數的其餘部分。

TestCreateUser 函數將處理 create_user 函數的檢定。我們的目標是測試如果傳遞了唯一的電子郵件,該函數是否會建立用戶,並且如果非唯一的電子郵件已用於建立另一個用戶,則該函數應該無法建立用戶。

首先我們建立測試數據,用於測試這兩種情況場景。

test_project
|__cmd
   |__api
      |__api.go
   |__main.go
|__db
   |___seed.go
|__internal
   |___db
       |___db.go
   |___services
       |___records
           |___routes_test.go
           |___routes.go
           |___store_test.go
           |___store.go
       |___user
           |___routes_test.go
           |___routes.go
           |___store_test.go
           |___store.go
|__test_data
|__docker-compose.yml
|__Dockerfile
|__Makefile

我將循環遍歷地圖執行 create_user 函數,並以測試日期作為參數,並比較傳回的值是否與我們期望的結果相同

// main.go

package main

import (
    "log"

    "finance-crud-app/cmd/api"
    "finance-crud-app/internal/db"

    "github.com/gorilla/mux"
    "github.com/jmoiron/sqlx"
    _ "github.com/lib/pq"
)

type Server struct {
    db  *sqlx.DB
    mux *mux.Router
}

func NewServer(db *sqlx.DB, mux *mux.Router) *Server {
    return &Server{
        db:  db,
        mux: mux,
    }
}

func main() {

    connStr := "postgres://postgres:Password123@localhost:5432/crud_db?sslmode=disable"

    dbconn, err := db.NewPGStorage(connStr)
    if err != nil {
        log.Fatal(err)
    }
    defer dbconn.Close()

    server := api.NewAPIServer(":8085", dbconn)
    if err := server.Run(); err != nil {
        log.Fatal(err)
    }
}

如果回傳的結果與預期結果不一樣,那麼我們的測試將會失敗

函數的最後一部分是使用內建的測試包函數 Cleanup。該函數註冊了一個函數,當測試中的所有函數都已執行完畢時將呼叫該函數。在我們的範例中,我們使用該函數來清除在此測試函數執行期間使用的使用者資料。

單元測試

對於我們的單元測試,我們將測試 api 的路由處理程序。在這種情況下,路由與用戶實體相關。請觀察下面。

// api.go
package api

import (
    "finance-crud-app/internal/services/records"
    "finance-crud-app/internal/services/user"
    "log"
    "net/http"

    "github.com/gorilla/mux"
    "github.com/jmoiron/sqlx"
)

type APIServer struct {
    addr string
    db   *sqlx.DB
}

func NewAPIServer(addr string, db *sqlx.DB) *APIServer {
    return &APIServer{
        addr: addr,
        db:   db,
    }
}

func (s *APIServer) Run() error {
    router := mux.NewRouter()
    subrouter := router.PathPrefix("/api/v1").Subrouter()

    userStore := user.NewStore(s.db)
    userHandler := user.NewHandler(userStore)
    userHandler.RegisterRoutes(subrouter)

    recordsStore := records.NewStore(s.db)
    recordsHandler := records.NewHandler(recordsStore, userStore)
    recordsHandler.RegisterRoutes(subrouter)

    log.Println("Listening on", s.addr)

    return http.ListenAndServe(s.addr, router)
}

我們想要測試 3 個函數

  • 處理登入
  • 句柄註冊
  • HandleGetUser

處理獲取用戶

此處理程序中的handleGetUser 函數根據HTTP 請求URL 中提供的使用者ID 檢索使用者詳細資料。它首先使用 mux 路由器從請求路徑變數中提取使用者 ID。如果 userID 遺失或無效(非整數),則會傳回 400 Bad Request 錯誤。驗證後,該函數將呼叫資料儲存體上的 GetUserByID 方法來檢索使用者資訊。如果檢索期間發生錯誤,它將傳回 500 內部伺服器錯誤。成功後,它會回應 200 OK 狀態,並在回應正文中以 JSON 形式發送使用者詳細資訊。

如前所述,為了測試處理程序函數,我們需要建立一個 routes_test.go。請參閱下面我的

test_project
|__cmd
   |__api
      |__api.go
   |__main.go
|__db
   |___seed.go
|__internal
   |___db
       |___db.go
   |___services
       |___records
           |___routes_test.go
           |___routes.go
           |___store_test.go
           |___store.go
       |___user
           |___routes_test.go
           |___routes.go
           |___store_test.go
           |___store.go
|__test_data
|__docker-compose.yml
|__Dockerfile
|__Makefile

我們的新處理程序函數需要一個使用者儲存作為參數來建立處理程序結構。
由於我們不需要實際存儲,因此我們建立一個模擬結構並建立模擬實際結構函數的接收器函數。我們這樣做是因為我們單獨處理儲存函數測試,因此我們不需要在處理程序測試中測試這部分程式碼。

測試函數 TestGetUserHandler 測試兩種情況,第一種是嘗試在不提供使用者 ID 的情況下檢索使用者

// main.go

package main

import (
    "log"

    "finance-crud-app/cmd/api"
    "finance-crud-app/internal/db"

    "github.com/gorilla/mux"
    "github.com/jmoiron/sqlx"
    _ "github.com/lib/pq"
)

type Server struct {
    db  *sqlx.DB
    mux *mux.Router
}

func NewServer(db *sqlx.DB, mux *mux.Router) *Server {
    return &Server{
        db:  db,
        mux: mux,
    }
}

func main() {

    connStr := "postgres://postgres:Password123@localhost:5432/crud_db?sslmode=disable"

    dbconn, err := db.NewPGStorage(connStr)
    if err != nil {
        log.Fatal(err)
    }
    defer dbconn.Close()

    server := api.NewAPIServer(":8085", dbconn)
    if err := server.Run(); err != nil {
        log.Fatal(err)
    }
}

如果 http 請求回應 400 狀態代碼,則測試預計會通過。

第二個測試案例場景是我們使用包含有效使用者 ID 的正確 url 檢索使用者資訊的情況。在此測試案例中,我們期望得到 200 狀態代碼的回應。如果沒有,測試就會失敗。

// api.go
package api

import (
    "finance-crud-app/internal/services/records"
    "finance-crud-app/internal/services/user"
    "log"
    "net/http"

    "github.com/gorilla/mux"
    "github.com/jmoiron/sqlx"
)

type APIServer struct {
    addr string
    db   *sqlx.DB
}

func NewAPIServer(addr string, db *sqlx.DB) *APIServer {
    return &APIServer{
        addr: addr,
        db:   db,
    }
}

func (s *APIServer) Run() error {
    router := mux.NewRouter()
    subrouter := router.PathPrefix("/api/v1").Subrouter()

    userStore := user.NewStore(s.db)
    userHandler := user.NewHandler(userStore)
    userHandler.RegisterRoutes(subrouter)

    recordsStore := records.NewStore(s.db)
    recordsHandler := records.NewHandler(recordsStore, userStore)
    recordsHandler.RegisterRoutes(subrouter)

    log.Println("Listening on", s.addr)

    return http.ListenAndServe(s.addr, router)
}

結論

我們已經透過為路由處理程序建立測試來在我們的專案中實現單元測試。我們已經了解如何使用模擬來僅測試一小部分程式碼。我們已經能夠為與 Postgresql DB 互動的函數開發整合測試。
如果您想親身體驗專案程式碼,請在此處從 github 複製儲存庫

以上是在 Go 中測試 REST API:使用 Go 標準測試庫進行單元和整合測試的指南的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
Golang vs. Python:利弊Golang vs. Python:利弊Apr 21, 2025 am 12:17 AM

Golangisidealforbuildingscalablesystemsduetoitsefficiencyandconcurrency,whilePythonexcelsinquickscriptinganddataanalysisduetoitssimplicityandvastecosystem.Golang'sdesignencouragesclean,readablecodeanditsgoroutinesenableefficientconcurrentoperations,t

Golang和C:並發與原始速度Golang和C:並發與原始速度Apr 21, 2025 am 12:16 AM

Golang在並發性上優於C ,而C 在原始速度上優於Golang。 1)Golang通過goroutine和channel實現高效並發,適合處理大量並發任務。 2)C 通過編譯器優化和標準庫,提供接近硬件的高性能,適合需要極致優化的應用。

為什麼要使用Golang?解釋的好處和優勢為什麼要使用Golang?解釋的好處和優勢Apr 21, 2025 am 12:15 AM

選擇Golang的原因包括:1)高並發性能,2)靜態類型系統,3)垃圾回收機制,4)豐富的標準庫和生態系統,這些特性使其成為開發高效、可靠軟件的理想選擇。

Golang vs.C:性能和速度比較Golang vs.C:性能和速度比較Apr 21, 2025 am 12:13 AM

Golang適合快速開發和並發場景,C 適用於需要極致性能和低級控制的場景。 1)Golang通過垃圾回收和並發機制提升性能,適合高並發Web服務開發。 2)C 通過手動內存管理和編譯器優化達到極致性能,適用於嵌入式系統開發。

golang比C快嗎?探索極限golang比C快嗎?探索極限Apr 20, 2025 am 12:19 AM

Golang在編譯時間和並發處理上表現更好,而C 在運行速度和內存管理上更具優勢。 1.Golang編譯速度快,適合快速開發。 2.C 運行速度快,適合性能關鍵應用。 3.Golang並發處理簡單高效,適用於並發編程。 4.C 手動內存管理提供更高性能,但增加開發複雜度。

Golang:從Web服務到系統編程Golang:從Web服務到系統編程Apr 20, 2025 am 12:18 AM

Golang在Web服務和系統編程中的應用主要體現在其簡潔、高效和並發性上。 1)在Web服務中,Golang通過強大的HTTP庫和並發處理能力,支持創建高性能的Web應用和API。 2)在系統編程中,Golang利用接近硬件的特性和對C語言的兼容性,適用於操作系統開發和嵌入式系統。

Golang vs.C:基準和現實世界的表演Golang vs.C:基準和現實世界的表演Apr 20, 2025 am 12:18 AM

Golang和C 在性能對比中各有優劣:1.Golang適合高並發和快速開發,但垃圾回收可能影響性能;2.C 提供更高性能和硬件控制,但開發複雜度高。選擇時需綜合考慮項目需求和團隊技能。

Golang vs. Python:比較分析Golang vs. Python:比較分析Apr 20, 2025 am 12:17 AM

Golang适合高性能和并发编程场景,Python适合快速开发和数据处理。1.Golang强调简洁和高效,适用于后端服务和微服务。2.Python以简洁语法和丰富库著称,适用于数据科学和机器学习。

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

ZendStudio 13.5.1 Mac

ZendStudio 13.5.1 Mac

強大的PHP整合開發環境

mPDF

mPDF

mPDF是一個PHP庫,可以從UTF-8編碼的HTML產生PDF檔案。原作者Ian Back編寫mPDF以從他的網站上「即時」輸出PDF文件,並處理不同的語言。與原始腳本如HTML2FPDF相比,它的速度較慢,並且在使用Unicode字體時產生的檔案較大,但支援CSS樣式等,並進行了大量增強。支援幾乎所有語言,包括RTL(阿拉伯語和希伯來語)和CJK(中日韓)。支援嵌套的區塊級元素(如P、DIV),

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

PhpStorm Mac 版本

PhpStorm Mac 版本

最新(2018.2.1 )專業的PHP整合開發工具

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具