AntiPatterns基础:Rails测试

本文是对在Rails中测试AntiPatterns的高级知识的介绍。 如果您不是测试驱动开发的新手,并且想学习一些非常有价值的最佳实践,那么这篇文章正是为您写的。

主题

  • 神秘客人
  • 模糊测试
  • 测试缓慢
  • 治具
  • 脆性测试
  • 数据属性

describe Mission do

  let(:james_bond)   { build_stubbed(:agent, name: 'James Bond', number: '007') }
  let(:mission)      { build_stubbed(:mission, title: 'Moonraker') }

...

end

RSpec中的let helper方法非常常用于创建在多个测试之间可用的实例变量。 作为TDD练习的热心学生,您可能已经写下了相当一部分,但是遵循这种练习很容易导致很多神秘来宾出现(请参阅下文),这绝对不是使我们的派对崩溃的必要条件!

let这种特殊副作用因可能导致整个测试套件中的测试维护量增加和可读性差而赢得了声誉。 let听起来确实诱人,因为它懒洋洋地评估和艾滋病秉承干燥,所有的通常零缺陷理念。 因此,不定期使用似乎太好了。 它的近亲subject也应避免的大部分时间。

当您开始嵌套这些东西时,情况会变得更糟。 let语句遍布嵌套的describe块一直是人们的最爱。 我认为快速地将其称为吊死自己的秘诀是不公平的。 通常,更有限的范围更容易理解和遵循。

我们不想用半全局的let夹具来建造一所纸牌屋,这些夹具会模糊理解并增加破坏相关测试的机会。 通过这种方法,编写质量代码的几率对我们不利。 通过普通的旧Ruby方法甚至需要的话,类也可以更容易地提取公共对象设置。

这种let生物成为广泛共享的装置,通常需要首先对其进行解密,然后才能确切了解该对象在测试中所从事的业务。 同样,来回了解它们的确切构成以及通过关联具有的关系也可能会很耗时。

测试设置中这些细节的清晰性通常可以帮助其他开发人员充分了解测试套件的每个特定部分所需要的一切-不要忘了未来的自己! 在这个世界中,您不必重新访问特定的测试,甚至不必重构测试套件的某些部分,这可能没什么大不了的,但是现在这只是梦pipe以求的事情!

对于每个测试,我们希望有尽可能少的协作者和尽可能少的数据。 let我们在这方面也对您不利。 这些让灯具可以聚集很多属性和方法,这也会使它们变得太大。

如果您开始走这条路,那么您通常会遇到很多胖的对象,这些对象试图同时使很多测试快乐。 当然,您可以为这些let东西制造很多变体,但是我认为这与它们的整体想法有点无关。 为什么不更进一步,避免让步,并依靠没有RSpec DSL魔术的Ruby?

我更愿意为每个测试提供重复的设置代码,而不是在测试套件中过于干燥,模糊或含糊。 我总是会提高可读性。 测试方法应该清楚其涉及的原因和结果-使用可能在远离测试练习的地方定义的对象协作者并不是您的最大利益。 如果您需要提取内容,请使用封装该知识的表达方法。

这些几乎总是安全的选择。 这样,您还可以提供每次测试实际需要的设置,而不会因为需要的数据而导致测试缓慢。 良好的旧变量,方法和类通常是您提供快速,稳定且易于阅读的测试所需要的。

神秘客人

神秘来宾确实是RSpec DSL拼图。 有一段时间,通过RSpec的DSL定义的各种对象let并不难,以保持在检查,但很快,当测试套件的增长,你邀请了很多神秘嘉宾到您的规格。 这给您未来的自我和其他不必要的情境难题提供了解决方案。

结果将是晦涩的测试,需要您进入完整的福尔摩斯模式。 我想这听起来比以前有趣得多。 最重要的是,这浪费了每个人的时间。

神秘客人提出两个有问题的问题:

  • 这个物体从哪里来?
  • 它到底由什么组成?
