golang 读取文件最后一行_测试用例是开发人员最后一块遮羞布

5dde59cc8cbb3c744911d3349088b55b.gif

测试用例是开发人员最后一块遮羞布

最近一周写一个比较复杂的业务模块,越写到后面真心越心虚。操作越来越复杂了,代码也逐渐凌乱了起来。比如一个接口,传入的是一个比较复杂的大json,我需要解析这个大json,然后根据json中字段进行增删改查,调用第三方服务等操作。告诉前端接口已经完成的时候,总是有点没有底气。说实话,在写PHP的时候,我确实很少写单元测试,大都是对着页面进行一波一波的测试,现在想想,一个是懒,还有一个是确实PHP是不需要编译的语言,没有编译时间,测试-修正,整个流程非常短。但是这次是一个比较大的GoLang项目,如果还是按照“编译-起服务-调用-调整代码-编译-起服务-调用-...” 这种循环来做调试,真是会疯了的。所以我能静下心好好研究研究如何写Golang的单元测试了。

数据库怎么办?

这个是第一个需要思考的问题。这个问题和语言无关。一旦有数据库操作,就需要考虑如何在测试用例中如何处理数据库操作。我想了想,无外乎两种做法,一种是直接mock数据库的返回对象。另外一种,是搭建一个测试DB,然后灌入假数据,进行测试。这两种方式我选择了后一种。有几个理由:首先,mock数据库返回数据是一个比灌入DB数据更为复杂的逻辑,数据库返回的数据根据sql各种各样,要想在每个环节都写好数据库操作返回,倒不如我直接伪造一些数据来的方便。其次,mock数据库返回会丢失model层的测试逻辑,当然如果你是轻model层,整个model就只有一个orm,这个可能就不是理由了。

所以,我操作的第一步,从线上把数据库表结构copy一份到我本地vagrant的mysql中。

这里必须要注意,你的测试数据库和测试代码最好是同一个机器上,否则每跑一个测试用例,消耗的时间非常大,你的测试体验也不会太好。

第三方请求怎么办?

我的代码逻辑中也有一些第三方调用,调用其他服务。当然这里也有同样的两种办法,一种是直接在本地测试环境搭建第三方服务,另外一种是mock第三方服务的返回数据。这里我选择了mock数据的方式。基本想法是因为我这个测试毕竟不是一种全链路测试,测试的主体还是我的服务,我的服务基本上只包含服务+DB,如果要搭建第三方服务,这就有点舍本逐末的感觉了。

好了,如何mock第三方服务呢?

查了下golang中mock的包有两个比较出名,一个是golang官网出品的golang/mock,另外一个是monkey(https://github.com/bouk/monkey)。两个相比之下,我感觉golang/mock是师出有名,但是不如monkey好用,monkey属于黑科技,使用修改函数指针的方式进行mock函数。我想了想,实用第一位,投入了monkey的怀抱。

基本使用代码如下:

// mock路网接口

guard := monkey.Patch(lib.Curl, func(trace *lib.TraceContext, CurlType, urlString string, data url.Values, addToken bool) ([]byte, error) {

return []byte("{[\"10010\":\"后厂村路\"}"]), nil

})

defer guard.Unpatch()

将lib.Curl整个函数给mock了,并且在函数结束后修改mock的函数,保证不影响其他测试用例。

配置文件怎么办?

web服务一般都会有读取配置的代码,我的服务是读取一个参数config=base.json来进行配置的读取的。go test中是没有办法给test的代码传递参数的,(我看网上的一些文章说有个-args的参数,但是我在go1.11版本中确实没有看到这个参数)。于是我只能选择使用环境变量的方式。在运行go test的时候,在最开头的部分设置下当前这个go test的环境变量CONFIG_PATH,然后修改下我的初始化配置文件的代码,允许传入参数进行配置文件的读取。

大概代码如下:

在运行go test的时候设置环境变量:

CONFIG_PATH=/home/vagrant/foo/conf/yejianfeng/base.json go test foo/signaledit/... -v -test.run TestGetGroups

测试环境的初始化配置文件逻辑:

package test

import (

..

)

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)

}

web怎么进行单元测试?

关于这个,httptest这个包提供给我们想要的逻辑了,网上的文章也一大堆了。使用起来也是很方便,

router := gin.New()

jc := Controller{}

// 灯组模型表获取信息

router.GET("/group/all", jc.GroupAll)

...

//构建返回值

w := httptest.NewRecorder()

//构建请求

r, _ := http.NewRequest("GET", "/group/all?logic_junction_id=test_junction", nil)

//调用请求接口

router.ServeHTTP(w, r)

resp := w.Result()

body, _ := ioutil.ReadAll(resp.Body)

就没有什么好说的了。

关于数据初始化和销毁

既然我选择使用本地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会进行比较大的变动)。这样有一些需求变化了之后,我甚至不需要修改任何测试用例的代码就可以。

当然有的测试用例粒度太大,一些小的分支可能就测试不到,或者很难构建测试数据,所以有的时候,还是需要一写稍微小一点的粒度的测试用例。

