在开发中, 单元测试是必不可少的, 个人觉得越是庞大复杂的系统, 就越应该具有良好的测试覆盖, 也许一开始会觉得很费时和多余, 但这无疑是能把问题暴露在上线之前的有利保障。按照一般的系统架构, 通常会将系统垂直分为Dao, Service, Controller三层, 所以我们需要单独对这几层进行。
这是系统功能最基础的支持, 必然保证准确无误。对于DB类型, 我们无非就是RDBS(如MySQL)和NoSQL(如Redis), 那我们要怎么比较优雅进行Dao的单元测试呢?个人觉得不依赖任何环境, 是最优雅的。 先说关于RDBS类型的测试, 我们可以利用Java实现的数据库H2这种内嵌数据库就能完成, 即在单元测试中使用H2作为数据库(这需要忽略其与对应RDBS的差异),对应的Spring配置大概为
<!-- MyBatis 配置 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="configLocation" value="classpath:spring/mybatis-config.xml"/>
<property name="mapperLocations" value="classpath:mapper/*Mapper.xml"/>
</bean>
<!-- 事务管理器配置, 使用jdbc事务 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 使用annotation定义事务 -->
<tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true"/>
<!-- 嵌入式内存中数据库 -->
<jdbc:embedded-database id="dataSource" type="H2">
<jdbc:script location="classpath:h2/user.sql"/><!-- 对应模块的数据库schema -->
</jdbc:embedded-database>
这样, RDBMS的测试就能在内部就完成。那对于Redis这种NoSQL, 很遗憾没能对应这样的解决方案(若读者有这样的解决方案, 请@me) 对于Dao测试, 若有对应的测试服务器, 当然可以在CI上作相应的环境切换, 利用外部环境完成测试。
Service主要负责业务逻辑, 既然是业务逻辑, 我们测试就应该只是逻辑, 不应该包含其他无关动作(外部调用, Dao操作等), 如何测试逻辑呢? 这时我们需要Mockito, PowerMock等Mock测试库(这也是谷歌推荐的测试库), 测试Service将预演系统的业务流程, 这也是十分关键的。请看下面的代码样例:
@Test
public void testSignupNicknameExist(){
User signuping = mockUser();
Mockito.when(userDao.findByNick(anyString())).thenReturn(new User());
CatchExceptionBdd.when(userManager).createUser(signuping);
then(caughtException())
.isInstanceOf(ServiceException.class)
.hasMessage("user.nickname.used")
.hasNoCause();
}
@Test
public void testSignupEmailExist() {
User signuping = mockUser();
Mockito.when(userDao.findByNick(anyString())).thenReturn(null);
Mockito.when(userDao.findByEmail(anyString())).thenReturn(new User());
CatchExceptionBdd.when(userManager).createUser(signuping);
then(caughtException())
.isInstanceOf(ServiceException.class)
.hasMessage("user.email.used")
.hasNoCause();
}
@Test
public void testSignupMobileExist() {
User signuping = mockUser();
Mockito.when(userDao.findByNick(anyString())).thenReturn(null);
Mockito.when(userDao.findByEmail(anyString())).thenReturn(null);
Mockito.when(userDao.findByMobile(anyString())).thenReturn(new User());
CatchExceptionBdd.when(userManager).createUser(signuping);
then(caughtException())
.isInstanceOf(ServiceException.class)
.hasMessage("user.mobile.used")
.hasNoCause();
}
要像上面一样捕获异常, 还需要一些Assert库, 如fest-assert, assertj等.
若Controller层做得比较轻薄, 个人觉得单元测试不是那么必要, 或者也可采用Service层一样的Mock方法进行测试。当然, SpringMVC也为我们提供了一种真实的 测试Controller层的解决方案。如,
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration(value = "src/main/webapp")
@ContextHierarchy({
@ContextConfiguration(name = "parent", locations = "classpath:spring/spring-root.xml"),
@ContextConfiguration(name = "child", locations = "classpath:spring/spring-mvc.xml")
})
public class UserControllerTest {
@Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
@Before
public void setUp() {
mockMvc = webAppContextSetup(wac).build();
}
@Test
public void testLogin() throws Exception{
mockMvc.perform(
post("/user/login4") // RequestMapping.value
.contentType(MediaType.APPLICATION_JSON).content("{\"username\":\"haolin\", \"password\":\"123456\"}")) //当Controller方法参数为@RequestBody时
.andExpect(status().isOk()) //状态码200
.andExpect(content().contentType(MediaType.APPLICATION_JSON)) //返回内容类型
.andExpect(jsonPath("$.username", is("haolin"))) //json-path库可用于解析json结果
.andExpect(jsonPath("$.password", is("123456")));
}
}
单元测试是系统稳定的有利保证.