describe Mission do

  let(:agent_01)   { build_stubbed(:agent, name: 'James Bond', number: '007') }
  let(:agent_02)   { build_stubbed(:agent, name: 'Moneypenny', number: '243') }
  let(:title)   { 'Moonraker' }
  let(:mission) { build_stubbed(:mission, title: title) }
  mission.agents << agent_01 << agent_02

  ...

  ...

  ...

  #lots of other tests

  describe '#top_agent' do
    it 'returns highest ranking agent associated to a mission' do

      expect(mission.top_agent).to eq('James Bond')
    end
  end
end

#top_agent此describe块缺乏清晰度和上下文。 涉及什么代理人,我们在这里谈论什么任务? 这迫使开发人员去寻找在测试中突然弹出的对象。

神秘客人的经典例子。 当我们在相关测试和这些对象的起源之间有很多代码时,您就会增加混淆测试中发生的事情的机会。

解决方案非常简单:您需要全新的“夹具”,并使用所需的确切数据来构建对象的本地版本,并且不止于此! Factory Girl是处理此问题的不错选择。

可以将这种方法视为更冗长的方法,有时您可能会复制内容(将内容提取到方法中通常是个好主意),但是它更具表现力,并且在提供上下文的同时保持测试的重点。

describe Mission do

  #...

  #...

  #...

  #lots of other tests

  describe '#top_agent' do
    it 'returns a list of all agents associated to a mission' do
      agent_01 = build_stubbed(:agent, name: 'James Bond', number '007')
      agent_02 = build_stubbed(:agent, name: 'Moneypenny', number '243')
      mission  = build_stubbed(:mission, title: 'Moonraker')
      mission.agents << agent_01 << agent_02

      expect(mission.top_agent).to eq('James Bond')
    end
  end
end

上面的示例在实际测试用例中构建了测试所需的所有对象,并提供了所需的所有上下文。 开发人员可以专注于特定的测试用例,而无需“下载”另一个(可能是完全不相关的)测试用例来处理当前的情况。 不再晦涩!

是的,您是对的,这种方法意味着我们没有达到可能的最低重复级别,但是在这些情况下的清晰度对于测试套件的质量以及项目的健壮性而言更为重要。 在这方面,您可以有效地将更改应用于测试的速度也很重要。

测试的另一个重要方面是您的测试套件不仅可以用作文档,而且绝对可以! 零重复并不是对规范应用程序文档的目标。 尽管如此,控制不必要的重复仍然是一个重要的目标-平衡在这里至关重要!

模糊测试

下面是另一个尝试在测试中本地设置您需要的所有内容的示例,但由于没有告诉我们完整的故事而失败了。

...

  context "agent status" do
    it "returns the status of the mission’s agent" do
      double_o_seven = build_stubbed(:agent)
      mission = build_stubbed(:mission, agent: double_o_seven)

      expect(mission.agent_status).to eq(double_o_seven.status)
    end
  end

我们正在创建一个通用代理。 我们怎么知道它是007? 我们也正在测试代理的状态,但也找不到它-无论是在安装过程中还是在我们的expect语句的验证阶段均未明确发现。 double_o_seven.status与任务状态之间的关系可能会令人迷惑,因为它确实double_o_seven.status 。 我们可以做得更好:

...

  context "agent status" do
    it "returns the status of the mission’s agent" do
      double_o_seven = build_stubbed(:agent, name: 'James Bond', status: 'Missing in action'))
      mission = build_stubbed(:mission, agent: double_o_seven)

      expect(mission.agent_status).to eq('James Bond: Missing in action')
    end
  end

再说一遍,这里我们需要讲一个故事。 我们需要的所有数据就在我们眼前。

测试缓慢

