当我们准备开始编写单元测试或者集成测试的第一件事情就是先做好相关配置。
如果我们想写一份干净的测试,那么也一定要按干净简单的方式去配置它们,这很明显,对吧?
不幸的是,很多开发者会以不重复的原则(DRY)为借口而选择忽视这件事情。
这是错误的。
这篇博客将会指出 DRY 原则存在的问题和一种更好的配置测试用例的方式。
假使我们需要使用 SpringMVC 测试框架对 SpringMVC 的控制器(Controller) 编写测试用例。第一个需要测的控制器叫 TodoController,当然别的控制器对应的测试也同样需要写。
作为开发,我们知道重复的代码不是一件好事。当我们写代码的时候,要遵循 DRY 原则,它是这样描述的:
在一个系统中的每一项事物必须有一个唯一的,明确的,权威的表示。
我猜这也是很多程序员喜欢在他们的测试用例中使用继承的一个原因。他们认为继承是重用代码和配置最廉价和简单的方式。这也是为什么他们喜欢把公用代码和配置放在实际测试类的公有父类中的原因。
让我们看看如何通过这种方式实现配置“单元测试”。
首先,我们需要创建一个配置了 SpringMVC 测试框架的抽象基类,并且它的子类可以通过实现 setUpTest(MockMvc mockMvc) 方法来进行补充配置。
AbstractControllerTest 类的源代码看起来是这样的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {WebUnitTestContext.class})
@WebAppConfiguration
public abstract class AbstractControllerTest {
private MockMvc mockMvc;
@Autowired
private WebApplicationContext webAppContext;
@Before
public void setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext).build();
setupTest(MockMvc mockMvc)
}
protected abstract void setUpTest(MockMvc mockMvc);
}
然后, 我们还需要实现一个负责创建需要的冒烟对象和控制器对象的实际测试类。TodoControllerTest 类的源代码看起来是这样的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import org.mockito.Mockito;
import org.springframework.test.web.servlet.MockMvc;
public class TodoControllerTest extends AbstractControllerTest {
private MockMvc mockMvc;
@Autowired
private TodoService serviceMock;
@Override
protected void setUpTest(MockMvc mockMvc) {
Mockito.reset(serviceMock);
this.mockMvc = mockMvc;
}
//Add test methods here
}
这个测试类相当简洁,但它有个明显的失陷:
如果我们想看看我们的测试用例是如何配置的,我们必须要 TodoControllerTest 和 AbstractControllerTest 两个类的源代码。
这看起来像是一个小问题但它会导致我们的注意力不停在测试类和测试基类间切换。这需要一个精神层面的上下文切换,然后这个切换代价是很昂贵的。
当然你有可能会说这种用继承实现的测试用例进行精神切换的代价很低,因为基类相当简单。这是对的,但,要知道,在真正的测试程序中不会总是这么简单。
精神切换的实际代价取决于测试类继承树的深度和配置的复杂度。
我们可以通过把所有的配置信息都放在实际测试类中来提高可读性。我们可以这样干:
如果我们按这些规则重写测试类后的代码看起来是这样的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import org.junit.Before;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {WebUnitTestContext.class})
@WebAppConfiguration
public class TodoControllerTest {
private MockMvc mockMvc;
@Autowired
private TodoService serviceMock;
@Autowired
private WebApplicationContext webAppContext;
@Before
public void setUp() {
Mockito.reset(serviceMock);
mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext).build();
}
//Add test methods here
}
在我看来,新的配置方式比原来把配置信息拆分后放在 TodoControllerTest 和 AbstractControllerTest 两个类中的方式简单和整洁多了。
不幸的是,天下没有免费的午餐。
每一项设计决策都是利和弊之间的较量。现在也不例外。
按我们的方式做测试配置至少有以下好处:
同样,这么做也有坏处:
如果我们的目标仅仅是尽快完成测试用例,很明显我们应该消除重复代码和配置。
但,这不是我唯一的目标。
至少有两个原因会让我认为这些付出是值得的: