TDD(测试驱动开发)演示案例:SpringBoot+Junit4+myBatis
简介
通过一个小型的电商系统,来演示TDD开发过程
TDD开发流程
技术环境
IDEA
SpringBoot 2.1.1.RELEASE
Junit4.12
myBatis
案例源码
https://gitee.com/sujianfeng/lab-tdd
功能需求
用户模块
用户信息:用户名、密码、年龄、累计充值金额、累计使用金额、当前余额、消费积分
模块功能:新增用户、查询用户
充值模块
用户充值
消费模块
用户购买
功能说明
积分规则:
当用户累计消费100元以下,每元1累积积分
当用户累计消费101元到1000元,每1元累计2积分
当用户累计消费1001元以上,每1元累计3积分
开发设计
技术与规范
RESTful规范
三层体系:controller、service、dao层
持久化框架:myBatis
数据库:mySql
数据库设计
用户表、充值记录表、消费记录表
模块与类定义
实体类定义
用户:LabUser
充值记录:LabAddMoney
购买记录:LabBuyGood
接口方法定义
新增用户
查询用户
充值接口
消费接口
TDD开发过程
利用模板自动生成测试和生成代码框架
开发标准化后,完全可以使用模板化生成框架代码
Controller测试代码编写
先准备好一个ControllerTestBase基础类,主要是使用mockMvc方式模拟好web请求,并将rest请求和返回的结果检查统一写好,方便调用,可用通过源码网址(https://gitee.com/sujianfeng/lab-tdd
)获取代码。
从这里大家可用看出,编写测试代码只要根据开发设计好的文档进行编写,基本上不怎么思考,从这里我们应该可用体会到,TDD是以需求为中心进行编写单元测试代码。
一个模块对应单元测试类,一个接口对应一个单元测试方法:
UserControllerKotlinTest 用户测试
public class UserControllerTest extends ControllerTestBase {
@Override
public void beforeTest() {
//测试前环境准备
//初始化session等
}
@Override
public void afterTest() {
//测试后恢复现场
}
@Test
public void addUser() throws Exception {
post("/user",
new HashMap<String, String>(){{put("username", "张三"); put("password", "123");}},
new HashMap<String, Object>(){{put("$.success", true); put("$.message", "新增成功!");}}
);
}
@Test
public void getUser() throws Exception {
get("/getUser",
new HashMap<String, String>(){{put("id", "1");}},
new HashMap<String, Object>(){{put("$.success", true);}}
);
}
}
RechargeControllerTest 充值测试
public class RechargeControllerTest extends ControllerTestBase {
@Override
public void beforeTest() {
//测试前环境准备
//初始化session等
}
@Override
public void afterTest() {
//测试后恢复现场
}
/**
* 充值
* @throws Exception
*/
@Test
public void addMoney() throws Exception {
post("/addMoney",
new HashMap<String, String>(){{put("id", "1"); put("addMoney", "1000");}},
new HashMap<String, Object>(){{put("$.success", true);}}
);
}
}
ConsumeControllerTest 消费测试
public class ConsumeControllerTest extends ControllerTestBase {
@Override
public void beforeTest() {
//测试前环境准备
//初始化session等
}
@Override
public void afterTest() {
//测试后恢复现场
}
/**
* 消费测试
* @throws Exception
*/
@Test
public void buyGood() throws Exception {
post("/buyGood",
new HashMap<String, String>(){{put("id", "1"); put("useMoney", "1000"); put("goodName", "apple");}},
new HashMap<String, Object>(){{put("$.success", true);}}
);
}
}
运行单元测试
几个接口测试都失败了,很显然这是正确的反馈,我们的产品代码都还没写,接下来开始准备编写产品代码。
Controller产品代码编写
UserController 用户
TDD就是先写测试代码后写产品代码,那么我们就可用根据测试代码编写产品代码了。
注意:这里只是简单写了下Controller的产品代码,目前是为了先让他测试通过(当然也可以先不管它测试是否通过,后面再测试也行,看个人习惯),后面再补充业务代码。这样写的意义在于:保证接口的输入输出是符合功能需求的。
一个单位测试类对应一个产品代码类,一个测试方法对应一个产品代码方法:
@RestController
public class UserController {
@PostMapping("/user")
public Map<String, Object> addUser(LabUser labUser){
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("message", "新增成功!");
return result;
}
@GetMapping("/getUser")
public Map<String, Object> getUser(int id){
Map<String, Object> result = new HashMap<>();
result.put("success", true);
return result;
}
}
RechargeController 充值
@RestController
public class RechargeController {
@PostMapping("/addMoney")
public Map<String, Object> addMoney(int id, int addMoney){
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("message", "充值成功!");
return result;
}
}
ConsumeController消费
@RestController
public class ConsumeController {
@PostMapping("/buyGood")
public Map<String, Object> buyGood(int id, int useMoney, String goodName){
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("message", "消费成功!");
return result;
}
}
运行UserControllerKotlinTest
从上面代码看出,我们看出,只是写了接口的基本实现,写完,我们再来测试下:
Ok,那么我们基本实现了controller层的代码编写,并且通过单元测试保证了接口的基本功能是没有问题的,那么接下来我们可用进行service层代码的编写。
Servcice层测试代码编写
UserServiceTest用户测试
由于UserService依赖UserDao,需要使用mock把UserDao隔离出去:
userDao = mock(IUserDao.class);
当访问userDao时,根据模拟返回值:
when(userDao.addUser(opResult, labUser)).thenReturn(1);
when(userDao.getUser(opResult, 1)).thenReturn(labUserReturn);
使用反射工具类,将userDao注入到userService的私有属性中:
ReflectionTddUtils.setFieldValue(userService, "userDao", userDao);
public class UserServiceTest {
private static UserService userService;
private static RestResult opResult = RestResult.create();
private static IUserDao userDao;
@BeforeClass
public void beforeTest() throws NoSuchFieldException, IllegalAccessException {
userDao = mock(IUserDao.class);
userService = new UserService();
ReflectionTddUtils.setFieldValue(userService, "userDao", userDao);
}
/**
* 创建用户测试
*/
@Test
public void addUser(){
LabUser labUser = new LabUser();
labUser.setUsername("张三");
labUser.setAge(12);
when(userDao.addUser(opResult, labUser)).thenReturn(1);
int update = userService.addUser(opResult, labUser);
assertThat(update, equalTo(1));
}
/**
* 查询用户测试
*/
@Test
public void getUser(){
LabUser labUserReturn = new LabUser();
labUserReturn.setId(1);
when(userDao.getUser(opResult, 1)).thenReturn(labUserReturn);
LabUser labUser = userService.getUser(opResult, 1);
assertThat(labUser.getId(), equalTo(1));
}
}
UserService产品代码编写
@Service
public class UserService implements IUserService {
@Autowired
private IUserDao userDao;
@Override
public int addUser(RestResult opResult, LabUser labUser) {
return userDao.addUser(opResult, labUser);
}
@Override
public LabUser getUser(RestResult opResult, int id) {
return userDao.getUser(opResult, id);
}
}
运行UserServiceTest
RechargeServiceTest充值测试代码
public class RechargeServiceTest {
private static IRechargeService rechargeService;
private static IRechargeDao rechargeDao;
private static RestResult opResult = RestResult.create();
@BeforeClass
public static void beforeTest() throws NoSuchFieldException, IllegalAccessException {
rechargeDao = mock(IRechargeDao.class);
rechargeService = new RechargeService();
ReflectionTddUtils.setFieldValue(rechargeService, "rechargeDao", rechargeDao);
}
@Test
public void addMoney(){
when(rechargeDao.addMoney(opResult, 1, 1000)).thenReturn(1);
int update = rechargeService.addMoney(opResult, 1, 1000);
assertThat(update, equalTo(1));
}
}
RechargeService充值产品代码
public class RechargeService implements IRechargeService {
@Autowired
private IRechargeDao rechargeDao;
@Override
public int addMoney(RestResult opResult, int id, int addMoney) {
return rechargeDao.addMoney(opResult, id, addMoney);
}
}
ConsumeServiceTest消费测试代码
public class ConsumeServiceTest {
private static IConsumeService consumeService;
private static IConsumeDao consumeDao;
private static RestResult restResult = RestResult.create();
@BeforeClass
public static void beforeClass() throws NoSuchFieldException, IllegalAccessException {
consumeService = new ConsumeService();
consumeDao = mock(IConsumeDao.class);
ReflectionTddUtils.setFieldValue(consumeService, "consumeDao", consumeDao);
}
@Test
public void test(){
when(consumeDao.buyGood(restResult, 1, 3000, "apple")).thenReturn(1);
int update = consumeService.buyGood(restResult, 1, 3000, "apple");
assertThat(update, equalTo(1));
}
}
ConsumeService消费产品代码
public class ConsumeService implements IConsumeService {
@Autowired
private IConsumeDao consumeDao;
@Override
public int buyGood(RestResult opResult, int id, int useMoney, String goodName) {
return consumeDao.buyGood(opResult, id, useMoney, goodName);
}
}
全部测试service的测试代码
Dao层测试代码编写
Dao层测试由于依赖sqlSessionTemplate ,那么也需要把sqlSessionTemplate 进行隔离:
sqlSessionTemplate = mock(SqlSessionTemplate.class);
UserDaoTest 用户dao测试
public class UserDaoTest {
private static SqlSessionTemplate sqlSessionTemplate;
private static IUserDao userDao;
private static RestResult restResult = RestResult.create();
@BeforeClass
public static void beforeClass() throws NoSuchFieldException, IllegalAccessException {
sqlSessionTemplate = mock(SqlSessionTemplate.class);
userDao = new UserDao();
ReflectionTddUtils.setFieldValue(userDao, "sqlSessionTemplate", sqlSessionTemplate);
}
@Test
public void space(){
assertThat(userDao.space(), equalTo("UserDao"));
}
@Test
public void addUser(){
LabUser labUser = new LabUser();
labUser.setId(1);
labUser.setUsername("张三");
when(sqlSessionTemplate.insert(userDao.space() + ".insertUser", labUser)).thenReturn(1);
int update = userDao.addUser(restResult, labUser);
assertThat(update, equalTo(1));
}
@Test
public void getUser(){
Map<Object, Object> params = new HashMap<>();
params.put("condition", " and id = 1");
LabUser labUser = new LabUser();
labUser.setId(1);
labUser.setUsername("张三");
List<Object> rows = new ArrayList<>();
rows.add(labUser);
when(sqlSessionTemplate.selectList(userDao.space() + ".queryLabUsers", params)).thenReturn(rows);
LabUser labUserReturn = userDao.getUser(restResult, 1);
assertThat(labUserReturn.getId(), equalTo(labUser.getId()));
}
@Test
public void queryLabUsers(){
Map<Object, Object> params = new HashMap<>();
params.put("condition", " and id = 1");
LabUser labUser = new LabUser();
labUser.setId(1);
labUser.setUsername("张三");
List<Object> rows = new ArrayList<>();
rows.add(labUser);
when(sqlSessionTemplate.selectList(userDao.space() + ".queryLabUsers", params)).thenReturn(rows);
List<LabUser> rowsReturn = userDao.queryLabUsers(restResult, " and id = 1");
assertThat(rowsReturn.get(0).getId(), equalTo(labUser.getId()));
}
}
UserDao用户dao产品代码
@Repository
public class UserDao implements IUserDao {
@Autowired
private SqlSessionTemplate sqlSessionTemplate;
@Override
public String space(){
return StringUtilsEx.rightStr(this.getClass().getName(), ".");
}
/**
* 新增用户
* @param opResult
* @param labUser
* @return
*/
@Override
public int addUser(RestResult opResult, LabUser labUser) {
return sqlSessionTemplate.insert(space() + ".insertUser", labUser);
}
/**
* 查询用户
* @param opResult
* @param id
* @return
*/
@Override
public LabUser getUser(RestResult opResult, int id) {
List<LabUser> rows = queryLabUsers(opResult, String.format(" and id = %s", id));
return rows.size() > 0 ? rows.get(0) : null;
}
/**
* 根据条件查询多个用户信息
* @param condition
* @return
*/
public List<LabUser> queryLabUsers(RestResult restResult, String condition){
Map<Object, Object> params = new HashMap<>();
params.put("condition", condition);
List<LabUser> rows = sqlSessionTemplate.selectList(space() + ".queryLabUsers", params);
return rows;
}
/**
* 用户余额变更
* @param restResult
* @param userId
* @param money
*/
public int userMoneyUpdate(RestResult restResult, int userId, int money){
Map<String, Object> params = new HashMap<>();
params.put("userId", userId);
params.put("money", money);
params.put("totalAddMoney", money > 0 ? money : 0);
params.put("totalUseMoney", money < 0 ? money : 0);
int update = sqlSessionTemplate.update(space() + ".updateUserMoney", params);
if (update == 0){
LabUser labUser = new LabUser();
labUser.setId(userId);
labUser.setTotalAddMoney(0);
labUser.setTotalAddMoney(0);
labUser.setScore(0);
labUser.setRemainMoney(money);
update = addUser(restResult, labUser);
}
return update;
}
}
其他三个dao代码类似,就不列出来了,完整代码在此(https://gitee.com/sujianfeng/lab-tdd)
全部单元测试效果
查看测试覆盖率
集成测试
集成测试整个系统所有模块联合起来进行测试,集成测试一定是要在单元测试全部通过后才进行测试。
@Transactional
public class labTddIntegrationTest extends ControllerTestBase {
@Autowired
private IUserService userService;
@Autowired
private IConsumeService consumeService;
@Autowired
private IRechargeService rechargeService;
@Autowired
private IScoreService scoreService;
private LabUser labUser;
@Override
public void beforeTest() {
RestResult restResult = RestResult.create();
//新增一个用户
userService.addUser(restResult, new LabUser(0, "张三"));
//取出这个用户数据
List<LabUser> labUsers = userService.queryLabUsers(restResult, "");
labUser = labUsers.get(0);
}
private void assertUserInfo(LabUser labUserTmp, int totalAddMoney, int totalUseMoney, int remainMoney, int scoreMoney){
assertThat("用户累计充值金额存储错误!", labUserTmp.getTotalAddMoney(), equalTo(totalAddMoney));
assertThat("用户累计消费金额存储错误!", labUserTmp.getTotalUseMoney(), equalTo(totalUseMoney));
assertThat("用户可用金额存储错误!", labUserTmp.getRemainMoney(), equalTo(remainMoney));
assertThat("用户可用积分存储错误!", labUserTmp.getScore(), equalTo(scoreMoney));
}
@Override
public void afterTest() {
}
@Test
public void test(){
LabUser labUserTmp = null;
RestResult restResult = RestResult.create();
//充值1000
rechargeService.addMoney(restResult, labUser.getId(), 1000);
labUserTmp = userService.getUser(restResult, labUser.getId());
assertUserInfo(labUserTmp, 1000, 0, 1000, 0);
//再充值12000
rechargeService.addMoney(restResult, labUser.getId(), 12000);
labUserTmp = userService.getUser(restResult, labUser.getId());
assertUserInfo(labUserTmp, 13000, 0, 13000, 0);
//消费5500
consumeService.buyGood(restResult, labUser.getId(), 5500, "apple");
labUserTmp = userService.getUser(restResult, labUser.getId());
assertUserInfo(labUserTmp, 13000, 5500, 13000 - 5500, 100 + 900 * 2 + (5500 - 1000) * 3);
}
}