Rumah >pembangunan bahagian belakang >Golang >Menguji REST API dalam Go: Panduan untuk Ujian Unit dan Penyepaduan dengan Perpustakaan Ujian Standard Go

Menguji REST API dalam Go: Panduan untuk Ujian Unit dan Penyepaduan dengan Perpustakaan Ujian Standard Go

Barbara Streisand
Barbara Streisandasal
2024-11-17 01:33:03299semak imbas

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

pengenalan

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.

Menyediakan

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.

Ujian Integrasi

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:

  • CreateUser
  • GetUserByEmail
  • GetUserById

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.

Fungsi

TestCreateUser 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.

Ujian Unit

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

  • HandleLogin
  • Daftar Pengendali
  • HandleGetUser

HandleGetUser

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)
}

Kesimpulan

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!

Kenyataan:
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn