【问题标题】:Is it okay to run for loops in functional test methods?可以在功能测试方法中运行 for 循环吗?
【发布时间】:2023-03-29 02:47:02
【问题描述】:

是否可以(从概念上)在测试方法中运行 for 循环?

我想在控制器中测试一系列参数值,以确定不同的输入是否返回正确的值。

  test "logged in user - add something - 0 qty" do
    @app = Factory.create(:app)

    (0..5).each do |t|
      @qty = t
      login(:user)
      get :add. :id => @app.id, :qty => @qty
      assert_nil(flash[:error])
      assert_response :redirect
      assert_redirect_to :controller => :apps, :action => :show, :id => @app.id
      if @qty = 0 
        assert_equal(Invite.all.count, @qty + 1)
      else 
        assert_something .........
    end
  end

类似的东西。

【问题讨论】:

  • 顺便说一句,我碰巧在一个项目中工作,我们有一个完整的测试套件,其中每个测试用例都有一个循环。 (该套件测试 Tracemonkey,一个只编译热循环的 JIT。);)

标签: ruby-on-rails ruby unit-testing functional-testing


【解决方案1】:

我通常会尽量避免在测试代码中使用任何类型的条件语句或循环。您希望您的测试尽可能简单,如果您开始在测试中包含逻辑,则必须测试 它们 以确保它们按设计工作。我会将循环分解为单独的测试用例,这样如果其中任何一个失败,就更容易准确地查明是什么输入导致了失败。当测试失败时,应该立即清楚导致它的原因。您不必分析测试代码来找出答案。

更新:

我确实想补充一点,有一些非常罕见的情况,您希望在测试用例中有一个循环。一个具体的例子是当您测试并发问题时。这是一般规则的一个例外,您应该有一个很好且易于理解的理由在您的测试中使用任何类型的逻辑。

【讨论】:

  • 这听起来是个好主意,但是您如何清除混乱呢?因为文件可能会变得非常大,您可能会忘记整个文件中正在测试的内容。
  • 如果每个测试都独立存在,那么从技术上讲,它们甚至不需要在同一个文件中。将测试按测试内容分组是个好主意(一个模块的测试都应该在同一个测试文件中),但是如果您的测试代码变得过于笨拙,您可以根据需要将其分解。
  • 我认为如果它显示了您将为问题中的测试编写的代码,这将是一个更好的答案。
【解决方案2】:

是否可以(从概念上)在测试方法中运行 for 循环?

你的意思是,这在政治上正确吗?

它有效,对吧?我试图想象反对意见会是什么。

【讨论】:

  • 有点像“为什么不坚持 GOTO?它工作正常吗?是的,它有效,但并不意味着您应该这样做(除非在极少数情况下,例如使用最多允许标签/地址跳转的语言)。测试是小程序或脚本,它们应该以聪明的方式设计,而不是“为了获得绿色代码覆盖率而采取任何可能的方式”。
  • “从不使用 goto”是一条与其原因脱节的规则:“因为它使理解代码变得困难,并且重构是一场噩梦。”但如果你记得规则背后的原因,你就会知道什么时候可以安全地违反它。 “测试中的循环很糟糕”也是如此,这是没有理由的规则。 “测试中的循环很糟糕,因为它们使测试更难阅读和理解”是一条规则,有其原因。如果你知道原因,你就可以说:“看,这个循环让测试更容易理解,而不是更难,并且没有引入其他问题。我想这个循环是好的。”
  • yoosiba:您的评论让我仍然试图想象反对意见可能是什么。我衷心同意“测试是小程序”,“应该以聪明的方式设计”。问题在于手动循环展开(例如 Bill the Lizard 的回答倡导者)是否“聪明”。
  • 在我看来,测试中的循环有很多值得尊敬的用例。模糊测试实际上需要它。成对(或组合)测试也是如此。唉,有时您确实需要测试“在删除 eval 16 次后”某些东西仍然有效,例如 bugzilla.mozilla.org/show_bug.cgi?id=428366
【解决方案3】:

测试也应该被视为你的软件应该做的什么的“活文档”,所以要尽可能的清楚。

【讨论】:

  • 尤其是更高级别的功能测试,这些测试不仅应该被开发人员而且用户也应该可以阅读,他们可能完全没有任何技巧知道什么是“循环”。
【解决方案4】:

在每个测试中使用多个断言或验证在政治上是不正确的。也就是说,每个人都这样做。在 Cucumber 的测试场景中可以看到其中一种新样式,其中该场景仍然具有极强的可读性,但允许测试多组数据。

但它是 Ruby,如果你是那种严格遵循其他人的指导的人,你就不会使用它。没有正确的方法,只有最常见的并且经常变化。

我曾经问过我的牙医,我应该按什么顺序刷牙、使用牙线和冲洗。他告诉我,他不在乎我是否真的做到了这三个方面。我相信重点是,通常不合标准的实现总比没有好。如果循环使测试变得更有趣,因此更有可能,那么你应该把地狱从你的测试中循环出来。

【讨论】:

    【解决方案5】:

    如果您可以让您的框架意识到这一单一测试实际上正在执行多个测试(使用不同的参数),那就更好了。它允许您在测试报告中查看哪些确切的参数组合失败以及哪些成功。

    【讨论】:

    • 不能再同意了。如果您得到的测试除了参数之外 100% 相等,请将通用测试分解为单个函数,并根据您需要的次数调用该函数。将使测试非连续函数变得更加容易,这需要大量的组合。
    【解决方案6】:

    在某些情况下,您可能需要循环,但您的循环不是其中之一。请记住,向测试添加更多复杂性会使使用它们变得更加困难。当应用程序发展时,测试也会发展。如果你一开始就让它们过于复杂,那么有一天你可能会面临一个选择:

    • 我应该花 3 天时间来重构这个失败的大型旧杂乱测试吗?
    • 是否应该删除测试并编写新的、更简单的优雅(在 3,5 天内)?

    这是一个艰难的选择。在第一个选项中,您浪费时间为不会推动项目前进的事物实施新功能?你有时间吗?你的经理认为你有时间吗?客户是否认为您有时间为此项目支付您的时间?
    第二个选项接缝是合理的,但是,在编写新测试时,你怎么知道你覆盖了所有旧的情况(加上新的)?这一切都在文档中吗?它在测试文档中吗?你还记得他们所有人吗?或者,您可能会检查测试代码并对其进行重构以揭示隐藏在此代码块中的所有案例?这不是成为首选吗?

    不要像遗留代码那样进行测试。没有人想触碰的代码,没有人真正知道,每个人都尽量避免和忽略它。测试应设计为休息应用程序。在应用代码设计时应用许多设计原则。让它们变得简单。分开负责。对它们进行逻辑分组。使它们易于重构。使它们可扩展。您应该考虑很多事情。

    至于你的情况。假设您有一个用例,您的代码对 中的参数执行某些操作(代码中的 0..5 靠得很近,使用更广泛的范围时示例看起来更清晰)。在使用其他值时,它会进行一些异常处理。在这种情况下,您需要测试用例:

    • 当参数 = -1
    • 当参数 = 0
    • 当参数 = 1 时
    • 当参数 = 99 时
    • 当参数 = 100
    • 当参数 = 101 时

    简单、独立的测试用例易于重构、易于阅读,同时仍能正确检查代码。
    您可以添加测试用例,当参数位于 (10,70) 时,您将使用循环来检查行为,但不建议这样做。通过大量测试和广泛的参数范围,这只是浪费资源。如果算法是确定性的,则对一组值执行相同的步骤,如果它适用于一个值,它将适用于所有值。
    尝试阅读等价类、边界值、成对测试、路径覆盖、语句覆盖、分支覆盖和其他测试技术,以使您的测试更好。

    【讨论】:

    • 本文的前半部分似乎将“包含循环的代码”与“没有人愿意接触的大而杂乱的遗留代码”混为一谈。
    • 不,绝对不。当然,在编写测试时,如果需要,您应该毫不犹豫地使用循环——创建是有原因的。我只是指出不应该使用它。 Loop 只是一个工具 - 它本质上没有坏处或好处。可能会被评判的是您对工具的使用。
    【解决方案7】:

    我要把自己加入政治不正确的名单。在测试一系列值时,循环可以增加测试的可读性。此外,它还有助于 DRY,使重构更容易:您是将新参数添加到测试中调用方法的八个位置,还是仅添加一个?

    这是一个使用这种技术的测试。它使用的是本土测试库,但该技术是通用的:

      def test_swap_name
        test_cases = [
          [
            'Paul, Ron P.A.',
            'Ron Paul PA'
          ],
          [
            "PUBLIC, SR., JOHN Q",
            "JOHN Q PUBLIC SR"
          ],
          [
            "SMITH, JR., MARK A",
            "MARK A SMITH JR"
          ],
          [
            'James Brown',
            'James Brown'
          ],
          # (more test cases)
        ]
        for original, swapped in test_cases
          assertInfo("For original = #{original.inspect}") do
            assertEquals(original.swap_name, swapped)
          end
        end
      end
    

    assertInfo 将任意字符串添加到任何异常消息的开头。这样您就可以知道,当测试失败时,正在测试哪些数据:

    ./StringUtil.test.rb
    Method "test_swap_name" failed:
    Assert::BlownAssert: For original = "Paul, Ron P.A.": Expected:
    "Ron Paul PA"
    but got
    "Paul, Ron P.A."
    ./../../testlib/Assert.rb:125:in `fail_test'
    ./../../testlib/Assert.rb:43:in `assertEquals'
    ./StringUtil.test.rb:627:in `test_swap_name'
    

    【讨论】:

    • 使用 Ron Paul 作为样本数据绝对是政治不正确的!
    【解决方案8】:

    只要测试中只有一次断言出现,我通常可以接受测试中的循环。换句话说,这没关系:

    test "something" do
      for item in @collection
        assert_something item
      end
    end
    

    但这不是:

    test "something" do
      for item in @collection
        assert_something item
        assert_something_else item
      end
    end
    

    这些会让你讨厌自己:

    test "something" do
      for item in @collection
        assert_something item
        if x == y
          assert_something_else item
        end
      end
    end
    
    test "something" do
      for item in @collection
        assert_something item
      end
      for item in @collection
        assert_something_else item
      end
    end
    

    唯一一次我会以这种方式编写测试是如果集合中的项目彼此之间存在很大差异,但有一些需要验证共同的共享行为。例如,您可能有十个不同对象的实例,但它们都应该响应某些消息。所以你验证所有实例都可以做鸭子类型方法应该做的事情。但是,如果您有一个始终包含Foo 实例的集合,那么您通常最好只对@collection.first 进行断言。测试执行得更快,并且在所有实例上重复断言并没有真正获得太多收益,而且在大多数情况下,无论如何,您最好还是单独测试 item 与集合的其余部分隔离。如果你感觉特别偏执,这通常是可以的:

    test "items are all Foo" do
      for item in @collection
        assert_kind_of Foo, item, "Everything in @collection must be Foo."
      end
    end
    test "something" do
      assert_something @collection.first
    end
    

    如果集合中有非Foo 对象,“某事”测试无论如何都会失败,但前面的测试非常清楚真正的问题是什么。

    简而言之,避免它,但如果你有充分的理由这样做,那就继续吧。如果它在未来成为问题,测试应该仍然足够简单,以便将其重构为问题较少的东西。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2022-01-16
      • 2014-04-13
      • 2011-12-10
      • 2017-01-28
      • 1970-01-01
      • 1970-01-01
      • 2021-12-28
      • 2017-08-22
      相关资源
      最近更新 更多