因此,您已经开始涉足测试驱动开发,并且已经开始欣赏它提供的功能。 荣誉,太好了! 我敢肯定,这样做的决定和实现这一目标的学习曲线都不是小菜一碟。 但是,在第一步之后通常会发生的情况是,您努力地进行全面的测试覆盖,并且当您的规格速度开始使您烦恼时,您开始意识到有些不对劲。

为什么您认为自己在做所有正确的事情,但测试套件却变得越来越慢? 写作测试会受到惩罚吗? 缓慢的测试糟透了-大时间! 他们有两个问题。 最重要的问题是,从长期来看,缓慢的测试会导致跳过测试。 一旦您的测试套件需要永久完成,您将更愿意对自己进行思考:“拧紧,稍后再运行! 我要做的事情比等待这些事情完成要好。” 而且您绝对正确,您还有更好的事情要做。

事实是,慢速测试更可能在代码质量的折衷方面受到欢迎,而不是一开始就显而易见。 缓慢的测试也会激起人们反对TDD的争论-我认为这是不公平的。 我什至不想知道非技术产品经理要说些什么,如果您必须定期出门休息很长时间,只是为了运行测试套件,然后再继续工作。

我们不要走那条路! 当您只需要一点时间进行测试并因此获得超快速的反馈周期来开发新功能的每一步时,实践TDD就会变得更具吸引力,而论据也就更少了。 在此过程中,通过一点点的工作和照顾,我们可以非常有效地避免进行慢速测试。

缓慢的测试也是进入“区域”的杀手。 如果您在流程中经常遇到这种麻烦,则必须等待缓慢的测试才能从昂贵的往返行程中返回,从而使整体工作质量下降。 您希望获得尽可能多的“区域内时间”-难以忍受的缓慢测试是主要的流量杀手。

在这种情况下,另一个值得一提的问题是,这可能会导致测试覆盖您的代码,但是由于您不会花时间来完成整个套件的使用,也不会在事后编写测试,因此您的应用程序设计不会由测试驱动。 如果您没有参加“测试驱动的炒作”培训,这可能不会给您带来太大的麻烦,但是对于TDD人士来说,这一方面至关重要,不应忽略。

最重要的是,测试越快,您就越愿意执行它们-这是设计应用程序以及尽早且经常发现错误的最佳方法。

我们怎样做才能加快测试速度? 这里有两个重要的速度:

  • 测试可以真正执行套件的速度
  • 从测试套件获得反馈以设计应用程序的速度

尽量避免写入数据库

这并不意味着您应该避免所有费用。 通常,您无需编写用于测试数据库的测试,并且可以节省大量运行测试所需的时间。 仅使用new实例化对象通常足以进行测试设置。 伪造未直接测试的对象是另一个可行的选择。

创建测试倍增是一种使测试更快,同时又使设置所需的协作对象保持高度集中和轻巧的好方法。 Factory Girl还为您提供了各种选项,可以巧妙地“创建”测试数据。 但是有时没有办法保存到数据库中(这比您预期的要少得多),这正是您应该划清界限的地方。 在任何其他时间,请避免像地狱这样的环境,您的测试套件将保持快速,灵活。

在这方面,您还应尽量减少依赖关系,这意味着您需要协作以使测试通过的对象最少,同时在此过程中尽可能少地对数据库进行保存。 剔除对象(它们只是合作者,而不是直接受测试的对象)通常还使您的设置更易于消化和创建。 总体上不错的速度提升,只需很少的努力。

使用测试金字塔来构建测试

这意味着您希望在该层次结构的底部拥有大多数单元测试-所有这些单元测试都独立地专注于应用程序的非常特定的部分-并且在金字塔的顶部具有最少数量的集成测试。 集成测试模拟用户浏览系统时,同时与大量同时行使的组件进行交互。

它们很容易编写,但维护起来却不那么容易,而且速度上的损失也不值得走简单的路线。 集成测试与单元测试在高层次和吸收大量需要在测试中设置的组件方面几乎是相反的,这是它们比单元测试慢的主要原因之一。

