【问题标题】:Why should you avoid conditional logic in unit tests and how? [closed]为什么要在单元测试中避免条件逻辑以及如何避免? [关闭]
【发布时间】:2015-03-08 23:00:26
【问题描述】:

想象一下有以下类:

public class Product {
   private String name;
   private double price;

   // Constructors, getters and setters
}

public class Products {
   private List<Product> products;

   // CRUD methods

   public double getTotalPrice() {
      // calculates the price of all products
   }
}

我已经读过在单元测试中应该避免条件逻辑(if 和循环),但不明白为什么,更重要的是如何。如何有效地测试我在Products 中添加一些不同价格的产品,然后在不使用循环的情况下验证getTotalPrice() 的结果的场景?

【问题讨论】:

  • 为什么不直接用三个语句明确添加(比如说)3 个不同的产品?无论如何,这可能比循环更简单。
  • 我相信不会有问题,但我想了解它背后的意义。如果我想用更多项目测试它怎么办?如果计算更复杂怎么办?一般来说,我想了解我应该如何验证在一个类中完成的操作,该类使用项目列表计算特定值,以及如何避免迭代测试用例中作为参数给出的所有项目。
  • 为什么要迭代 test 代码?您可能会在 real 代码中进行迭代,但在测试代码中,您可能只是硬编码总和或将其显式为 5+10+20 或其他。并不是说我认为迭代必然是测试中的一个问题——尽管if 条件至少很少见。不过,我建议不要太教条。我比较关心double 用于货币数据...
  • "您可以在真实代码中进行迭代,但在测试代码中您可能只是硬编码总和或将其明确为 5+10+20 或其他。"这可能回答了我的问题。并不是说我在测试当前案例时遇到问题,而是我宁愿理解为什么在不同的来源中说明应该避免条件逻辑。我还想知道如何测试方法,需要来自列表中不同对象的信息。

标签: java unit-testing testing junit automated-tests


【解决方案1】:

条件句和循环的恐惧有两个方面:

  1. 您的 TEST 代码中可能存在错误,导致测试无法正常运行,或者更糟糕的是,条件块内的断言未断言。
  2. 它更难阅读。

其他人回答只是通过复制和粘贴对列表进行硬编码。我不喜欢这样,因为它不仅使测试变得混乱,而且使以后更难重构。

如果代码类似于 Invisible Arrow 的答案:

@Test
public void testsTotalPriceAsSumOfProductPrices() {
    Products products = new Products(); // Or any appropriate constructor
    products.add(new Product("first", 10)); // Assuming such a constructor exists
    products.add(new Product("second", 20));
    products.add(new Product("second", 30));
    assertEquals("Sum must be eq to 60", 60, products.getTotalPrice());
}

Product 的构造函数发生了变化,你必须在很多不同的地方更改所有测试代码。

我更喜欢制作帮助方法,使测试代码更能揭示意图,并将重构期间要更改的位置数量保持在最低限度(希望只有一个)。

这不是更容易阅读吗:

 @Test
public void testsTotalPriceAsSumOfProductPrices() {
    Products products = new Products(); 

    addProductsWithPrices(products, 10,20,30);

    assertEquals(60, products.getTotalPrice());

}

private static void addProductsWithPrices(Products products, Double...prices){
  for(Double price : prices){
     //could keep static counter or something
     //to make names unique
     products.add(new Product("name", price));
  }
}

是的,这确实使用了 for 循环。但是,如果您担心它有错误或者辅助方法更复杂,您也可以为它们编写额外的测试!最终,您可能希望将这些辅助方法分解到它们自己的类中,以便在其他测试中重用它们。

此外,您可以看到测试方法隐藏了制作与测试无关的产品(name)所需的其他字段。我们的测试只关心价格,所以我们的助手只是编造名字,阅读测试的人不会被额外的参数弄糊涂。很明显,这个测试确保 10+20+30 ==60。

最后,如果我们将价格从double 更改为某种Currency 对象,我们只需要在一次地方进行更改,我们的测试代码同样可读。

 private static void addProductsWithPrices(Products products, Double...prices){
  for(Double price : prices){
     //could keep static counter or something
     //to make names unique
     products.add(new Product("name", Currency.valueOf(price)));
  }
}

【讨论】:

    【解决方案2】:

    在单元测试中避免条件和循环的原因是它们变得难以阅读和维护。每个条件很可能代表它自己的测试场景,并且应该在单独的单元测试中。 然后,每个测试都将作为功能的文档或规范。查看测试的人会确切地知道该功能的预期/不预期。

    对于上述特定情况,您可以采用简单的方法并通过向列表中添加一些产品来测试求和函数。

    public class ProductsTest {
        @Test
        public void testsTotalPriceAsSumOfProductPrices() {
            Products products = new Products(); // Or any appropriate constructor
            products.add(new Product("first", 10)); // Assuming such a constructor exists
            products.add(new Product("second", 20));
            products.add(new Product("second", 30));
            assertEquals("Sum must be eq to 60", 60, products.getTotalPrice());
        }
    }
    

    在我看来,没有必要使用上面的循环来创建/插入项目,这会增加测试的复杂性。

    您当然可以进行更多测试来测试边界条件,例如总和溢出、排除负数等。

    【讨论】:

      猜你喜欢
      • 2020-08-12
      • 2016-06-30
      • 2011-05-09
      • 2019-03-23
      • 1970-01-01
      • 2011-06-24
      相关资源
      最近更新 更多