go 有没有什么优雅的办法可以进行单元测试?
刚刚入坑 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 一个属性,感觉很不优雅,产生了很多没有必要的垃圾代码。