Artikel ini akan membawa anda melalui cara menggunakan ujian unit dan ujian integrasi untuk meningkatkan pengalaman pembangunan anda semasa anda membuat apis rehat di golang.
Ujian unit direka bentuk untuk mengesahkan kefungsian bahagian terkecil, bahagian individu aplikasi, selalunya memfokuskan pada satu fungsi atau kaedah. Ujian ini dijalankan secara berasingan daripada bahagian lain kod untuk memastikan setiap komponen berfungsi seperti yang diharapkan dengan sendirinya.
Ujian integrasi, sebaliknya, menilai cara modul atau komponen aplikasi yang berbeza berfungsi bersama. Dalam artikel ini, kami akan menumpukan pada ujian penyepaduan untuk aplikasi Go kami, khususnya menyemak sama ada ia berinteraksi dengan betul dengan pangkalan data PostgreSQL dengan berjaya membuat dan melaksanakan pertanyaan SQL.
Artikel ini menganggap bahawa anda sudah biasa dengan golang dan cara membuat api rehat dalam golang tumpuan utama adalah untuk mencipta ujian untuk laluan anda (ujian unit) dan menguji fungsi pertanyaan sql anda (ujian integrasi) untuk rujukan, lawati github untuk melihat projek itu.
Dengan mengandaikan anda telah menyediakan projek anda serupa dengan yang dipautkan di atas, anda akan mempunyai struktur folder yang serupa dengan ini
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
Pengujian dalam golang mudah dibandingkan dengan bahasa lain yang mungkin anda temui kerana pakej ujian terbina yang menyediakan alatan yang diperlukan untuk menulis ujian.
Fail ujian dinamakan dengan _test.go akhiran ini membenarkan go untuk menyasarkan fail ini untuk dilaksanakan semasa menjalankan ujian arahan go.
Titik masuk untuk projek kami ialah fail main.go yang terletak dalam folder cmd
// 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) } }
Daripada kod anda boleh lihat kami sedang mencipta pelayan api baharu dengan menghantar sambungan pangkalan data dan nombor port. Selepas mencipta pelayan, kami menjalankannya pada port yang dinyatakan.
Arahan NewAPIServer datang daripada fail api.go yang
// 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) }
Untuk api ini kami menggunakan mux sebagai penghala http kami.
Kami mempunyai struktur Kedai pengguna yang mengendalikan pertanyaan sql yang berkaitan dengan entiti pengguna.
// 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 }
Dalam fail di atas kami mempunyai 3 kaedah penerima penunjuk:
Untuk kaedah ini untuk melaksanakan fungsi mereka, mereka perlu berinteraksi dengan sistem luaran yang , dalam kes ini iaitu Postgres DB .
Untuk menguji kaedah ini, kami akan membuat fail store_test.go dahulu. Dalam go, kami biasanya menamakan fail ujian kami selepas fail yang kami sasarkan untuk menguji dan menambah akhiran _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) } }) }
Mari lihat fail melihat apa yang dilakukan oleh setiap bahagian.
Tindakan pertama ialah mengisytiharkan pembolehubah userTestStore dan testDB. Pembolehubah ini akan digunakan untuk menyimpan penunjuk ke stor pengguna dan db masing-masing. Sebab kami mengisytiharkannya dalam skop fail global adalah kerana kami mahu semua fungsi dalam fail ujian mempunyai akses kepada penunjuk.
Fungsi TestMain membolehkan kami melakukan beberapa tindakan persediaan sebelum ujian utama dijalankan. Kami pada mulanya menyambung ke kedai postgres dan menyimpan penunjuk ke dalam pembolehubah global kami.
Kami telah menggunakan penunjuk itu untuk mencipta userTestStore yang akan kami gunakan untuk melaksanakan pertanyaan sql yang kami cuba sambungkan.
tunda testDB.Close() menutup sambungan pangkalan data selepas ujian selesai
kod := m.Run() menjalankan seluruh fungsi ujian sebelum kembali dan keluar.
FungsiTestCreateUser akan mengendalikan ujian fungsi create_user. Matlamat kami adalah untuk menguji sama ada fungsi itu akan mencipta pengguna jika e-mel unik diluluskan dan fungsi itu sepatutnya tidak boleh mencipta pengguna jika e-mel bukan unik telah digunakan untuk mencipta pengguna lain.
Mula-mula kami mencipta data ujian yang akan kami gunakan untuk menguji kedua-dua senario kes.
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
Saya akan mengulangi peta yang melaksanakan fungsi create_user dengan tarikh ujian sebagai parameter dan bandingkan jika nilai yang dikembalikan adalah sama dengan hasil yang kami jangkakan
// 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) } }
Untuk kes di mana keputusan yang dikembalikan tidak sama dengan keputusan yang dijangkakan maka ujian kami akan gagal
Bahagian terakhir fungsi ini menggunakan fungsi pakej ujian terbina dalam Pembersihan. Fungsi ini mendaftarkan fungsi yang akan dipanggil apabila semua fungsi dalam ujian telah dilaksanakan. Dalam kes contoh kami di sini, kami menggunakan fungsi untuk mengosongkan data pengguna yang digunakan semasa pelaksanaan fungsi ujian ini.
Untuk ujian unit kami, kami akan menguji pengendali laluan untuk api kami. Dalam kes ini laluan yang berkaitan dengan entiti pengguna. Perhatikan di bawah.
// 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) }
Kami mempunyai 3 fungsi di sini yang ingin kami uji
Fungsi handleGetUser dalam pengendali ini mendapatkan semula butiran pengguna berdasarkan ID pengguna yang disediakan dalam URL permintaan HTTP. Ia bermula dengan mengekstrak userID daripada pembolehubah laluan permintaan menggunakan penghala mux. Jika ID pengguna tiada atau tidak sah (bukan integer), ia bertindak balas dengan ralat 400 Permintaan Buruk. Setelah disahkan, fungsi memanggil kaedah GetUserByID pada stor data untuk mendapatkan maklumat pengguna. Jika ralat berlaku semasa mendapatkan semula, ia mengembalikan Ralat Pelayan Dalaman 500. Apabila berjaya, ia bertindak balas dengan status 200 OK, menghantar butiran pengguna sebagai JSON dalam badan respons.
Seperti yang dinyatakan sebelum ini untuk anda menguji fungsi pengendali yang kami perlukan untuk mencipta routes_test.go. Lihat saya di bawah
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
Fungsi Pengendali Baharu kami memerlukan stor pengguna sebagai parameter untuknya mencipta struct pengendali.
Oleh kerana kita tidak memerlukan stor sebenar, kita mencipta struct olok-olok dan mencipta fungsi penerima yang mengejek fungsi struct sebenar. Kami melakukan ini kerana kami mengendalikan ujian fungsi kedai secara berasingan oleh itu kami tidak perlu menguji bahagian kod itu dalam ujian pengendali.
Fungsi ujian TestGetUserHandler menguji dua senario kes, yang pertama cuba mendapatkan pengguna tanpa memberikan id pengguna
// 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) } }
Ujian dijangka lulus jika permintaan http membalas dengan 400 kod status.
Senario kes ujian kedua ialah kes di mana kami mendapatkan semula maklumat pengguna dengan menggunakan url yang betul yang mengandungi id pengguna yang sah. Dalam kes ujian ini kami menjangkakan respons dengan 200 kod status. Jika tidak ujian itu akan gagal.
// 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) }
Kami telah berjaya melaksanakan ujian unit dalam projek kami dengan mencipta ujian untuk pengendali laluan kami. Kami telah melihat cara menggunakan olok-olok untuk hanya menguji unit kecil kod. Kami telah dapat membangunkan ujian integrasi untuk fungsi kami yang berinteraksi dengan Postgresql DB.
Jika anda mahukan sedikit masa dengan kod projek klon repo dari github di sini
Atas ialah kandungan terperinci Menguji REST API dalam Go: Panduan untuk Ujian Unit dan Penyepaduan dengan Perpustakaan Ujian Standard Go. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!