粗心的Mock模拟测试是有害的 - Philippe Bourgau

栏目: Ruby · 发布时间: 5年前

内容简介:在2010年至2014年期间,我正在开展一个名为我一直在练习它达到了我无法忍受的程度。所有这些问题都指向了模拟,因此我尝试将它们从测试文件中删除。以下是我最终用来删除它们的技术:

在2010年至2014年期间,我正在开展一个名为 http://mes-courses.fr 的辅助项目。这实际上类似“家庭购物”。我希望人们能够在5分钟内通过使用更好的在线百货界面购物。我使用的是Ruby,我刚读过 以测试为导向的面向对象的成长软件 。我对Mock模拟感到有点太兴奋了,而且用得太多了。

我一直在练习 测试驱动开发 超过5年,我期待使用像 Ruby 这样的语言取得很好的成绩。几个月之后,我可以感觉到事情并没有那么好。测试初始化​​代码越来越长,因为它包含了大量的模拟设置。这使得测试更复杂,更不易读。这也使它们变得不可靠,我的所有单元测试都无法测试出系统异常情况。我越来越习惯于经常进行端到端的测试,我也失去了很多时间来维护模拟设置代码与真实类一致。Mocks还欺骗了我在代码和测试文件之间保持1对1映射的不良做法。当将代码从一个文件移动到另一个文件时,这又增加了我的维护负担。

它达到了我无法忍受的程度。所有这些问题都指向了模拟,因此我尝试将它们从测试文件中删除。以下是我最终用来删除它们的技术:

最终的结果超出了我的希望,因为我的问题几乎神奇地消失了。代码变得更简单,我对单元测试变得更加自信,并且更容易维护。作为一个例子,这里是轨道控制器测试文件的差异的摘录,该文件经历了这种模拟饮食。

其他人已经谈到了模拟的危险:

不可变的值对象如何对抗模拟

过度使用模拟会使测试非常痛苦。如果我们长时间坚持痛苦的模拟,我们最终会放弃单位测试。最终,系统将降级为遗留系统。

不可变的值对象:

  • 创建后无法改变状态
  • 仅依赖于其他不可变值对象
  • 不要以任何方式改变整个系统的状态
  • 不要做副作用,例如输入和输出

Eric Evans在 Domain-Driven Design Blue Book中 推广了这个名字。不可变值对象在函数式语言中已经存在了数十年。我们说这些对象是不可变的(它们不能改变)和纯粹的(它们不能做副作用)。以下是值对象Value Objects的两个有趣属性:

  • 您可以多次调用方法,而不会有任何改变系统的风险
  • 每次在同一个对象上调用相同的方法时,总会得到相同的结果

这些本身在测试时已经很方便了。

让我们反过来看看副作用如何导致嘲弄。每个测试都从设置运行测试的状态开始。副作用使这变得复杂,因为许多对象需要协作来设置此状态。当这变得太痛苦时,人们开始用Mock模拟(模拟相当于一种黑客技术),这反过来使测试更加脆弱:

  • 我们没有测试“真实”的情况
  • 我们需要将此设置与实际代码保持一致

错综复杂的状态初始化鼓励人们使用模拟。

隔离系统的各个部分

不幸的是,这不是故事的全部!可变状态也诱使我们使用模拟。一旦您的测试处理可变状态,就有可能在“真实”系统中更改此状态,这意味着一些错误可能会“逃避”单元测试并出现在端到端测试或生产中,那是使用Mock模拟的地方!为了在快速反馈循环中检测到这个错误,我们可能会添加更大范围的测试并使用模拟来加速它们......

可变状态和副作用使单元测试效果降低。

但是,不可变值对象帮助我们避免模拟的另一个原因。由于前两个原因我们会尝试越来越多地使用它们,我们需要调整我们的编程风格。随着我们将在不可变值对象中推送越来越多的代码,“命令式”部分将缩小。这种“命令性”部分是副作用发生的地方。这是模拟IO有意义的部分。总而言之,我们使用的不可变值对象越多,IO就越孤立,我们需要的模拟越少。