另外对于不需要依赖测试数据的类库函数,如果你对这个类库函数的输入输出的需求变更有把握控制的话,(你需要对自己的这个判断负责)这种类库函数的测试用例则是越细越好。

其他原则性的东西

说说写测试用例的一些原则性的东西。

检验逻辑抗需求变更能力越强越好

首先,测试用例的检验逻辑不是越全越好,而且有很多技巧。比如一个插入的接口,你测试是否插入成功,有很多时候,你根据判断插入条数是否多一条会比你判断这个插入条数的所有字段是否是你要求的更好。原则还是那个,测试用例的抗需求变更能力会更高,首先基本上如果我的插入逻辑很简单,那么插入成功就约等于插入的每个字段都满足,当然这里是约等于,但是因为业务代码也是我自己写的,心里这个B数还是有的。然后,如果一旦需求变更我这个数据多了一个字段,那么我这个测试用例基本不需要做任何修改就还可以继续跑起来。

再次强调下,这里的约等于的判断就是看你对你业务代码的感觉了。

并不是所有的错误都需要完美处理

测试代码毕竟不像业务代码那么需要完美的严谨,所有的panic都是欢迎的。换句话说,我们业务代码基本上对所有error都需要有所处理,但是测试用例并不一定了。如果我在上一行代码中没有处理这个error,那么我传递给下一行的参数很可能就是nil,很有可能在下一行代码中直接panic了一个错误出来。这个也能让我发现我的错误。

所以,测试用例并不需要写那么严谨,有的地方直接panic错误也是一个很好的选择。

Fatal和Error的选择

基本上我觉得Error没啥用,我目前的测试用例都要求所有的判断节点都跑成功,任何一个地方失败了,直接就报错进行调试。我的精力也不允许我一次性能处理多个错误case,基本上调试失败的测试用例是一个个调试的,所以error并没有什么用。

这点纯粹我个人观点,估计会有很多人不同意。

检验逻辑多用变量

检验逻辑尽量少用 response.Name == "测试路口" 这种代码,能尽量找到替换"测试路口" 这个的变量尽量使用变量,同样的理由,测试用例的抗需求变更能力会更高。

总结

测试用例是开发人员最后一块遮羞布,写Golang的代码和写PHP的代码确实体验完全不一样,在Golang代码中,首先写测试用例异常方便了。其次,Golang的调试成本远远高于PHP,写测试用例看起来是浪费时间,实际上是节省你的调试时间。最后,golang代码的每次重构(增加一个字段,少一个字段)影响的文件数远远高于PHP,如果没有这块遮羞布,你怎么确保你的代码修改后还能正常运行呢?

Just Testing!

▌上图

b1081bfc412d7b8d089741d31919887e.png

▌你可能对这些文章感兴趣

 vimium使用

② 聊聊osm

③ golang的cms

④ 一次composer错误使用引发的思考

⑤ colly源码学习

⑥ 使用chan的时候选择对象还是指针

⑦ golang中Context的使用场景

⑧ 论凛冬

⑨ 如何加速golang业务的开发速度

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/409816.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

android 进度条_Android仿水波纹流球进度条控制器,实现高端大气的主流特效

今天看到一个效果挺不错的,就模仿了下来,加上了一些自己想要的效果,感觉还不错的样子,所以就分享出来了,话不多说,上图CircleView这里主要是实现中心圆以及水波特效package com.lgl.circleview;import andr…

[html] iframe父页面如何获取子页面的元素?

[html] iframe父页面如何获取子页面的元素? 在父页面监听 onmessage,子页面 postMessage。$(iframe)[0].contentWindow.document.getElementByIddocument.frames[xx].document.getElementById个人简介 我是歌谣,欢迎和大家一起交流前后端知识…

怎么更换WIN7欢迎界面的背景图?

第一步:先单击“开端 →运转 ”,打开“运转 ”对话框并输入 “Regedit”(不包含外侧引号);接着单击“确定”按钮打开注册表编辑器,再定位到“HKEY_LOCAL_MACHINESOFTWARE\Microsoft\Windows\CurrentVersion…

docker php composer 使用_如何使用Docker部署PHP开发环境

本文主要介绍了如何使用Docker构建PHP的开发环境,文中作者也探讨了构建基于Docker的开发环境应该使用单容器还是多容器,各有什么利弊。推荐PHP开发者阅读。希望对大家有所帮助。环境部署一直是一个很大的问题,无论是开发环境还是生产环境&…

1-docker安装

一、看官方地址选择平台,我的是centos7 系统 https://docs.docker.com/install/linux/docker-ce/centos/二、安装依赖 sudo yum install -y yum-utils device-mapper-persistent-data lvm2三、添加软件仓库,我们这⾥使⽤稳定版 Docker,执⾏下…

from 下拉框多个值提交_Git提交规范

规范的作用大多数情况下,看提交历史的人跟提交代码的人都不是同一个人,当别人阅读你的提交历史时,他很可能是不知道具体代码细节的,你如何在最短的时间内让他一眼知道每次提交的意义:每次提交影响的具体范围&#xff1…

[html] 给内联元素加float与给块元素加float有什么区别?

