TDD(测试驱动开发)演示案例:SpringBoot+Junit4+myBatis

本文通过一个小型电商系统,详细演示了测试驱动开发(TDD)的全过程,包括技术环境搭建、功能需求分析、模块设计、代码实现及测试案例编写。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

简介

通过一个小型的电商系统,来演示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);

    }
}

 

测试驱动的编程是 XP 困扰程序员的一个方面。对于测试驱动的编程意味着什么以及如何去做,大多数人都做出了不正确的假设。这个月,XP 方面的讲师兼 Java 开发人员 Roy Miller 谈论了测试驱动的编程是什么,它为什么可以使程序员的生产力和质量发生巨大变化,以及编写测试的原理。请在与本文相随的 论坛中提出您就本文的想法,以飨笔者和其他读者。(您也可以单击本文顶部或底部的“讨论”来访问该论坛。) 最近 50 年来,测试一直被视为项目结束时要做的事。当然,可以在项目进行之中结合测试测试通常并不是在 所有编码工作结束后才开始,而是一般在稍后阶段进行测试。然而,XP 的提倡者建议完全逆转这个模型。作为一名程序员,应该在编写代码 之前编写测试,然后只编写足以让测试通过的代码即可。这样做将有助于使您的系统尽可能的简单。 先编写测试 XP 涉及两种测试: 程序员测试和 客户测试测试驱动的编程(也称为 测试为先编程)最常指第一种测试,至少我使用这个术语时是这样。测试驱动的编程是让 程序员测试(即单元测试 ― 重申一下,只是换用一个术语)决定您所编写的代码。这意味着您必须在编写代码之前进行测试测试指出您 需要编写的代码,从而也 决定了您要编写的代码。您只需编写足够通过测试的代码即可 ― 不用多,也不用少。XP 规则很简单:如果不进行程序员测试,则您不知道要编写什么代码,所以您不会去编写任何代码。 测试驱动开发(TDD)是极限编程的重要特点,它以不断的测试推动代码的开发,既简化了代码,又保证了软件质量。本文从开发人员使用的角度,介绍了 TDD 优势、原理、过程、原则、测试技术、Tips 等方面。 背景 一个高效的软件开发过程对软件开发人员来说是至关重要的,决定着开发是痛苦的挣扎,还是不断进步的喜悦。国人对软件蓝领的不屑,对繁琐冗长的传统开发过程的不耐,使大多数开发人员无所适从。最近兴起的一些软件开发过程相关的技术,提供一些比较高效、实用的软件过程开发方法。其中比较基础、关键的一个技术就是测试驱动开发(Test-Driven Development)。虽然TDD光大于极限编程,但测试驱动开发完全可以单独应用。下面就从开发人员使用的角度进行介绍,使开发人员用最少的代价尽快理解、掌握、应用这种技术。下面分优势,原理,过程,原则,测试技术,Tips等方面进行讨论。 1. 优势 TDD的基本思路就是通过测试来推动整个开发的进行。而测试驱动开发技术并不只是单纯的测试工作。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值