我想这很清楚地说明了为什么它们应该处于测试金字塔的顶端,以避免明显的速度损失。 这里的另一个重要问题是,您希望这两个测试类别之间的重叠尽可能少—理想情况下,您毕竟只希望测试一次。 您不能指望有完美的分离,但是尽可能少地瞄准是一个合理且可以实现的目标。

与单元测试相反,您希望使用集成测试来测试尽可能少的细节。 内部力学应该已经被广泛的单元测试所涵盖。 而是只关注交互必须能够行使的最重要部分! 另一个主要原因是网络驱动程序需要模拟通过浏览器并与页面交互的过程。 这种方法不会伪造或伪造很少,将内容保存到数据库中并真正通过UI。

这也是可以将它们称为接受测试的原因之一,因为这些测试试图模拟真实的用户体验。 这是您要尽量少运动的另一个主要减速带。 如果您要进行大量此类测试(我估计要占测试总数的10%以上),则应放慢速度并将其减少到尽可能少的数量。

另外,请记住,有时您不需要锻炼整个应用程序-较小的,集中的视图测试通常也可以解决问题。 如果您重写几个集成测试,而这些测试仅测试了一些不需要进行完整集成检查的逻辑,那么您的速度将会更快。 但是也不要写很多。 他们提供了最低的价格。 话虽这么说,集成测试对于测试套件的质量至关重要,并且您需要找到平衡的平衡,即应用程序过于小巧和周围没有太多。

从您的应用程序获取反馈并快速测试

快速反馈和快速迭代周期是设计对象的关键。 一旦开始避免频繁运行这些测试,您将失去这一优势,这对设计对象有很大帮助。 不要等到您选择的持续集成服务开始测试整个应用程序。

那么在运行测试时我们应该牢记的魔幻数字是什么? 好吧,不同的人会为此告诉您不同的基准。 我认为停留在30秒以下是一个非常合理的数字,这使其很有可能定期进行全面测试。 如果您将该基准越来越落后,则可能需要进行一些重构。 这将是值得的,它将使您感到更加舒适,因为您可以更定期地检查。 您很有可能也会更快地前进。

您希望与测试的对话尽可能快。 通过使用也可以进行测试的编辑器来加强此反馈周期,这一点不可低估。 在编辑器和终端之间来回切换不是解决此问题的最佳解决方案。 这很快就变老了。

如果您喜欢使用Vim,您还有另一个理由要花一些时间来提高使用编辑器的效率。 有许多方便的工具可用于Vim窥视。 我记得Sublime Text还提供从编辑器内部运行测试的功能,但除此之外,您需要做一些研究才能找出您选择的编辑器在这方面的能力。 TDD发烧友会经常听到的论点是,您不想离开编辑器,因为总的来说,您会花太多时间在此上。 当您可以通过代码编辑器内部的快速快捷方式来执行此类操作时,您希望在此区域中停留更多的时间,而又不会失去思路。

要注意的另一件事是,您还希望能够切片要运行的测试。 如果您不需要运行整个文件,那么最好运行单个测试或仅关注您需要立即获得反馈的块。 拥有可帮助您运行单个测试,单个文件或仅执行上一个测试的快捷方式,可以节省大量时间,并使您始终处于专区–更不用说高度的便利性以及超级花哨的感觉了。 有时真棒的编码工具真是令人惊讶。

路上的最后一件事。 使用像Spring这样的预加载器。 当您不必为每次测试运行都加载Rails时,您将节省多少时间,您会感到惊讶。 您的应用程序将在后台运行,并且不需要一直启动。 做吧!

治具

我不确定对于Ruby / Rails领域的新手来说夹具是否仍然是一个问题。 万一没有人告诉您有关它们的信息,我将尽力让您快速掌握这些令人恐惧的事物。