Javascript专家Eric Elliot也在 这里 写了关于不变性和模拟的文章。

Fizz Buzz示例

举个简单的例子,我将介绍经典的 Fizz Buzz 。我已经使用和不使用不可变值对象来实现和测试它。请记住,这是一个玩具示例,问题很明显且很容易修复。我试图在小范围内突出大型程序的复杂性所隐藏的相同问题。

让我们从典型的FizzBu​​zz实现开始。

1.upto(100) <b>do</b> |i|
  <b>if</b> (i%3 == 0 and i%5 == 0)
    STDOUT.puts(<font>"FizzBuzz\n"</font><font>)
  elsif (i%3 == 0)
    STDOUT.puts(</font><font>"Fizz\n"</font><font>)
  elsif (i%5 == 0)
    STDOUT.puts(</font><font>"Buzz\n"</font><font>)
  <b>else</b>
    STDOUT.puts(</font><font>"#{i}\n"</font><font>)
  end
end
</font>

假设您需要在代码周围添加一些测试。最简单的方法是模拟STDOUT:

require 'rspec'

def fizzBuzz(max, out)
  1.upto(max) <b>do</b> |i|
    <b>if</b> (i%3 == 0 and i%5 == 0)
      out.puts(<font>"FizzBuzz\n"</font><font>)
    elsif (i%3 == 0)
      out.puts(</font><font>"Fizz\n"</font><font>)
    elsif (i%5 == 0)
      out.puts(</font><font>"Buzz\n"</font><font>)
    <b>else</b>
      out.puts(</font><font>"#{i}\n"</font><font>)
    end
  end
end

# main
fizzBuzz(100,STDOUT)

describe 'Mockist Fizz Buzz' <b>do</b>

  it 'should print numbers, fizz and buzz' <b>do</b>
    out = <b>double</b>(</font><font>"out"</font><font>)
    expect(out).to receive(:puts).with(</font><font>"1\n"</font><font>).ordered
    expect(out).to receive(:puts).with(</font><font>"2\n"</font><font>).ordered
    expect(out).to receive(:puts).with(</font><font>"Fizz\n"</font><font>).ordered
    expect(out).to receive(:puts).with(</font><font>"4\n"</font><font>).ordered
    expect(out).to receive(:puts).with(</font><font>"Buzz\n"</font><font>).ordered
    expect(out).to receive(:puts).with(</font><font>"Fizz\n"</font><font>).ordered
    expect(out).to receive(:puts).with(</font><font>"7\n"</font><font>).ordered
    expect(out).to receive(:puts).with(</font><font>"8\n"</font><font>).ordered
    expect(out).to receive(:puts).with(</font><font>"Fizz\n"</font><font>).ordered
    expect(out).to receive(:puts).with(</font><font>"Buzz\n"</font><font>).ordered
    expect(out).to receive(:puts).with(</font><font>"11\n"</font><font>).ordered
    expect(out).to receive(:puts).with(</font><font>"Fizz\n"</font><font>).ordered
    expect(out).to receive(:puts).with(</font><font>"13\n"</font><font>).ordered
    expect(out).to receive(:puts).with(</font><font>"14\n"</font><font>).ordered
    expect(out).to receive(:puts).with(</font><font>"FizzBuzz\n"</font><font>).ordered

    fizzBuzz(15, out)
  end
end
</font>

不幸的是,这段代码存在一些问题:

  • 使用嵌套逻辑和复杂的模拟设置,代码和测试都不是非常易读
  • 他们似乎也违反了单一责任原则
  • 这取决于可变输出。在一个更大的程序中,有些东西可能会搞乱这个输出流。这会破坏FizzBu​​zz。

现在让我们尝试使用尽可能多的不可变值对象,看看模拟会发生什么。

require 'rspec'

# We extracted a function to <b>do</b> the fizz buzz on a single number
def fizzBuzzN(i)
  <b>if</b> (i%3 == 0 and i%5 == 0)
    <font>"FizzBuzz"</font><font>
  elsif (i%3 == 0)
    </font><font>"Fizz"</font><font>
  elsif (i%5 == 0)
    </font><font>"Buzz"</font><font>
  <b>else</b>
    i.to_s
  end
