为什么要避免单元测试中的条件逻辑以及如何避免?
Why should you avoid conditional logic in unit tests and how?
想象一下 类:
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()
的结果的场景?
在单元测试中避免条件和循环的原因是它们变得难以阅读和维护。每个条件很可能代表它自己的测试场景,并且应该在单独的单元测试中。
然后,每个测试都将充当该功能的文档或规范。查看测试的人会确切地知道 expected/not 期望的功能是什么。
对于上面的特定情况,您可以采用简单的方法,通过向列表中添加一些产品来测试求和功能。
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());
}
}
在我看来,对 create/insert 个项目使用上面的循环是没有必要的,并且会增加测试的复杂性。
您当然可以进行更多测试来测试边界条件,例如总和溢出、排除负数等。
对条件和循环的恐惧有两方面:
- 您的 TEST 代码中可能存在错误,因此测试 运行 不正确,或者更糟的是,条件块内的断言未断言。
- 读起来更难。
其他人的回答是通过复制和粘贴对列表进行硬编码。我不喜欢那样,因为它不仅会使测试混乱,而且使以后重构变得更加困难。
如果代码与 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)));
}
}
想象一下 类:
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()
的结果的场景?
在单元测试中避免条件和循环的原因是它们变得难以阅读和维护。每个条件很可能代表它自己的测试场景,并且应该在单独的单元测试中。 然后,每个测试都将充当该功能的文档或规范。查看测试的人会确切地知道 expected/not 期望的功能是什么。
对于上面的特定情况,您可以采用简单的方法,通过向列表中添加一些产品来测试求和功能。
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());
}
}
在我看来,对 create/insert 个项目使用上面的循环是没有必要的,并且会增加测试的复杂性。
您当然可以进行更多测试来测试边界条件,例如总和溢出、排除负数等。
对条件和循环的恐惧有两方面:
- 您的 TEST 代码中可能存在错误,因此测试 运行 不正确,或者更糟的是,条件块内的断言未断言。
- 读起来更难。
其他人的回答是通过复制和粘贴对列表进行硬编码。我不喜欢那样,因为它不仅会使测试混乱,而且使以后重构变得更加困难。
如果代码与 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)));
}
}