Go-kratos 框架商城微服務實戰之使用者服務 (四)
這篇主要引入測試服務。寫的不清晰的地方可看原始碼 , 歡迎大佬指教。
注:豎排 … 程式碼省略,為了保持文章的篇幅簡潔,我會將一些不必要的程式碼使用豎排的 . 來代替,你在複製本文程式碼塊的時候,切記不要將 . 也一同複製進去。
編寫單元測試 ginkgo 的用法
- 引入測試所需的包
- 匯入 gomock、ginkgo、gomega
// gomock 主要包含兩個部分:gomock庫和輔助程式碼生成工具 mockgen
go get github.com/golang/mock
go get github.com/golang/mock/gomock
// ginkgo 包 它會獲取 Ginkgo 並在 $GOPATH/bin 下安裝 ginkgo 可執行檔案
go get github.com/onsi/ginkgo/v2/ginkgo
go install github.com/onsi/gomega
- 新增
service/user/internal/data/docker_mysql.go
檔案,啟動 Docker 容器並將其用於測試
package data
import (
"database/sql"
"fmt"
"github.com/ory/dockertest/v3" // 注意這個包的引入
"log"
"time"
)
func DockerMysql(img, version string) (string, func()) {
return innerDockerMysql(img, version)
}
// 初始化 Docker mysql 容器
func innerDockerMysql(img, version string) (string, func()) {
// uses a sensible default on windows (tcp/http) and linux/osx (socket)
pool, err := dockertest.NewPool("")
pool.MaxWait = time.Minute * 2
if err != nil {
log.Fatalf("Could not connect to docker: %s", err)
}
time.Sleep(time.Second * 20) // 可能回
// pulls an image, creates a container based on it and runs it
resource, err := pool.Run(img, version, []string{"MYSQL_ROOT_PASSWORD=secret", "MYSQL_ROOT_HOST=%"})
if err != nil {
log.Fatalf("Could not start resource: %s", err)
}
conStr := fmt.Sprintf("root:secret@(localhost:%s)/mysql?parseTime=true", resource.GetPort("3306/tcp"))
if err := pool.Retry(func() error {
var err error
db, err := sql.Open("mysql", conStr)
if err != nil {
return err
}
return db.Ping()
}); err != nil {
log.Fatalf("Could not connect to docker: %s", err)
}
// 回撥函式關閉容器
return conStr, func() {
if err = pool.Purge(resource); err != nil {
log.Fatalf("Could not purge resource: %s", err)
}
}
}
參考 ginkgo 官方文件,編寫測試檔案
新建
service/user/internal/data/data_suite_test.go
這裡只是測試資料庫的連結
package data_test
import (
"context"
"github.com/pkg/errors"
"gorm.io/gorm"
"testing"
"user/internal/biz"
"user/internal/conf"
"user/internal/data"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
// 測試 data 方法
func TestData(t *testing.T) {
// Ginkgo 測試通過呼叫 Fail(description string) 功能來表示失敗
// 使用 RegisterFailHandler 將此函式傳遞給 Gomega 。這是 Ginkgo 和 Gomega 之間的唯一連線點
RegisterFailHandler(Fail)
// 通知 Ginkgo 啟動測試套件。如果您的任何 specs 失敗,Ginkgo 將自動使 testing.T 失敗。
RunSpecs(t, "biz data test user")
}
var cleaner func() // 定義刪除 mysql 容器的回撥函式
var Db *data.Data // 用於測試的 data
var ctx context.Context // 上下文
// initialize AutoMigrate gorm自動建表
func initialize(db *gorm.DB) error {
err := db.AutoMigrate(
&biz.User{},
)
return errors.WithStack(err)
}
// ginkgo 官方格式方法
var _ = BeforeSuite(func() {
// 執行測試資料庫操作之前,連結之前 docker 容器建立的 mysql
con, f := data.DockerMysql("mysql", "latest")
cleaner = f // 測試完成,關閉容器的回撥方法
config := &conf.Data{Database: &conf.Data_Database{Driver: "mysql", Source: con}}
db := data.NewDB(config)
mySQLDb, _, err := data.NewData(config, nil, db, nil)
if err != nil {
return
}
if err != nil {
return
}
Db = mySQLDb
err = initialize(db)
if err != nil {
return
}
Expect(err).NotTo(HaveOccurred())
})
// 測試結束後 通過回撥函式,關閉並刪除 docker 建立的容器
var _ = AfterSuite(func() {
cleaner()
})
- 新增
service/user/internal/testdata/user.go
檔案,編寫用於測試的模擬資料
package testdata
import (
"gorm.io/gorm"
"time"
"user/internal/biz"
)
func User(id ...int64) *biz.User {
user := &biz.User{
ID: 1,
Mobile: "13509876789",
Password: "admin",
NickName: "aliliin",
Birthday: nil,
Role: 0,
CreatedAt: time.Time{},
UpdatedAt: time.Time{},
DeletedAt: gorm.DeletedAt{},
IsDeletedAt: false,
}
if len(id) > 0 {
user.ID = id[1]
}
return user
}
- 新增
service/user/internal/data/user_test.go
檔案,用於測試具體入庫的方法
package data_test
import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"time"
"user/internal/biz"
"user/internal/data"
"user/internal/testdata"
)
var _ = Describe("User", func() {
var ro biz.UserRepo
var uD *biz.User
BeforeEach(func() {
ro = data.NewUserRepo(Db, nil)
// 這裡你可以不引入外部組裝好的資料,可以在這裡直接寫
uD = testdata.User()
})
// 設定 It 塊來新增單個規格
It("CreateUser", func() {
u, err := ro.CreateUser(ctx, uD)
Ω(err).ShouldNot(HaveOccurred())
// 組裝的資料 mobile 為 13509876789
Ω(u.Mobile).Should(Equal("13509876789")) // 手機號應該為建立的時候寫入的手機號
})
// 設定 It 塊來新增單個規格
It("ListUser", func() {
user, total, err := ro.ListUser(ctx, 1, 10)
Ω(err).ShouldNot(HaveOccurred()) // 獲取列表不應該出現錯誤
Ω(user).ShouldNot(BeEmpty()) // 結果不應該為空
Ω(total).Should(Equal(1)) // 總數應該為 1,因為上面只建立了一條
for _, u := range user {
Ω(u.ID).Should(Equal(int64(1))) // ID 應該為 int64 型別的1
Ω(u.Mobile).Should(Equal("13509876789"))
}
})
// 設定 It 塊來新增單個規格
It("UpdateUser", func() {
birthDay := time.Unix(int64(693646426), 0)
uD.NickName = "gyl"
uD.Birthday = &birthDay
uD.Gender = "female"
user, err := ro.UpdateUser(ctx, uD)
Ω(err).ShouldNot(HaveOccurred()) // 更新不應該出現錯誤
Ω(user).Should(BeTrue()) // 結果應該為 true
})
It("CheckPassword", func() {
p1 := "admin"
encryptedPassword := "$pbkdf2-sha512$5p7doUNIS9I5mvhA$b18171ff58b04c02ed70ea4f39bda036029c107294bce83301a02fb53a1bcae0"
password, err := ro.CheckPassword(ctx, p1, encryptedPassword)
Ω(err).ShouldNot(HaveOccurred()) // 密碼驗證通過
Ω(password).Should(BeTrue()) // 結果應該為true
encryptedPassword1 := "$pbkdf2-sha512$5p7doUNIS9I5mvhA$b18171ff58b04c02ed70ea4f39"
password1, err := ro.CheckPassword(ctx, p1, encryptedPassword1)
Ω(err).ShouldNot(HaveOccurred())
Ω(password1).Should(BeFalse()) // 密碼驗證不通過
})
})
驗證單元測試
- 修改
service/user/Makefile
檔案
.
.
.
.PHONY: test # 新增測試命令
test:
ginkgo -r -cover -v .
.
.
.
- 根目錄
service/user
執行測試命令make test
看到如下結果:
提示 packets.go:37 : unexpected EOF 錯誤,是因為docker模擬資料庫連結導致的,目前還沒找到解決辦法。
這裡你也可以使用IDE的提示進行測試此檔案
service/user/internal/data/user_test.go
編輯器頂部回有執行的提示
如圖顯示 4 個測試通過,0 個測試失敗。
編寫單元測試 mock 的用法
- 用 mock 物件模擬依賴項的行為
具體的使用方法可以參考這篇文章
- 修改
service/user/internal/biz/user.go
檔案
package biz
.
.
.
func (User) TableName() string {
return "users"
}
// 注意這裡新增的 mock 資料的命令
//go:generate mockgen -destination=../mocks/mrepo/user.go -package=mrepo . UserRepo
type UserRepo interface {
.
.
.
}
.
.
.
- 進入
service/user/internal/biz
目錄執行下列命令
這裡是用 gomock 提供的 mockgen 工具生成要 mock 的介面的實現,在生成 mock 程式碼的時候,我們用到了 mockgen 工具,這個工具是 gomock 提供的用來為要mock的介面生成實現的。它可以根據給定的介面,來自動生成程式碼。
// 也可以在編輯器中的 user.go 檔案中執行
mockgen -destination=../mocks/mrepo/user.go -package=mrepo . UserRepo
// 生成的檔案目錄結構如下
service/user/internal/mocks
└── mrepo
└── user.go
- 新增
service/user/internal/biz/user_test.go
檔案
package biz_test
import (
"context"
"github.com/golang/mock/gomock"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"testing"
"user/internal/biz"
"user/internal/mocks/mrepo"
"user/internal/testdata"
)
func TestBiz(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "biz user test")
}
var ctl *gomock.Controller
var cleaner func()
var ctx context.Context
var _ = BeforeEach(func() {
ctl = gomock.NewController(GinkgoT())
cleaner = ctl.Finish
ctx = context.Background()
})
var _ = AfterEach(func() {
// remove any mocks
cleaner()
})
var _ = Describe("UserUsecase", func() {
var userCase *biz.UserUsecase
var mUserRepo *mrepo.MockUserRepo
BeforeEach(func() {
mUserRepo = mrepo.NewMockUserRepo(ctl)
userCase = biz.NewUserUsecase(mUserRepo, nil)
})
It("Create", func() {
info := testdata.User()
mUserRepo.EXPECT().CreateUser(ctx, gomock.Any()).Return(info, nil)
l, err := userCase.Create(ctx, info)
Ω(err).ShouldNot(HaveOccurred())
Ω(err).ToNot(HaveOccurred())
Ω(l.ID).To(Equal(int64(1)))
Ω(l.Mobile).To(Equal("13509876789"))
})
It("List", func() {
info := testdata.User()
info1 := testdata.User()
info1.ID = 2
info1.Mobile = "2323232323"
u := []*biz.User{
info,
info1,
}
mUserRepo.EXPECT().ListUser(ctx, 1, 1).Return(u, 2, nil)
list, total, err := userCase.List(ctx, 1, 1)
if err != nil {
return
}
Ω(err).ToNot(HaveOccurred())
Ω(total).Should(Equal(2))
Ω(list).ShouldNot(BeEmpty())
Ω(list[0].ID).To(Equal(int64(1)))
Ω(list[1].ID).To(Equal(int64(2)))
Ω(list[0].Mobile).To(Equal("13509876789"))
Ω(list[1].Mobile).To(Equal("2323232323"))
})
})
驗證單元測試
- 根目錄
service/user
執行測試命令make test
驗證是否都通過了
本作品採用《CC 協議》,轉載必須註明作者和本文連結