end

# We replaced the many calls to STDOUT.puts by building a single 
# large (and immutable) string
def fizzBuzz(max)
  ((1..max).map {|i| fizzBuzzN(i)}).join(</font><font>"\n"</font><font>)
end

# main, with a single call to STDOUT.puts
STDOUT.puts fizzBuzz(100)

describe 'Statist Fizz Buzz' <b>do</b>

  it 'should print numbers not multiples of 3 or 5' <b>do</b>
    expect(fizzBuzzN(1)).to eq(</font><font>"1"</font><font>)
    expect(fizzBuzzN(2)).to eq(</font><font>"2"</font><font>)
    expect(fizzBuzzN(4)).to eq(</font><font>"4"</font><font>)
  end

  it 'should print Fizz <b>for</b> multiples of 3' <b>do</b>
    expect(fizzBuzzN(3)).to eq(</font><font>"Fizz"</font><font>)
    expect(fizzBuzzN(6)).to eq(</font><font>"Fizz"</font><font>)
  end

  it 'should print Buzz <b>for</b> multiples of 5' <b>do</b>
    expect(fizzBuzzN(5)).to eq(</font><font>"Buzz"</font><font>)
    expect(fizzBuzzN(10)).to eq(</font><font>"Buzz"</font><font>)
  end

  it 'should print FizzBuzz <b>for</b> multiples of 3 and 5' <b>do</b>
    expect(fizzBuzzN(15)).to eq(</font><font>"FizzBuzz"</font><font>)
    expect(fizzBuzzN(30)).to eq(</font><font>"FizzBuzz"</font><font>)
  end


  it 'should print numbers, fizz and buzz' <b>do</b>
    expect(fizzBuzz(15)).to start_with(</font><font>"1\n2\nFizz"</font><font>).and(end_with(</font><font>"14\nFizzBuzz"</font><font>))
  end
end
</font>

正如我们所看到的,使用不可变值对象让我们摆脱了模拟。显然,这个新代码不如原始版本有效,但大多数时候,这并不重要。虽然我们获得了更好的颗粒和更可读的测试作为奖励。

不可变值对象具有与测试相关的其他优点。

  • 我们可以直接断言他们的等同,而不必深入了解他们的内部结构
  • 我们可以根据需要多次调用方法,而不会有改变任何东西和破坏测试的风险
  • 不可变值对象不太可能包含无效状态。这消除了对一系列有效性测试的需要。

为什么说服其他开发人员使用不可变数据结构如此困难?

到目前为止,遇到共享可变状态的错误时,我获得了最大的成功。当这种情况发生时,不变设计的长期利益和安全性赢得了人们的青睐。好消息是,当你说服团队中的更多人时,不变性会像病毒一样传播!

在这种情况之外,您可以尝试以下一些参数来说服其他人员:

  • 不可变值可防止由系统的不同部分引起的错误改变相同的可变状态
  • 它们使得在较小的部分中处理程序变得更容易并且一般地推理系统
  • 不可变值不需要任何同步,使多线程编程更容易
  • 当试图添加一个简单的setter而不是保持一个类不可变时,突出显示压力很大的调试时间
  • 如果您正在处理设计合同熟练,请解释 内置的不变性
  • 承认主流语言对不可变值对象的支持不足。指向可以解决这些限制的 数据构建器 等模式

以上所述就是小编给大家介绍的《粗心的Mock模拟测试是有害的 - Philippe Bourgau》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Heuristic Search

Heuristic Search

Stefan Edelkamp、Stefan Schrodl / Morgan Kaufmann / 2011-7-15 / USD 89.95

Search has been vital to artificial intelligence from the very beginning as a core technique in problem solving. The authors present a thorough overview of heuristic search with a balance of discussio......一起来看看 《Heuristic Search》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

随机密码生成器
随机密码生成器

多种字符组合密码