在 Go 中測試 REST API:使用 Go 標(biāo)準(zhǔn)測試庫進(jìn)行單元和整合測試的指南
Nov 17, 2024 am 01:33 AM介紹
本文將帶您了解如何使用單元測試和整合測試來改善您在 golang 中建立 REST API 時的開發(fā)體驗。
單元測試旨在驗證應(yīng)用程式最小的各個部分的功能,通常側(cè)重於單一函數(shù)或方法。這些測試與程式碼的其他部分隔離進(jìn)行,以確保每個組件都能按預(yù)期工作。
另一方面,整合測試評估應(yīng)用程式的不同模組或元件如何協(xié)同工作。在本文中,我們將重點放在 Go 應(yīng)用程式的整合測試,特別是透過成功建立和執(zhí)行 SQL 查詢來檢查它是否與 PostgreSQL 資料庫正確互動。
本文假設(shè)您熟悉golang 以及如何在golang 中建立Rest api,主要重點是為您的路由建立測試(單元測試)並測試您的sql 查詢函數(shù)(整合測試)以供參考,請造訪github來看看這個項目。
設(shè)定
假設(shè)您已設(shè)定與上面連結(jié)的項目類似的項目,您將擁有與此類似的資料夾結(jié)構(gòu)
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 中的測試很容易,因為內(nèi)建的測試包提供了編寫測試所需的工具。
測試檔以 _test.go 命名,此後綴允許 go 在執(zhí)行 go test 指令時以該檔案為目標(biāo)執(zhí)行。
我們專案的入口點是位於 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 伺服器。創(chuàng)建伺服器後,我們在指定的連接埠上運(yùn)行它。
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 路由器。
整合測試
我們有一個使用者儲存結(jié)構(gòu),用於處理與使用者實體相關(guān)的 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 個指標(biāo)接收器方法:
- 建立使用者
- 透過電子郵件取得使用者
- 取得UserById
為了讓這些方法執(zhí)行其功能,它們必須與外部系統(tǒng)交互,在本例中為 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) } }) }
讓我們?yōu)g覽一下文件,看看每個部分的作用。
第一個操作是宣告變數(shù) userTestStore 和 testDB。這些變數(shù)將用於分別儲存指向用戶儲存和資料庫的指標(biāo)。我們在全域檔案作用域中聲明它們的原因是因為我們希望測試檔案中的所有函數(shù)都可以存取指標(biāo)。
TestMain 函數(shù)允許我們在執(zhí)行主測試之前執(zhí)行一些設(shè)定操作。我們最初連接到 postgres 儲存並將指標(biāo)保存到我們的全域變數(shù)中。
我們已經(jīng)使用該指標(biāo)建立了一個 userTestStore,我們將使用它來執(zhí)行我們嘗試連接的 sql 查詢。
defer testDB.Close() 測試完成後關(guān)閉資料庫連線
code := m.Run() 在返回和退出之前運(yùn)行測試函數(shù)的其餘部分。
TestCreateUser 函數(shù)將處理 create_user 函數(shù)的檢定。我們的目標(biāo)是測試如果傳遞了唯一的電子郵件,該函數(shù)是否會建立用戶,並且如果非唯一的電子郵件已用於建立另一個用戶,則該函數(shù)應(yīng)該無法建立用戶。
首先我們建立測試數(shù)據(jù),用於測試這兩種情況場景。
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
我將循環(huán)遍歷地圖執(zhí)行 create_user 函數(shù),並以測試日期作為參數(shù),並比較傳回的值是否與我們期望的結(jié)果相同
// 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) } }
如果回傳的結(jié)果與預(yù)期結(jié)果不一樣,那麼我們的測試將會失敗
函數(shù)的最後一部分是使用內(nèi)建的測試包函數(shù) Cleanup。該函數(shù)註冊了一個函數(shù),當(dāng)測試中的所有函數(shù)都已執(zhí)行完畢時將呼叫該函數(shù)。在我們的範(fàn)例中,我們使用該函數(shù)來清除在此測試函數(shù)執(zhí)行期間使用的使用者資料。
單元測試
對於我們的單元測試,我們將測試 api 的路由處理程序。在這種情況下,路由與用戶實體相關(guān)。請觀察下面。
// 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 個函數(shù)
- 處理登入
- 句柄註冊
- HandleGetUser
處理獲取用戶
此處理程序中的handleGetUser 函數(shù)根據(jù)HTTP 請求URL 中提供的使用者ID 檢索使用者詳細(xì)資料。它首先使用 mux 路由器從請求路徑變數(shù)中提取使用者 ID。如果 userID 遺失或無效(非整數(shù)),則會傳回 400 Bad Request 錯誤。驗證後,該函數(shù)將呼叫資料儲存體上的 GetUserByID 方法來檢索使用者資訊。如果檢索期間發(fā)生錯誤,它將傳回 500 內(nèi)部伺服器錯誤。成功後,它會回應(yīng) 200 OK 狀態(tài),並在回應(yīng)正文中以 JSON 形式發(fā)送使用者詳細(xì)資訊。
如前所述,為了測試處理程序函數(shù),我們需要建立一個 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
我們的新處理程序函數(shù)需要一個使用者儲存作為參數(shù)來建立處理程序結(jié)構(gòu)。
由於我們不需要實際存儲,因此我們建立一個模擬結(jié)構(gòu)並建立模擬實際結(jié)構(gòu)函數(shù)的接收器函數(shù)。我們這樣做是因為我們單獨(dú)處理儲存函數(shù)測試,因此我們不需要在處理程序測試中測試這部分程式碼。
測試函數(shù) 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 請求回應(yīng) 400 狀態(tài)代碼,則測試預(yù)計會通過。
第二個測試案例場景是我們使用包含有效使用者 ID 的正確 url 檢索使用者資訊的情況。在此測試案例中,我們期望得到 200 狀態(tài)代碼的回應(yīng)。如果沒有,測試就會失敗。
// 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) }
結(jié)論
我們已經(jīng)透過為路由處理程序建立測試來在我們的專案中實現(xiàn)單元測試。我們已經(jīng)了解如何使用模擬來僅測試一小部分程式碼。我們已經(jīng)能夠為與 Postgresql DB 互動的函數(shù)開發(fā)整合測試。
如果您想親身體驗專案程式碼,請在此處從 github 複製儲存庫
以上是在 Go 中測試 REST API:使用 Go 標(biāo)準(zhǔn)測試庫進(jìn)行單元和整合測試的指南的詳細(xì)內(nèi)容。更多資訊請關(guān)注PHP中文網(wǎng)其他相關(guān)文章!

熱AI工具

Undress AI Tool
免費(fèi)脫衣圖片

Undresser.AI Undress
人工智慧驅(qū)動的應(yīng)用程序,用於創(chuàng)建逼真的裸體照片

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

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費(fèi)的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

記事本++7.3.1
好用且免費(fèi)的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強(qiáng)大的PHP整合開發(fā)環(huán)境

Dreamweaver CS6
視覺化網(wǎng)頁開發(fā)工具

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

Golang主要用於後端開發(fā),但也能在前端領(lǐng)域間接發(fā)揮作用。其設(shè)計目標(biāo)聚焦高性能、並發(fā)處理和系統(tǒng)級編程,適合構(gòu)建API服務(wù)器、微服務(wù)、分佈式系統(tǒng)、數(shù)據(jù)庫操作及CLI工具等後端應(yīng)用。雖然Golang不是網(wǎng)頁前端的主流語言,但可通過GopherJS編譯成JavaScript、通過TinyGo運(yùn)行於WebAssembly,或搭配模板引擎生成HTML頁面來參與前端開發(fā)。然而,現(xiàn)代前端開發(fā)仍需依賴JavaScript/TypeScript及其生態(tài)。因此,Golang更適合以高性能後端為核心的技術(shù)棧選擇。

要構(gòu)建一個GraphQLAPI在Go語言中,推薦使用gqlgen庫以提高開發(fā)效率。 1.首先選擇合適的庫,如gqlgen,它支持根據(jù)schema自動生成代碼;2.接著定義GraphQLschema,描述API的結(jié)構(gòu)和查詢?nèi)肟?,如定義Post類型和查詢方法;3.然後初始化項目並生成基礎(chǔ)代碼,實現(xiàn)resolver中的業(yè)務(wù)邏輯;4.最後將GraphQLhandler接入HTTPserver,通過內(nèi)置Playground測試API。注意事項包括字段命名規(guī)範(fàn)、錯誤處理、性能優(yōu)化及安全設(shè)置等,確保項目可維護(hù)性

