GoLang - Go中Mocking(3)
仍然还有一些问题
还有一个重要的特性,我们还没有测试过。
Countdown 应该在第一个打印之前 sleep,然后是直到最后一个前的每一个,例如:
Sleep
Print N
Sleep
Print N-1
Sleep
etc
我们最新的修改只断言它已经 sleep 了 4 次,但是那些 sleeps 可能没按顺序发生。
当你在写测试的时候,如果你没有信心,你的测试将给你足够的信心,尽管推翻它!(不过首先要确定你已经将你的更改提交给了源代码控制)。将代码更改为以下内容。
func Countdown(out io.Writer, sleeper Sleeper) {
for i := countdownStart; i > 0; i-- {
sleeper.Sleep()
}
for i := countdownStart; i > 0; i-- {fmt.Fprintln(out, i)
}sleeper.Sleep()
fmt.Fprint(out, finalWord)
}
如果你运行测试,它们仍然应该通过,即使实现是错误的。
让我们再用一种新的测试来检查操作的顺序是否正确。
我们有两个不同的依赖项,我们希望将它们的所有操作记录到一个列表中。所以我们会为它们俩创建 同一个监视器。
type CountdownOperationsSpy struct {
Calls []string
}
func (s *CountdownOperationsSpy) Sleep() {
s.Calls = append(s.Calls, sleep)
}
func (s *CountdownOperationsSpy) Write(p []byte) (n int, err error) {
s.Calls = append(s.Calls, write)
return
}
const write = “write”
const sleep = “sleep”
我们的 CountdownOperationsSpy 同时实现了 io.writer 和 Sleeper,把每一次调用记录到 slice。在这个测试中,我们只关心操作的顺序,所以只需要记录操作的代名词组成的列表就足够了。
现在我们可以在测试套件中添加一个子测试。
t.Run(“sleep before every print”, func(t *testing.T) {
spySleepPrinter := &CountdownOperationsSpy{}
Countdown(spySleepPrinter, spySleepPrinter)
want := []string{sleep,write,sleep,write,sleep,write,sleep,write,
}if !reflect.DeepEqual(want, spySleepPrinter.Calls) {t.Errorf("wanted calls %v got %v", want, spySleepPrinter.Calls)
}
})
现在这个测试应该会失败。恢复原状新测试应该又可以通过。
我们现在在 Sleeper 上有两个测试监视器,所以我们现在可以重构我们的测试,一个测试被打印的内容,另一个是确保我们在打印时间 sleep。最后我们可以删除第一个监视器,因为它已经不需要了。
func TestCountdown(t *testing.T) {
t.Run("prints 3 to Go!", func(t *testing.T) {buffer := &bytes.Buffer{}Countdown(buffer, &CountdownOperationsSpy{})got := buffer.String()want := `3
2
1
Go!`
if got != want {t.Errorf("got %q want %q", got, want)}
})t.Run("sleep before every print", func(t *testing.T) {spySleepPrinter := &CountdownOperationsSpy{}Countdown(spySleepPrinter, spySleepPrinter)want := []string{sleep,write,sleep,write,sleep,write,sleep,write,}if !reflect.DeepEqual(want, spySleepPrinter.Calls) {t.Errorf("wanted calls %v got %v", want, spySleepPrinter.Calls)}
})
}
我们现在有了自己的函数,并且它的两个重要的属性已经通过合理的测试。
通过配置扩展 Sleeper
一个不错的特性是 Sleeper 是可配置的。这意味着我们可以在主程序中调整睡眠时间。
首先编写测试
让我们首先为 ConfigurableSleeper 创建一个新类型,它接受我们需要的配置和测试。
type ConfigurableSleeper struct {
duration time.Duration
sleep func(time.Duration)
}
我们使用 duration 来配置睡眠时间和 sleep 作为传递 sleep 函数的一种方式。sleep 的签名与 time.Sleep 允许我们在实际实现中使用 time.Sleep 以及在我们的测试中使用下面的 spy 相同:
type SpyTime struct {
durationSlept time.Duration
}
func (s *SpyTime) Sleep(duration time.Duration) {
s.durationSlept = duration
}
有了我们的 spy,我们可以为可配置的睡眠者创建一个新的测试。
func TestConfigurableSleeper(t *testing.T) {
sleepTime := 5 * time.Second
spyTime := &SpyTime{}
sleeper := ConfigurableSleeper{sleepTime, spyTime.Sleep}
sleeper.Sleep()if spyTime.durationSlept != sleepTime {t.Errorf("should have slept for %v but slept for %v", sleepTime, spyTime.durationSlept)
}
}
在这个测试中应该没有什么新东西,它的设置与前面的模拟测试非常相似。
尝试运行我们的测试
sleeper.Sleep undefined (type ConfigurableSleeper has no field or method Sleep, but does have sleep)
你应该会看到一个非常清楚的错误消息,表明我们没有在 ConfigurableSleeper 上创建 Sleep 方法。
为运行测试编写最少的代码,并检查失败的测试输出
func (c *ConfigurableSleeper) Sleep() {
}
实现了新的 Sleep 功能后,我们的测试失败了。
countdown_test.go:56: should have slept for 5s but slept for 0s
编写足够的代码使其通过
我们现在需要做的就是为实现 Sleep 函数的 ConfigurableSleeper。
func (c *ConfigurableSleeper) Sleep() {
c.sleep(c.duration)
}
有了这个改变,所有的测试都应该再次通过,你可能会奇怪为什么主程序的所有麻烦都没有改变。希望在下一节之后你会变得清楚.
清理和重构
我们需要做的最后一件事是在主函数中实际使用我们的 ConfigurableSleeper。
func main() {
sleeper := &ConfigurableSleeper{1 * time.Second, time.Sleep}
Countdown(os.Stdout, sleeper)
}
如果我们手动运行测试和程序,我们可以看到所有的行为都保持不变。
因为我们使用的是 ConfigurableSleeper,所以现在删除 DefaultSleeper 实现是安全的。结束我们的程序,并有一个更通用睡眠与任意时长的倒计时。
难道 mocking 不是在作恶(evil)吗?
你可能听过 mocking 是在作恶。就像软件开发中的任何东西一样,它可以被用来作恶,就像 DRY (Don’t repeat yourself) 一样。
当人们 不听从他们的测试 并且 不尊重重构阶段时,他们通常会陷入糟糕的境地。
如果你的模拟代码变得很复杂,或者你需要模拟很多东西来测试一些东西,那么你应该 倾听 那种糟糕的感觉,并考虑你的代码。通常这是一个征兆:
你正在进行的测试需要做太多的事情
把模块分开就会减少测试内容
它的依赖关系太细致
考虑如何将这些依赖项合并到一个有意义的模块中
你的测试过于关注实现细节
最好测试预期的行为,而不是功能的实现
通常,在你的代码中有大量的 mocking 指向 错误的抽象。
人们在这里看到的是测试驱动开发的弱点,但它实际上是一种力量,通常情况下,糟糕的测试代码是糟糕设计的结果,而设计良好的代码很容易测试。
但是模拟和测试仍然让我举步维艰!
曾经遇到过这种情况吗?
你想做一些重构
为了做到这一点,你最终会改变很多测试
你对测试驱动开发提出质疑,并在媒体上发表一篇文章,标题为「Mocking 是有害的」
这通常是您测试太多 实现细节 的标志。尽力克服这个问题,所以你的测试将测试 有用的行为,除非这个实现对于系统运行非常重要。
有时候很难知道到底要测试到 什么级别,但是这里有一些我试图遵循的思维过程和规则。
重构的定义是代码更改,但行为保持不变。 如果您已经决定在理论上进行一些重构,那么你应该能够在没有任何测试更改的情况下进行提交。所以,在写测试的时候问问自己。
我是在测试我想要的行为还是实现细节?
如果我要重构这段代码,我需要对测试做很多修改吗?
虽然 Go 允许你测试私有函数,但我将避免它作为私有函数与实现有关。
我觉得如果一个测试 超过 3 个模拟,那么它就是警告 —— 是时候重新考虑设计。
小心使用监视器。监视器让你看到你正在编写的算法的内部细节,这是非常有用的,但是这意味着你的测试代码和实现之间的耦合更紧密。如果你要监视这些细节,请确保你真的在乎这些细节。
和往常一样,软件开发中的规则并不是真正的规则,也有例外。Uncle Bob 的文章 「When to mock」 有一些很好的指南。
总结
更多关于测试驱动开发的方法
当面对不太简单的例子,把问题分解成「简单的模块」。试着让你的工作软件尽快得到测试的支持,以避免掉进兔子洞(rabbit holes,意指未知的领域)和采取「最终测试(Big bang)」的方法。
一旦你有一些正在工作的软件,小步迭代 应该是很容易的,直到你实现你所需要的软件。
Mocking
没有对代码中重要的区域进行 mock 将会导致难以测试。在我们的例子中,我们不能测试我们的代码在每个打印之间暂停,但是还有无数其他的例子。调用一个 可能 失败的服务?想要在一个特定的状态测试您的系统?在不使用 mocking 的情况下测试这些场景是非常困难的。
如果没有 mock,你可能需要设置数据库和其他第三方的东西来测试简单的业务规则。你可能会进行缓慢的测试,从而导致 缓慢的反馈循环。
当不得不启用一个数据库或者 webservice 去测试某个功能时,由于这种服务的不可靠性,你将会得到的是一个 脆弱的测试。
GoLang - Go中Mocking(3)相关推荐
- 在golang编程中总结的基础语法及常见的问题
写下,自己在用golang开发中,用到的东西,有啥写啥. 今个就写下golang中的控制语句 if else.for.switch.goto,这几个方面. if 判断对比 package main ...
- Golang字符串中常用的函数
Golang字符串中常用的函数 说明: 字符串在我们程序开发中,使用的是非常多的,常用的函数需要同学们掌握: 下面列出20种常用的字符串函数: 1)统计字符串的长度,按字节len(str) 2)字符串 ...
- golang roadrunner中文文档(一)基础介绍
2021年5月24日14:34:05 golang roadrunner中文文档(一)基础介绍 golang roadrunner中文文档(二)PHP Workers golang roadrunne ...
- sessionlistener方法中获取session中存储的值报空指针异常_从Golang实践中得到的教训...
当使用复杂的分布式系统时,可能会遇到并发处理的需求.我们知道golang的协程是处理并发的利器之一,加上Golang为静态类型和编译型使得其在企业中使用越来越广泛.Mode.net公司系统每天要处理实 ...
- 在Golang开发中使用Redis
周五上班的主要任务是在公司老平台上用redis处理一个队列问题,顺便复习了一下redis操作的基础知识,回来后就想着在自己的博客demo里,用redis来优化一些使用场景,学习一下golang开发下r ...
- 问题 | golang编程中的坑
文章目录 背景 坑一:遍历遇上指针 例子1: 例子2: 为什么? 解决方案 坑二:切片和闭包 例子 为什么 解决方案 坑三:切片的append 例子 为什么 解决方案 坑四:time包自定义格式的坑 ...
- golang工作中常用的一些库
1.json解析 非常好用的json解析工具库 github.com/tidwall/gjson 高性能json库,替代encoding/json https://github.com/json-it ...
- 解决 golang json 中 invalid character ‘\r‘ in string literal 报错
type Demo struct {Content string `json:"content"` }func main() {var demo Demostr := " ...
- golang: 密码中允许出现数字、大写字母、小写字母、特殊字符,但至少包含其中2种且长度在8-16之间(四种符号任意满足三种即可)
要求: 密码中允许出现数字.大写字母.小写字母.特殊字符(.@$!%*#_~?&^),但至少包含其中2种且长度在8-16之间(四种符号任意满足三种即可) package mainimport ...
- golang 生态中不错的组件
觉得不错的Golang优秀组件.算是个人笔记吧,只有介绍,没有使用说明. web 框架 Go的框架有很多很多,但至今还没有一款能和Spring媲美的神级框架出现.所以大神都是自己直接写,不用框架.这里 ...
最新文章
- 金山词霸2012不能在PDF中取词 解决办法
- AD16画线时如何切换90°、45°、任意角度画线模式
- darknet编译报错 error: ‘__fatBinC_Wrapper_t’ does not name a type
- 用命令行批处理bat,设置代理服务器、DNS、网关、WINS等
- 计算机一级考试自测题,计算机一级B考试自测题
- 实验七matlab数值计算,数学应用软件实验报告---MATLAB的数值计算
- 关于机器人方面的sci论文_科学网-2014年SCI收录机器人期刊22种目录-万跃华的博文...
- java毕业生设计学生课堂互动教学系统计算机源码+系统+mysql+调试部署+lw
- html中如何设置动画效果,css3如何设置动画?
- 1. Emacs使用本地elpa镜像
- #includecstring
- csp-s模拟测试49(9.22)养花(分块/主席树)·折射(神仙DP)·画作
- 读Applying Deep Learning To Airbnb Search有感
- 一图读懂 Unix 时间日期例程相互关系
- @NotNull、@NotEmpty和@NotBlank的区别
- UG许可资源优化解决方案-许可不够用,解决UG盗版,UG许可监控,UG律师函
- Android.应用软件.常用程序下载地址_20190913
- windows不安装wifi共享软件实现wifi共享
- 【技巧】Microsoft Edge 调节视频播放速度的方法
- 六一送好书|Cocos小粉丝回馈季来啦!
热门文章
- 【JVM】Java IDEA 配置项目的JVM运行内存大小
- 外包被裁能要n+1吗?签约软通动力,在滴滴工作,滴滴裁员,我要n+1,软通不认!...
- 文本过滤器Filters
- Mac新手需要知道的显示桌面的快捷方式
- netty使用中的LEAK: ByteBuf.release() was not called before it‘s garbage-collected
- 2020年9月份英语六级翻译-西游记
- 供应链金融融资的业务模式
- shell脚本实现从master节点批量配置salve节点(主机名有瑕疵,IP映射,ssh服务)
- PCB六层板如何分层最好?
- 施密特正交化过程编程c语言,利用C程序编写格拉姆-施密特正交化的过程.docx