[html] 给内联元素加float与给块元素加float有什么区别? 内联元素加float无效个人简介 我是歌谣,欢迎和大家一起交流前后端知识。放弃很容易, 但坚持一定很酷。欢迎大家一起讨论 主目录 与歌谣一起通关前端面试题

PHP---反射

所谓反射就是定义好一个类之后,通过ReflectionClass读取类的名字从而获取类结构信息的过程class mycoach {protected $name;protected $age;public function __construct($name,$age){$this->name $name;$this->age $age;}public function eat(){echo "…

python词云_python词云

python词云[编辑] 概述 python词云是一种构建词云的方法,利用通用的编程语言Python来做词云,虽然不如专用工具便捷,但是适用范围很广,满足了不同人对词云的个性化需求。 一.Python的介绍 Python是一种面向对象的解释型计算机程序设…

【物联网智能网关-03】GPRS模块中文短信收发

在去年年初,就已经推出V1.0.0的GPRS库,不过在这个版本上只是实现了西文短信收发和字符串方式的GPRS数据通信,功能还相对不完善(参见我以前的博文《GPRS通信实现》)。最近升级的版本,对以上功能进行了大幅度…

[html] html的img标签为什么要添加alt属性呢?

[html] html的img标签为什么要添加alt属性呢? alt 属性是一个必需的属性,它规定在图像无法显示时的替代文本。假设由于一些原因,用户无法查看图像,alt属性可以为图像提供替代的信息。比如:网速太慢src 属性中的错误浏…

蓄电池单格电压多少伏_蓄电池充电规范手册

很多用户在买完蓄电池之后第一想法就是赶紧充电,很多陋习让用户使用行为造成了新买的蓄电池没怎么用感觉就和旧的没啥区别。而且使用时间越来越短到头来企业还失去了很多的客户,德国阳光蓄电池手册整理整编了在不同的环境中我们该如何很好的去维护自己的…

1-1docker加速器

配置加速器 #编译配置 sudo vim /etc/docker/daemon.json#加入下面的数据{"registry-mirrors": ["https://docker.mirrors.aliyuncs.com"] }#阿里云的镜像: https://docker.mirrors.aliyuncs.com#docker-cn镜像:https://registry.do…

钉钉机器人关键词应答_除了用于电销,智能语音机器人可以应用哪些地方?

之前的文章探讨的是智能语音机器人在电销行业的应用,然而在实际的场景中,电销行业的应用只是大家所熟知的行业之一。对比于人工电销,使用智能语音机器人有着诸多优势,例如:工作效率高、意向筛选、电话录音并转化为文字…

[html] 举例说明实现文字贯穿线的方法有哪些?

[html] 举例说明实现文字贯穿线的方法有哪些&#xff1f; 就是这样吗&#xff0c;用 del 删除线 <del></del>个人简介 我是歌谣&#xff0c;欢迎和大家一起交流前后端知识。放弃很容易&#xff0c; 但坚持一定很酷。欢迎大家一起讨论 主目录 与歌谣一起通关前端…

python+html语音人物交互_将HTML输入传递到python脚本

我有一个Python脚本&#xff0c;我想创建一个非常简单的HTML表单&#xff0c;有3个字段(用户名、密码和ID)和一个submit按钮。在当我单击Submit时&#xff0c;我只想将这三个参数传递到Python脚本中并运行脚本。在我试着用CGI来做。我创建了一个cgi-bin文件夹&#xff0c;并在其…

1-2docker-基本的使用

1、Docker 官⽅提供了⼀个公共的镜像仓库 https://hub.docker.com2、获取镜像 docker pull [选项] [Docker Registry 地址[:端⼝]/]仓库名[:标签]3、运行镜像 docker run -it --rm ubuntu:16.04 /bin/bash -it&#xff1a;这是两个参数&#xff0c;⼀个是 -i&#xff1a;交互式…

[html] 说说你对html的嵌套规范的理解,都有哪些规范呢?

[html] 说说你对html的嵌套规范的理解&#xff0c;都有哪些规范呢&#xff1f; ul,li/ol,li/dl,dt,dd拥有父子级关系的标签&#xff1b;ul、ol下都只能跟li&#xff0c;dl下只能跟dt.dd。 p,dt,h标签里面不能嵌套块元素&#xff1b; a标签不能嵌套a&#xff1b; 行内元素不能嵌…

assert函数_PHP 之 assert()函数

assert()函数其实是一个断言函数。那么什么是断言呢&#xff1f;百度百科上是这么说的&#xff1a;编写代码时&#xff0c;我们总是会做出一些假设&#xff0c;断言就是用于在代码中捕捉这些假设。说到这里&#xff0c;大家应该能知道assert()函数是干嘛用的了吧&#xff1f;好…

android 同根动画_android 动画系列 (1) - tween 动画(view动画)

这是我这个系列的目录&#xff0c;有兴趣的可以看下&#xff1a; android 动画系列 - 目录tween 动画早些时候我们也叫补间动画(我也不知道为啥),现在也有叫 view 动画的。tween动画是2.X 时代的产物&#xff0c;因为效果不理想&#xff0c;4.X 时代推出了动画的升级版 属性动画…