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);

    }
}