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()
}

扫描二维码关注公众号,回复: 11422929 查看本文章
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 去测试某个功能时,由于这种服务的不可靠性,你将会得到的是一个 脆弱的测试。

猜你喜欢

转载自blog.csdn.net/xiabiao1974/article/details/107525909