关于数据初始化和销毁
既然我选择使用本地DB进行测试,那么按照逻辑,需要在测试用例开始初始化DB数据,然后在测试用例结束后销毁数据。这里我还选择在测试用例开始的时候,先销毁数据,然后初始化数据,测试用例结束的时候不要销毁数据。这样做我承认有不好的地方,就是有可能会有脏数据。比较好的地方,就是我在单个测试用例跑完的时候,我有机会去数据库看一眼现在数据库里面的测试数据是什么样子。
不管怎么洋,数据初始化和销毁的工作就变得异常重要了,它们必须是幂等,而且可以循环幂等。(销毁-初始化)=(销毁-销毁-初始化)=(初始化-销毁-初始化)。要做到这个我的感受必须借助具体的业务数据表逻辑了。比如我的所有数据表都有一个路口id的字段,那么我就很容易做到销毁的幂等,我每次销毁的时候,就只要把这个路口的所有数据删除就可以了。如果没有的话,由于我们的数据库是本地数据库,不妨采用整个数据表清空的方式操作。
数据初始化和销毁的函数我封装成两个函数,放在一个包里面
var (
SignalID = int64(999999)
LogicJunctionId = "test_junction"
)
// 创建测试数据
func CreateTestData(db *gorm.DB) {
// SignalInfo表创建一条数据
signalInfo := &models.SignalInfo{}
signalInfo.Id = SignalID
signalInfo.Name = "测试路口id"
signalInfo.LogicJunctionId = LogicJunctionId
signalInfo.Status = 1
db.Create(signalInfo)
}
// 销毁测试数据
func DestroyTestData(db *gorm.DB) {
db.Delete(&models.SignalInfo{}, "logic_junctionid=" + LogicJunctionId)
...
}
然后把上面说的初始化操作封装成一个函数
var HasSetup = false
// signalEdit初始化,只调用一次
func SetUpSignalEdit() {
if HasSetup == false {
gin.SetMode(gin.TestMode)
confPath := os.Getenv("CONFIG_PATH")
commonlib.Init(confPath, "")
conf.ParseLocalConfig()
db.InitDB()
HasSetup = true
}
DestroyTestData(db.EditDB)
CreateTestData(db.EditDB)
}
所有测试用例都先调用下这个函数
func TestGetGroups(t *testing.T) {
test.SetUpSignalEdit()
...
}
这里真心要吐槽下testing框架,既然做了测试框架,SetUp函数,SetDown函数这些都不考虑,和主流的测试框架的思想真的有点偏差,导致像这种“普通”的初始化的需求都要自己写方法来绕过,至少testing框架为应用思考的东西还是太少了。
测试用例的粒度
我一直知道写好测试用例是一个难度不亚于开发的工作。测试用例有粒度问题,我觉得,测试用例的粒度宜大不宜小。我这个项目是controller-service-mmodels架构,controller一个函数就是一个接口,service一个函数是一个通用性比较高的服务,model是比较瘦的model,基本只做增删改查。在我这个架构中,我写的测试用例粒度大多数是controller级别的,有少数是service级别的,model级别的测试用例基本没有。
测试用例粒度大一些,有个明显的好处,就是对需求的容忍度高了很多。一般测试用例最痛的就是需求一旦修改了,我的业务逻辑就修改了,我的测试用例也要跟着修改。修改测试用例是很痛苦的事情。所以如果测试用例足够大,比如和接口一样大,那么基本上,由于业务接口的兼容性要求,我们的测试用例的输入输出一般不会进行大的变动(虽然里面的service或者model会进行比较大的变动)。这样有一些需求变化了之后,我甚至不需要修改任何测试用例的代码就可以。
当然有的测试用例粒度太大,一些小的分支可能就测试不到,或者很难构建测试数据,所以有的时候,还是需要一写稍微小一点的粒度的测试用例。
另外对于不需要依赖测试数据的类库函数,如果你对这个类库函数的输入输出的需求变更有把握控制的话,(你需要对自己的这个判断负责)这种类库函数的测试用例则是越细越好。
其他原则性的东西
说说写测试用例的一些原则性的东西。
1、检验逻辑抗需求变更能力越强越好
首先,测试用例的检验逻辑不是越全越好,而且有很多技巧。比如一个插入的接口,你测试是否插入成功,有很多时候,你根据判断插入条数是否多一条会比你判断这个插入条数的所有字段是否是你要求的更好。原则还是那个,测试用例的抗需求变更能力会更高,首先基本上如果我的插入逻辑很简单,那么插入成功就约等于插入的每个字段都满足,当然这里是约等于,但是因为业务代码也是我自己写的,心里这个B数还是有的。然后,如果一旦需求变更我这个数据多了一个字段,那么我这个测试用例基本不需要做任何修改就还可以继续跑起来。
再次强调下,这里的约等于的判断就是看你对你业务代码的感觉了。
2、并不是所有的错误都需要完美处理
测试代码毕竟不像业务代码那么需要完美的严谨,所有的panic都是欢迎的。换句话说,我们业务代码基本上对所有error都需要有所处理,但是测试用例并不一定了。如果我在上一行代码中没有处理这个error,那么我传递给下一行的参数很可能就是nil,很有可能在下一行代码中直接panic了一个错误出来。这个也能让我发现我的错误。
所以,测试用例并不需要写那么严谨,有的地方直接panic错误也是一个很好的选择。
3、Fatal和Error的选择
基本上我觉得Error没啥用,我目前的测试用例都要求所有的判断节点都跑成功,任何一个地方失败了,直接就报错进行调试。我的精力也不允许我一次性能处理多个错误case,基本上调试失败的测试用例是一个个调试的,所以error并没有什么用。
这点纯粹我个人观点,估计会有很多人不同意。
4、检验逻辑多用变量
检验逻辑尽量少用 response.Name == "测试路口" 这种代码,能尽量找到替换"测试路口" 这个的变量尽量使用变量,同样的理由,测试用例的抗需求变更能力会更高。
总结
测试用例是开发人员最后一块遮羞布,写Golang的代码和写PHP的代码确实体验完全不一样,在Golang代码中,首先写测试用例异常方便了。其次,Golang的调试成本远远高于PHP,写测试用例看起来是浪费时间,实际上是节省你的调试时间。最后,golang代码的每次重构(增加一个字段,少一个字段)影响的文件数远远高于PHP,如果没有这块遮羞布,你怎么确保你的代码修改后还能正常运行呢?
Just Testing!