ActiveRecord数据库固定装置是在您的测试套件中拥有大量“神秘访客”的绝佳示例。 在Rails和Ruby TDD的早期,YAML固定装置实际上是在应用程序中设置测试数据的标准。 他们发挥了重要作用,并帮助推动了该行业的发展。 如今,他们的信誉代表相当差。

YAML装置

Quartermaster:
  name: Q
  favorite_gadget: Broom radio
  skills: Inventing gizmos and hacking

00Agent:
  name: James Bond
  favorite_gadget: Submarine Lotus Esprit
  skills: Getting Bond Girls killed and covert infiltration

像哈希一样的结构肯定看起来很方便并且易于使用。 如果要从模型中模拟关联,甚至可以引用其他节点。 但这就是音乐停止的地方,许多人说他们的痛苦开始了。 对于涉及更多的数据集,在不影响其他测试的情况下,难以维护和更改YAML固定装置。 我的意思是,您当然可以使它们正常工作-毕竟,开发人员过去曾大量使用它们-但许多开发人员会同意,管理固定装置所要付出的代价只是有点小气。

我们绝对希望避免的一种情况是更改现有夹具的一些细节并导致大量测试失败。 如果这些失败的测试无关,那么情况就更糟了,这就是测试过于脆弱的一个很好的例子。 为了在这种情况下“保护”现有测试,这还可能导致您的夹具组超出任何合理的尺寸-此时,很可能已经不再使用夹具进行DRY了。

为避免在不可避免的变化发生时破坏测试数据,开发人员很乐意采用更新的策略,这些策略提供了更大的灵活性和动态行为。 那是Factory Girl进来并亲吻YAML日子的地方。

另一个问题是测试与.yml固定文件之间的严重依赖性。 由于固定装置是在单独的.yml文件中定义的,因此神秘来宾由于晦涩难耐,还会等着咬你,这是一个很大的痛苦。 我是否提到过将固定装置导入测试数据库而不进行任何验证,并且不遵守Active Record的生命周期? 是的,那也不是太棒了-从任何角度看都可以!

通过Factory Factory,您可以通过创建与内联测试相关的对象(并且仅包含特定案例所需的数据)来避免所有这些情况。 座右铭是,仅在工厂定义中定义最低要求,然后在逐项测试的基础上添加其余的最低要求。 (在测试中)局部覆盖工厂中定义的默认值是比让大量灯具独角兽等待在灯具文件中过时更好的方法。

这种方法也更具扩展性。 Factory Girl为您提供了很多工具来创建您需要的所有数据-可以根据您的需要进行细微的修改-而且还为您提供了大量的弹药,以便在需要时保持DRY。 我认为该库的优缺点很好地平衡了。 不处理验证也不再引起关注。 我认为对测试数据使用工厂模式是非常合理的,这是“工厂女孩”之所以受到社区欢迎的主要原因之一。

复杂性是快速增长的敌人,YAML设备几乎无法有效应对。 在某种程度上,我觉得作为灯具的let类固醇。 您不仅将它们放置在更远的地方(与所有文件存放在单独的文件中),而且还可能预加载比实际需要更多的灯具。 RIP!

脆性测试

如果规格更改导致其他测试中似乎无关的失败,则您可能正在考虑由于上述原因而变得脆弱的测试套件。 这些通常令人困惑的,来宾神秘的测试很容易导致不稳定的纸牌屋。

当测试所需的对象被定义为与实际测试场景“相距甚远”时,不难忽略这些对象与其测试之间的关系。 当代码被删除,调整或只是有问题的安装对象被意外覆盖(不知道这如何影响周围的其他测试)时,失败的测试并非罕见。 它们很容易看起来像完全不相关的故障。 我认为将此类方案包含在紧密耦合的代码类别中是公平的。

