Go-kratos 框架商城微服務實戰之使用者服務 (四)

Aliliin發表於2022-02-12

Go-kratos 框架商城微服務實戰之使用者服務 (四)

這篇主要引入測試服務。寫的不清晰的地方可看原始碼 , 歡迎大佬指教。

注:豎排 … 程式碼省略,為了保持文章的篇幅簡潔,我會將一些不必要的程式碼使用豎排的 . 來代替,你在複製本文程式碼塊的時候,切記不要將 . 也一同複製進去。

編寫單元測試 ginkgo 的用法
  1. 引入測試所需的包

注:這裡用到了gomockginkgo 不熟悉的可以熟悉一下相關文件

  • 匯入 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 協議》,轉載必須註明作者和本文連結
高永立

相關文章