跳至主要內容
  • Hostloc 空間訪問刷分
  • 售賣場
  • 廣告位
  • 賣站?

4563博客

全新的繁體中文 WordPress 網站
  • 首頁
  • go 有没有什么优雅的办法可以进行单元测试?
未分類
6 9 月 2020

go 有没有什么优雅的办法可以进行单元测试?

go 有没有什么优雅的办法可以进行单元测试?

資深大佬 : pkoukk 1

刚刚入坑 golang 没多久,用 go 写了一个小的项目,逐渐感受到了 go 的特性和优点
为了增加项目的可靠性,想写一点单元测试,但是发现了重重阻碍,主要是在 mock 的时候实在是太复杂了
个人体验上感觉,无法做到无侵入性的 mock 某些 func 或者 struct 。
下面写一下自己的做法,不知道是因为我原本代码结构设计的就不对还是 mock 的姿势不对,希望大家指正

函数 mock 。

在一般的使用中假设是这样的

func BaseFunc() {  info := getInfo("123")  fmt.Printf(info) }  func getInfo(name string) string {  return name + ".cn" }  func usage() {  BaseFunc() }  

如果我需要 mock,就得

type getInfoFunc func(string) string  func BaseWithMock(getInfoV getInfoFunc) {  info := getInfoV("123")  fmt.Printf(info) }  func mockGetInfo(name string) string {  return name + ".com" }  func usage() {     // 正常调用     BaseWithMock(getInfo)     // mock  BaseWithMock(mockGetInfo) } 

那么就存在问题了,如果我的 baseFunc 当中有很多数据库或者 API 接口,在单元测试的时候我需要 mock 他们的数据,
我就必须要定义很多个 type,然后在 BaseFunc 的参数里传进来么?感觉这么做很不优雅。

如果试图去 mock 一个对象,我感觉就更复杂了..
以下是某种简化过的场景..
假设 ServiceRecord 代表一系列数据库和 API 等数据操作
Service 则代表具体处理的对象,那么如果 Service 需要通过 ServiceRecord 读取某些基础信息的场景。
一般情况下,我是这样写的

type ServiceRecord struct {  Name   string  Fields map[string]string }  func (s *ServiceRecord) LoadFields() {  // some database work  result := map[string]string{   "name":    "jack",   "address": "no.1 jack street",   "remark":  "none",  }  s.Fields = result }  type Service struct {  Name  string  Owner string }  func (s *Service) ReadMoreInfo() {  r := &ServiceRecord{Name: s.Name}  r.LoadFields()  s.Owner = r.Fields["name"] }  func usage(serviceName string) {  s := &Service{Name: serviceName}  s.ReadMoreInfo()  fmt.Print(s.Owner) }  

如果需要进行单元测试,我们需要 mock 掉数据层,也就是 ServiceRecord 这个对象。 一般是通过 Interface 来实现这件事情。

type ServiceRecordInterface interface {     LoadFields()     // 因为 Interface 本身不包含数据,所以原来的所有直接访问属性的地方,都必须使用函数来实现  GetField(string) string }  func (s *ServiceRecord) GetField(fieldName string) string {  return s.Fields[fieldName] }  // 为了 Mock,需要通过参数把接口传进来 func (s *Service) ReadMoreInfo(serviceRecord ServiceRecordInterface) {  serviceRecord.LoadFields()  s.Owner = serviceRecord.GetField("name") }  // 正常调用时 func usage(serviceName string) {  s := &Service{Name: serviceName}  s.ReadMoreInfoForMock(&ServiceRecord{Name: serviceName})  fmt.Printf(s.Owner) }  // mock 对象 type ServiceRecordMock struct {  ServiceRecord }  // mock 掉具体的函数实现 func (srm *ServiceRecordMock) LoadFields() {  result := map[string]string{   "name":    "tony",   "address": "no.1 tony street",   "remark":  "none",  }  srm.Fields = result }  // mock 时 func mockUsage(serviceName string) {  s := &Service{Name: serviceName}  s.ReadMoreInfoForMock(&ServiceRecordMock{ServiceRecord{Name: serviceName}})  fmt.Printf(s.Owner) }  

可以看出这仍然对原来的代码产生了很大的影响,为了满足可 mock,必须要声明一个接口,而且必须把这个接口抽离出来,作为参数注入到调用对象里面去。
让我觉得很尴尬的是,采用接口之后,必须通过函数去 get 或者 set 一个属性,感觉很不优雅,产生了很多没有必要的垃圾代码。

大佬有話說 (14)

  • 資深大佬 : wangsyi13

    关注一下,我也有这个问题,我最近转 go,接触新项目,想写些单元测试,发现只要涉及业务的就关联太多,很难实现,可能项目设计之初就没有考虑单元测试,但是如果考虑的话,项目结构设计应该遵循哪些原则呢?

  • 資深大佬 : abser

    单元测试需要 mock 的很少, 一般直接测试函数输入输出, 直接给用例测.

  • 主 資深大佬 : pkoukk

    @abser 那有数据或者外部依赖的函数怎么办呢?不测了么?…

  • 資深大佬 : cloudzhou

    关于 Go 的单元测试,一下是我的几个思考点:

    1. 基于 interface 的 mock,这是面向接口可插拔,已经很成熟,就是实现对应的 mock 接口实现注入,具体方法 mock,具体不说了

    2. 面向方法的 mock,使用 context 模型,进行 mock 方法注入

    举个例子:

    https://play.golang.org/p/z5RDSVcTSWD

  • 資深大佬 : cloudzhou

    @pkoukk 以上,可以做到针对函数精确的 mock,按需实现

  • 資深大佬 : vvmint233

    面向数据库的 mock 的话直接 mock 掉底层的 db 对象, 底层的 db 对象不是 interface 的话我一般本地起一个数据库或者 sqlite3 做 mock 数据源, 这样就只需要构造数据或者 sql 文件, 缺点是对 ci/cd 不友好. 不过你可以检查你 io 部分的函数有没有问题, 逻辑部分的函数直接构造输入输出 Test 就好了

  • 資深大佬 : wzw

    看看 goframe

  • 主 資深大佬 : pkoukk

    @vvmint233 是的..只不过这么做更像是跑在本地的集成测试了,不那么像单测了

  • 主 資深大佬 : pkoukk

    @wzw https://goframe.org/quality/unittest
    三个字,待完善

  • 資深大佬 : cloudzhou

    @vvmint233 @pkoukk 资源性的依赖,我更倾向不要 mock 资源,而是按需构建资源,其实代价很小
    我做过一个测试项目,就是将所有资源 Docker 化,并且做好初始,结束动作
    Mysql / Redis 都是调用 Docker,“资源即服务”,甚至可以构建一个 pool 来重复利用

    原因在于,如果资源 mock,可以不能测试到使用资源的错误,比如一条 SQL 语句其实错了,但是不能发现

  • 資深大佬 : crclz

    java 和 c#的特性才刚刚足够使用。对于 golang 这种丐版的语言,你还期望什么?

  • 資深大佬 : wzw

    @pkoukk #9 没文档而已吧, 你看看例子

  • 資深大佬 : limboMu

    跑单元测试说明需要 mock 说明你的方法本身就不是纯函数,总归是需要一个环境(ctx)执行的.

  • 資深大佬 : rim99

    看看上的讨论,也有了些结论:
    1. 对外部环境、框架有严重依赖的代码,应该从核心业务逻辑中抽离出去,用集成测试覆盖。
    2. 核心业务内部使用接口完成逻辑的组织,用单元测试覆盖。

文章導覽

上一篇文章
下一篇文章

AD

其他操作

  • 登入
  • 訂閱網站內容的資訊提供
  • 訂閱留言的資訊提供
  • WordPress.org 台灣繁體中文

51la

4563博客

全新的繁體中文 WordPress 網站
返回頂端
本站採用 WordPress 建置 | 佈景主題採用 GretaThemes 所設計的 Memory
4563博客
  • Hostloc 空間訪問刷分
  • 售賣場
  • 廣告位
  • 賣站?
在這裡新增小工具