关于单元测试请先回忆下面几个问题:
1)单元测试是否依赖网络?如果依赖网络,当没有网的时候怎么办?
2)单元测试是否支持多次可重复执行?
3)dubbo接口怎么单元测试?
4)如何计算单元测试对代码的覆盖率?
在回答上面几个问题前请先看下面介绍:
1、准备测试环境
1)引入依赖包:
junit
junit
4.12
test
org.mockito
mockito-all
1.10.19
test
org.springframework
spring-test
4.3.10.RELEASE
test
com.alibaba
dubbo
2.8.4
com.h2database
h2
1.4.197
test
2)服务启动时加载sql脚本配置
3)sql脚本
init-table.sql
SET MODE MYSQL;
drop table if exists `user_1`;
create table `user_1` (
`id` int(10) not null primary key AUTO_INCREMENT ,
`name` varchar(20) not null default '' ,
`status` int(11) not null DEFAULT 0
);
import-data.sql
SET MODE MYSQL;
insert into `user_1`(`id`,`name`,`status`)values(100,'test1',0);
4)准备单元测试基类,所有测试类继承该基类。
@RunWith(SpringJUnit4ClassRunner.class) //使用junit4进行测试
@ContextConfiguration({"classpath:spring/spring-config.xml"}) //加载配置文件
public class CommonTest {
@Test
public void test(){
}
}
spring-config.xml对应web.xml中的配置
contextConfigLocation
classpath*:/spring/spring-config.xml;
2、使用Junit进行常规单元测试
需要测试的service,包含增、删、改、查等接口。
@Service
public class UserService {
public static final String ERROR_MSG = "this is test exception!";
@Resource
private UserDao userDao;
public int saveUser(User user) {
return userDao.saveUser(user);
}
public User queryUserById(int id) {
return userDao.queryUserById(id);
}
public int updateUserById(User user) {
Preconditions.checkNotNull(user, "用户信息不能为空!");
return userDao.updateUserById(user);
}
public int deleteUserById(int id) {
Preconditions.checkArgument(id > 0, "id(%s)必须大于0!", id);
return userDao.deleteUserById(id);
}
public void testException() {
throw new IllegalStateException(ERROR_MSG);
}
}
单元测试类(继承上面的基类CommonTest.java)注入Service使用@Resource或@Autowired
//继承单元测试基类CommonTest.java
public class UserServiceTest extends CommonTest {
@Resource //注入Service使用@Resource或@Autowired
private UserService userService;
/**
* 创建测试模型
*/
private User createUser() {
Random random = new Random(1000000);
User user = new User();
user.setStatus(0);
user.setName("new name_" + random.nextInt());
return user;
}
@Test
public void saveUser() {
User user = createUser();
// 保存数据
userService.saveUser(user);
Assert.assertTrue(user.getId() > 0);
// 校验保存的数据与查询出来的数据是否一致
User temp = userService.queryUserById(user.getId());
Assert.assertNotNull(temp);
Assert.assertNotNull(temp.getName());
Assert.assertEquals(user.getName(), temp.getName());
Assert.assertEquals(user.getStatus(), temp.getStatus());
}
@Test
public void queryUserById() {
// 检查初始化脚本加载数据是否正确
User user = userService.queryUserById(100);
Assert.assertNotNull(user);
Assert.assertNotNull(user.getName());
Assert.assertEquals("test1", user.getName());
}
@Test
public void updateUserById() {
User user = createUser();
// 保存数据
userService.saveUser(user);
int id = user.getId();
Assert.assertTrue(id > 0);
// 校验保存的数据与查询出来的数据是否一致
User temp = userService.queryUserById(id);
Assert.assertNotNull(temp);
Assert.assertNotNull(temp.getName());
Assert.assertEquals(user.getName(), temp.getName());
Assert.assertEquals(user.getStatus(), temp.getStatus());
// 修改信息+更新数据
user.setStatus(1000);
user.setName("new-name" + new Random(1000000).nextInt());
int updateResult = userService.updateUserById(user);
Assert.assertEquals(1, updateResult);
// 校验更新数据是否成功
temp = userService.queryUserById(id);
Assert.assertNotNull(temp);
Assert.assertNotNull(temp.getName());
Assert.assertEquals(user.getName(), temp.getName());
Assert.assertEquals(user.getStatus(), temp.getStatus());
}
@Test
public void deleteUserById() {
// 检验数据库是否存在数据
User user = userService.queryUserById(100);
Assert.assertNotNull(user);
Assert.assertNotNull(user.getName());
Assert.assertEquals("test1", user.getName());
// 校验删除
int result = userService.deleteUserById(100);
Assert.assertEquals(1, result);
}
}
增、删、改、查主流程测试要点:
查询:初始化数据库表结构时导入数据,校验查询功能是否正常
新增:新增后需要再从数据查询,确保保存的数据与保存后查询的数据一致
修改:
1)先保存数据;
2)再根据保存的数据返回的主键查询数据,确认可在成功;
3)然后修改数据,从数据库获取数据,与修改的数据是否一致
删除:先校验数据是否存在,如果存在,再校验删除功能是否正常
3、Mockito工具介绍:
public class MockitoTest extends CommonTest {
@InjectMocks // mock注入说明:会注入变量
@Resource
private UserService userService;
@Mock // mock一个实例,注入到上面声明的 UserService 中
@Resource
private UserDao userDao;
@Rule // 对异常代码进行mock声明
public ExpectedException thrown = ExpectedException.none();
@Before
public void setUp() {
// mock注解声明及初始化
MockitoAnnotations.initMocks(this);
// 针对 @Mock 类中的方法进行定制:当调用该接口时返回固定值
when(userDao.saveUser(any(User.class))).thenReturn(1314);
when(userDao.updateUserById(any(User.class))).thenReturn(111);
}
@Test
public void testMock() {
// 调用前面指定的mock接口
int result = userService.saveUser(new User());
// 检验返回的固定值
Assert.assertEquals(1314, result);
// 调用前面指定的mock接口
result = userService.updateUserById(new User());
// 检验返回的固定值
Assert.assertEquals(111, result);
}
@Test
public void testThrown() {
// 检验异常类
thrown.expect(IllegalStateException.class);
// 检验异常信息
thrown.expectMessage(UserService.ERROR_MSG);
// 开始执行方法
userService.testException();
}
}
要点介绍:
1)mock service时使用 @InjectMocks
2)mock上面servicek中的变量时,使用 @Mock
3)mock异常时使用 (需要校验异常类及异常信息)
@Rule // 对异常代码进行mock声明
public ExpectedException thrown = ExpectedException.none();
4)@Before中初始化:MockitoAnnotations.initMocks(this);
5)mock类中的方法时使用when(接口名(参数名)).thenRerutn(返回值),样例如下:
when(userDao.saveUser(any(User.class))).thenReturn(1314);
4、dubbo接口mock介绍
1)在test/resource/*中配置dubbo
配置中对应的实现类
@Service
public class DubboServiceImpl implements DubboService {
@Override
public String sayHello(int id) {
if(id == 1){
return "123456";
}else {
return "000000";
}
}
}
dubbo单元测试类(继承基类CommonTest.java)
public class DubboTest extends CommonTest {
@Resource
private DubboService dubboService;
@Test
public void testDubbo(){
String hello = dubboService.sayHello(1);
Assert.assertNotNull(hello);
Assert.assertEquals(hello,"123456");
hello = dubboService.sayHello(123);
Assert.assertNotNull(hello);
Assert.assertEquals(hello,"000000");
}
}
要点介绍:
1)需要配置dubbo配置信息,覆盖正常配置。
2)可实现并自定义实现类
3)正常调用dubbo接口时,返回的结果是上面自定义实现类中返回值
总结:
1)单元测试不依赖网络:dubbo方法、http调用接口使用Mock;jdbc使用内存数据库(H2)
2)单元测试需要支持多次可重复执行:服务每次启动时都会加载sql脚本(重新初始化表结构及导入原始数据),保证数据在每次执行都是同样的状态
3)dubbo接口mock测试:使用本地伪装
4)如何计算单元测试对代码的覆盖率:使用sonar,详细介绍待续
本文代码见:https://gitee.com/liuyaohua/spring-simple/tree/master/dubbo-test
本文已同步更新到公众号,沟通交流请关注公众号。