安裝Go的關(guān)鍵在於選擇正確版本、配置環(huán)境變量並驗證安裝。 1.前往官網(wǎng)下載對應(yīng)系統(tǒng)的安裝包,Windows使用.msi文件,macOS使用.pkg文件,Linux使用.tar.gz文件並解壓至/usr/local目錄;2.配置環(huán)境變量,在Linux/macOS中編輯~/.bashrc或~/.zshrc添加PATH和GOPATH,Windows則在系統(tǒng)屬性中設(shè)置PATH為Go的安裝路徑;3.使用goversion命令驗證安裝,並運(yùn)行測試程序hello.go確認(rèn)編譯執(zhí)行正常。整個流程中PATH設(shè)置和環(huán)

sync.WaitGroup用於等待一組goroutine完成任務(wù),其核心是通過Add、Done、Wait三個方法協(xié)同工作。 1.Add(n)設(shè)置需等待的goroutine數(shù)量;2.Done()在每個goroutine結(jié)束時調(diào)用,計數(shù)減一;3.Wait()阻塞主協(xié)程直到所有任務(wù)完成。使用時需注意:Add應(yīng)在goroutine外調(diào)用、避免重複Wait、務(wù)必確保Done被調(diào)用,推薦配合defer使用。常見於並發(fā)抓取網(wǎng)頁、批量數(shù)據(jù)處理等場景,能有效控制並發(fā)流程。

使用Go的embed包可以方便地將靜態(tài)資源嵌入二進(jìn)制,適合Web服務(wù)打包HTML、CSS、圖片等文件。 1.聲明嵌入資源需在變量前加//go:embed註釋,如嵌入單個文件hello.txt;2.可嵌入整個目錄如static/*,通過embed.FS實現(xiàn)多文件打包;3.開發(fā)時建議通過buildtag或環(huán)境變量切換磁盤加載模式以提高效率;4.注意路徑正確性、文件大小限制及嵌入資源的只讀特性。合理使用embed能簡化部署並優(yōu)化項目結(jié)構(gòu)。

音視頻處理的核心在於理解基本流程與優(yōu)化方法。 1.其基本流程包括採集、編碼、傳輸、解碼和播放,每個環(huán)節(jié)均有技術(shù)難點;2.常見問題如音畫不同步、卡頓延遲、聲音噪音、畫面模糊等,可通過同步調(diào)整、編碼優(yōu)化、降噪模塊、參數(shù)調(diào)節(jié)等方式解決;3.推薦使用FFmpeg、OpenCV、WebRTC、GStreamer等工具實現(xiàn)功能;4.性能管理方面應(yīng)注重硬件加速、合理設(shè)置分辨率幀率、控制並發(fā)及內(nèi)存洩漏問題。掌握這些關(guān)鍵點有助於提升開發(fā)效率和用戶體驗。

搭建一個用Go編寫的Web服務(wù)器並不難,核心在於利用net/http包實現(xiàn)基礎(chǔ)服務(wù)。 1.使用net/http啟動最簡服務(wù)器:通過幾行代碼註冊處理函數(shù)並監(jiān)聽端口;2.路由管理:使用ServeMux組織多個接口路徑,便於結(jié)構(gòu)化管理;3.常見做法:按功能模塊分組路由,並可用第三方庫支持複雜匹配;4.靜態(tài)文件服務(wù):通過http.FileServer提供HTML、CSS和JS文件;5.性能與安全:啟用HTTPS、限制請求體大小、設(shè)置超時時間以提升安全性與性能。掌握這些要點後,擴(kuò)展功能將更加容易。

select加default的作用是讓select在沒有其他分支就緒時執(zhí)行默認(rèn)行為,避免程序阻塞。 1.非阻塞地從channel接收數(shù)據(jù)時,若channel為空,會直接進(jìn)入default分支;2.結(jié)合time.After或ticker定時嘗試發(fā)送數(shù)據(jù),若channel滿則不阻塞而跳過;3.防止死鎖,在不確定channel是否被關(guān)閉時避免程序卡住;使用時需注意default分支會立即執(zhí)行,不能濫用,且default與case互斥,不會同時執(zhí)行。