describe Mission do
  let(:agent)   { build_stubbed(:agent, name: 'James Bond', number: '007') }
  let(:title)   { 'Moonraker' }
  let(:mission) { build_stubbed(:mission, title: title) }

  #...

  #...

  #...

  #lots of other tests

  describe '#joint_operation_agent_name' do
    let(:agent) { build_stubbed(:agent, name: 'Felix Leiter', agency: 'CIA')
    mission.agents << agent

    it “returns mission’s joint operation’s agent name” do
      expect(mission.joint_operation_agent_name).to eq('Felix Leiter')
    end
  end
end

在这种情况下,我们已经在本地明确修改了在设置中定义的对象状态。 所涉agent现在是CIA agent ,并且使用不同的名称。 mission又一次无处不在。 真的很讨厌。

当其他可能依赖于不同版本agent开始崩溃时,也就不足为奇了。 让我们摆脱了let废话,打造对象,我们需要再次右键,我们对其进行测试,只与属性,我们需要测试的情况下,当然。

describe Mission do

  #...

  #...

  #...

  #lots of other tests

  describe '#joint_operation_agent_name' do
    agent   = build_stubbed(:agent, name: 'Felix Leiter', agency: 'CIA')
    mission = build_stubbed(:mission)
    mission.agents << agent

    it “returns mission’s joint operation’s agent name” do
      expect(mission.joint_operation_agent_name).to eq('Felix Leiter')
    end
  end
end

重要的是要了解对象之间的关系,最好是使用最少的设置代码。 您不希望派遣其他开发人员追赶他们,以免他们偶然发现您的代码。

如果很难快速掌握,并且昨天需要实施一项新功能,那么这些难题就不能指望得到最高优先级。 反过来,这通常意味着在不明朗的环境中开发新的东西,这是前进的脆弱基础,同时也极大地诱使开发中的错误。 在这里要汲取的教训是,在可能的地方不要覆盖任何东西。

数据属性

避免进行脆弱测试的最后一个有用技巧是在HTML标记中使用数据属性。 请帮自己一个忙,并使用它们-以后可以感谢我。 这样,您就可以将需要测试的元素与设计人员可能不接触而经常接触的样式信息分离。

如果您在测试中对诸如class='mission-wrapper'的类进行硬编码,并且聪明的设计师决定更改此不良名称,则测试会受到不必要的影响。 当然,设计师也不应责怪。 她怎么会知道这会影响您的测试套件的一部分-至少极不可能。

<div class='mission data-role='single-mission'>

  <h2><% = @mission.agent_status %></h2>

  ...

</div>
context "mission’s agent status" do
    it 'does something with a mission' do
      ...

      ...

      expect(page).to have_css '[data-role=single-mission]'
    end
  end

我们希望在页面上看到一些HTML元素,并用data-role标记它。 设计师没有理由去碰触它,并且可以保护您免受因样式方面的更改而发生的易碎测试。

这是一种非常有效和有用的策略,基本上不会给您带来任何回报。 唯一需要做的就是与设计师进行简短的交谈。 小菜一碟!

最后的想法

我们希望避免分散会阅读我们的测试的人的注意力,甚至更糟的是使他们感到困惑。 这为漏洞打开了大门,但也可能很昂贵,因为这可能会浪费宝贵的时间和脑力。 创建测试时,请尽量不要覆盖所有内容,这无助于提高清晰度。 它更有可能导致细微,耗时的错误,并且不会影响对文档进行积极记录的方面。

这造成了我们可以避免的不必要的负担。 突变测试数据比绝对必要的还多,有些偏执。 保持尽可能简单! 这确实有助于您避免在野鹅追逐中派遣其他开发人员或您将来的自我。

关于测试时应避免的事情,仍有很多要学习的知识,但我相信这是一个好的开始。 对于TDD来说是相当陌生的人们,在跳入更高级的水域之前,应该能够立即处理这几个AntiPatterns。

翻译自: https://code.tutsplus.com/articles/antipatterns-basics-rails-tests--cms-26011

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值