仍然还有一些问题
还有一个重要的特性,我们还没有测试过。

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)相关推荐

  1. 在golang编程中总结的基础语法及常见的问题

    写下,自己在用golang开发中,用到的东西,有啥写啥. 今个就写下golang中的控制语句  if  else.for.switch.goto,这几个方面. if 判断对比 package main ...

  2. Golang字符串中常用的函数

    Golang字符串中常用的函数 说明: 字符串在我们程序开发中,使用的是非常多的,常用的函数需要同学们掌握: 下面列出20种常用的字符串函数: 1)统计字符串的长度,按字节len(str) 2)字符串 ...

  3. golang roadrunner中文文档(一)基础介绍

    2021年5月24日14:34:05 golang roadrunner中文文档(一)基础介绍 golang roadrunner中文文档(二)PHP Workers golang roadrunne ...

  4. sessionlistener方法中获取session中存储的值报空指针异常_从Golang实践中得到的教训...

    当使用复杂的分布式系统时,可能会遇到并发处理的需求.我们知道golang的协程是处理并发的利器之一,加上Golang为静态类型和编译型使得其在企业中使用越来越广泛.Mode.net公司系统每天要处理实 ...

  5. 在Golang开发中使用Redis

    周五上班的主要任务是在公司老平台上用redis处理一个队列问题,顺便复习了一下redis操作的基础知识,回来后就想着在自己的博客demo里,用redis来优化一些使用场景,学习一下golang开发下r ...

  6. 问题 | golang编程中的坑

    文章目录 背景 坑一:遍历遇上指针 例子1: 例子2: 为什么? 解决方案 坑二:切片和闭包 例子 为什么 解决方案 坑三:切片的append 例子 为什么 解决方案 坑四:time包自定义格式的坑 ...

  7. golang工作中常用的一些库

    1.json解析 非常好用的json解析工具库 github.com/tidwall/gjson 高性能json库,替代encoding/json https://github.com/json-it ...

  8. 解决 golang json 中 invalid character ‘\r‘ in string literal 报错

    type Demo struct {Content string `json:"content"` }func main() {var demo Demostr := " ...

  9. golang: 密码中允许出现数字、大写字母、小写字母、特殊字符,但至少包含其中2种且长度在8-16之间(四种符号任意满足三种即可)

    要求: 密码中允许出现数字.大写字母.小写字母.特殊字符(.@$!%*#_~?&^),但至少包含其中2种且长度在8-16之间(四种符号任意满足三种即可) package mainimport ...

  10. golang 生态中不错的组件

    觉得不错的Golang优秀组件.算是个人笔记吧,只有介绍,没有使用说明. web 框架 Go的框架有很多很多,但至今还没有一款能和Spring媲美的神级框架出现.所以大神都是自己直接写,不用框架.这里 ...

最新文章

  1. 金山词霸2012不能在PDF中取词 解决办法
  2. AD16画线时如何切换90°、45°、任意角度画线模式
  3. darknet编译报错 error: ‘__fatBinC_Wrapper_t’ does not name a type
  4. 用命令行批处理bat,设置代理服务器、DNS、网关、WINS等
  5. 计算机一级考试自测题,计算机一级B考试自测题
  6. 实验七matlab数值计算,数学应用软件实验报告---MATLAB的数值计算
  7. 关于机器人方面的sci论文_科学网-2014年SCI收录机器人期刊22种目录-万跃华的博文...
  8. java毕业生设计学生课堂互动教学系统计算机源码+系统+mysql+调试部署+lw
  9. html中如何设置动画效果,css3如何设置动画?
  10. 1. Emacs使用本地elpa镜像
  11. #includecstring
  12. csp-s模拟测试49(9.22)养花(分块/主席树)·折射(神仙DP)·画作
  13. 读Applying Deep Learning To Airbnb Search有感
  14. 一图读懂 Unix 时间日期例程相互关系
  15. @NotNull、@NotEmpty和@NotBlank的区别
  16. UG许可资源优化解决方案-许可不够用,解决UG盗版,UG许可监控,UG律师函
  17. Android.应用软件.常用程序下载地址_20190913
  18. windows不安装wifi共享软件实现wifi共享
  19. 【技巧】Microsoft Edge 调节视频播放速度的方法
  20. 六一送好书|Cocos小粉丝回馈季来啦!

热门文章

  1. 【JVM】Java IDEA 配置项目的JVM运行内存大小
  2. 外包被裁能要n+1吗?签约软通动力,在滴滴工作,滴滴裁员,我要n+1,软通不认!...
  3. 文本过滤器Filters
  4. Mac新手需要知道的显示桌面的快捷方式
  5. netty使用中的LEAK: ByteBuf.release() was not called before it‘s garbage-collected
  6. 2020年9月份英语六级翻译-西游记
  7. 供应链金融融资的业务模式
  8. shell脚本实现从master节点批量配置salve节点(主机名有瑕疵,IP映射,ssh服务)
  9. PCB六层板如何分层最好?
  10. 施密特正交化过程编程c语言,利用C程序编写格拉姆-施密特正交化的过程.docx