# atomic **Repository Path**: dreamya0/atomic ## Basic Information - **Project Name**: atomic - **Description**: 单元、集成测试,dubbo、http接口测试框架 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 1 - **Created**: 2018-04-24 - **Last Updated**: 2024-01-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 项⽬背景 atomic的诞⽣主要⽤于解决,写测试代码⼯作量⼤且效率低下、依赖隔离难、测试⻔槛⾼、⽆法实现⾃动化、测试结果⽆法统⼀展示等因素。从根本上提升测试质量和测试效率 # 项⽬名称由来 atomic 中⽂名:原⼦,寓意⽤来解决测试的最⼩单元,⽤于攻克单测⼀系列痛点问题(哎~ 其实是想了半天没有想到⽐较⾼⼤上的名字,刚好看到atomic这个单词,所有就⽤了这个名字) # 单元测试、集成测试使⽤说明 ## 准备工作 开始使用单测框架之前我们需要在项目的pom文件中添加两个maven依赖,一个是测试框架本身的maven依赖,一个是测试用例生成的maven插件 ### 单测框架坐标 ```xml com.atomic atomic 1.0.0-SNAPSHOT ``` ### 测试用例生成插件坐标 ```xml com.atomic.plugin unittest-generator 1.0.0-SNAPSHOT /Users/dreamyao/Documents/forum/src/main/java/com/zbj/forum/service/impl/UserServiceImpl.java ${user.dir}/src/test ``` ### 注意事项 使⽤插件⽣成测试⽤例时需要先调整IDEA⼀项设置 ![image-20211019154551021](http://dreamyao.oss-cn-chengdu.aliyuncs.com/2021-10-19-074557.png) ⽣成测试⽤例的插件需要解析项⽬源码,⽆法解析到 **import \*** 具体导⼊了什么,如果在⽣成时插件提示有 **import \*** 时请先按照上图的⽅式设置**IDEA** 然后在删除 **import \*** 后从新导⼊ ### maven插件中的字段说明 - applicationName 当前需要测试的model中的SpringBoot启动类,如果为spring项目此行可以注释掉,生成好测试代码后手动在测试代码上添加 @ContextConfiguration(locations = {"/application.xml"}) /application.xml需替换为各种项目对应的spring启动文件 - classDirectory 被测类的物理地址,只能到包路径不能到具体哪个类 - classPath 具体某个被测试类的文件地址 - testDirectory 需要生成测试代码到哪个物理地址,默认地址为 ${user.dir}/src/test 不需要改动 **==注意==**:classDirectory 和 testDirectory 只能选择一个 示例: #### 运行Maven插件生成测试用例 image-20211019142808205 双击maven插件运行,就可以生成对应测试用例了 #### 被测试类内容示例: ```java @Service public class UserServiceImpl extends Observable implements IUserService { @Autowired private UserMapper userMapper; /** * 用户注册 * * @param user */ @Override public void save(User user) { User queryUser = null; try { queryUser = userMapper.getUserByUserName(user.getUserName()); } catch (NullPointerException e) { e.printStackTrace(); } if (queryUser != null) { throw new CRUDException(ExceptionCode.DATA_CONVERT_ERROR, "此用户名已被注册!"); } user.setLastVisit(new Date()); user.setLastIp(UserContext.getRequest().getRemoteAddr()); userMapper.register(user); } /** * 用户删除 * * @param id */ @Override public void delete(Integer id) { User user = this.get(id); if (user != null) { userMapper.delete(id); } else { throw new CRUDException(ExceptionCode.HAVE_NOT_DATA, "删除的用户不存在"); } } /** * 根据用户ID获取用户信息 * * @param id t_user表主键Id * @return */ @Override public User get(Integer id) throws NullPointerException { User user = userMapper.get(id); return user; } @Override public PageList findPage(BaseQuery baseQuery) { return null; } @Override public List getAll() { return null; } /** * 更新用户信息(包括密码修好、用户锁定、用户解锁) * * @param user */ @Override public void update(User user) { String userName = user.getUserName(); User queryUser = userMapper.getUserByUserName(userName); if (queryUser == null) { throw new CRUDException(ExceptionCode.HAVE_NOT_DATA, "更新的用户不存在"); } userMapper.update(user); } /** * 根据用户名获取用户信息 * * @param userName * @return */ @Override public User getUserByUserName(String userName) { User user = userMapper.getUserByUserName(userName); if (user == null) { throw new CRUDException(ExceptionCode.HAVE_NOT_DATA, "用户名不存在"); } return user; } /** * 查询所有用户信息 * * @return */ @Override public List getAllUsers() { List userList = userMapper.getAllUsers(); if (userList != null || userList.size() > 0) { return userList; } else { throw new CRUDException(ExceptionCode.HAVE_NOT_DATA, "数据库中没有用户!"); } } /** * 用户登录 * * @param user */ @Override public User login(User user) { User u = userMapper.login(user); if (u != null) { if (u.getLocked() == 1) { throw new CRUDException(ExceptionCode.USER_IS_LOCKED, "用户已被锁定!"); } // 每登录一次增加5点积分 u.setCredit(u.getCredit() + 5); // 更新访问时间 u.setLastVisit(new Date()); // 更新最后访问的IP地址 u.setLastIp(UserContext.getRequest().getRemoteAddr()); userMapper.updateCredit(u); // 返回增加积分后的登录信息 User queryUser = userMapper.getUserByUserName(user.getUserName()); //通知观察者,把日志写入数据库中 setChanged(); notifyObservers(queryUser); } return u; } /** * 用户更新密码 * * @param user */ @Override public void updatePassword(User user) { String userName = user.getUserName(); String password = user.getPassword(); User queryUser = userMapper.getUserByUserName(userName); if (queryUser == null) { throw new CRUDException(ExceptionCode.HAVE_NOT_DATA, "用户更新密码失败!"); } if (!UserContext.getLoginUser().getUserName().equals(user.getUserName()) && UserContext.getLoginUser().getUserType() != 2) { throw new CRUDException(ExceptionCode.HANDLE_NOT_ALLOWE, "操作不被允许!"); } userMapper.updatePassword(userName, password); } } ``` #### 生成的测试文件结果示例: image-20211019144026175 被测试类的类名会作为测试类和excel测试数据存放的包名,被测试类中的没个方法会对应生成一个测试类及excel文件 #### 生成好的测试类示例: ```java public class TestUpdate extends BaseTestCase { @Override public void beforeTest(Map context) { } /** * moke录制标签,加入了之后,会自动录制mybatis执行情况,之后可以改为replay模式会重放 * @Mode(TestMethodMode.REC) * @AutoAssert( checkMode = CheckMode.REC)注解实现智能化断言录制 * @AutoAssert( checkMode = CheckMode.REPLAY)注解实现智能化断言回放 * @AutoTest( autoTestMode = AutoTestMode.XXXXX)注解实现自动化测试 * @Test( dataProvider = Data.SINGLE(测试用例串行执行),Data.PARALLEL(测试用例并行执行)) */ @Test(dataProvider = Data.SINGLE,enabled = false) public void testCase(Map context, Object result) { } } ``` #### 生成好的excel文件内容示例: | caseName | XXX测试(用例标题) | | ------------ | ------------------- | | assertResult | Y | | autotest | N | | id | | | userName | | | password | | | userType | | | locked | | | credit | | | lastVisit | | | lastIp | | 被测试类UserServiceImpl update方法的入参是User对象,User对象内容如下所示: ```java public class User implements Serializable { private static final long serialVersionUID = -1118105041735759174L; /** * 用户ID */ private Integer id; /** * 用户名 */ private String userName; /** * 密码 */ private String password; /** * 1:普通用户 2:管理员 */ private Integer userType; /** * 0:未锁定 1锁定 */ private Integer locked; /** * 积分 */ private Integer credit; /** * 最后访问时间 */ private Date lastVisit; /** * 最后访问IP */ private String lastIp; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public Integer getUserType() { return userType; } public void setUserType(Integer userType) { this.userType = userType; } public Integer getLocked() { return locked; } public void setLocked(Integer locked) { this.locked = locked; } public Integer getCredit() { return credit; } public void setCredit(Integer credit) { this.credit = credit; } public Date getLastVisit() { return lastVisit; } public void setLastVisit(Date lastVisit) { this.lastVisit = lastVisit; } public String getLastIp() { return lastIp; } public void setLastIp(String lastIp) { this.lastIp = lastIp; } } ``` 和生产的excel文件中的入参字段是刚好对应的上的 ##### 必填字段说明 - caseName 用例名称:用例编写人员可以根据用例设计的用途来进行命名 - assertResult 当此字段的值为 Y 时,说明当前列的用例为正常流程测试用例需要被测方法正确返回的 如:success字段返回true,当此字段值为 N 时说明当前列为异常流程用例 如success字段返回false - autotest 是否启用单测框架自动化用例生成功能,对基本类型的入参字段的值进行自动生成,然后经排列组合产生用例,通常此方式推进用于 assertResult 为 N 的情况,也就是异常流程的测试 ### 非必填字段说明 - testOnly 对应某列用例此字段为 Y 时 此列用例会被执行,其他用例不会执行,通常用于调试时,只需要执行某条用例而不需要运行所以用例时 - threadCount 用于多线程测试,如果值为10,则表明启用10个线程并发执行当前例用例,通常用于测试代码是否是线程安全的 ### Excel中对应值,可选填写关键字 - foreach 对某个字段进行循环赋值,格式 foreach0:100 例如入参于分页参数时,当前页参数可以使用此方式 - 把sql语句作为某字段的值填入Excel 如:格式:sql:dataBaseName:select XXX dataBaseName对应数据库名称 示例: #### Excel中其他关键字用法 **${card} -→** **生成身份证号** **${phone} -→** **生成手机证号** **${email} -→** **生成邮箱号码** **${now()} -→** **生成当前时间** **${now()-1D} -→** **生成当前时间前一天的时间** **${now()-2D} -→** **生成当前时间前两天的时间** **${now()-3D} -→** **生成当前时间前三天的时间** **${now()-1M} -→** **生成当前时间前一月的时间** **${now()-2M} -→** **生成当前时间前二月的时间** **${now()-3M} -→** **生成当前时间前三月的时间** #### 使用示例: | **caseName** | | | ------------ | --------------------- | | idCard | **${card}** | | phone | **${phone}** | | email | **${email}** | | taskId | **random:int:5:1000** | | day | **${now()}** | | beforeDay | **${now()-1D}** | | beforeMonth | **${now()-1M}** | #### 生成结果: ```json { "phone": "13400323004", "idCard": "610404194207112923", "beforeDay": "2017-10-11 16:00:37", "beforeMonth": "2017-09-12 16:00:37", "day": "2017-10-12 16:00:37", "email": "fm01gjl7m7@gmail.com", "taskId": 297 } ``` #### 产生随机数据 1、产生int数据:对应需要产生int数据字段的值填写 random:int:1:10 其中1:10表示产生数据的长度范围 2、产生String数据:对应需要产生int数据字段的值填写 random:String:1:10 其中1:10表示产生数据的长度范围 3、产生long数据:对应需要产生int数据字段的值填写 random:long:1:10 其中1:10表示产生数据的长度范围 4、产生float数据:对应需要产生int数据字段的值填写 random:float:1:10 其中1:10表示产生数据的长度范围 5、产生double数据:对应需要产生int数据字段的值填写 random:double:1:10 其中1:10表示产生数据的长度范围 ## ⽤例**Java**类设计示例 ```java @Transactional // 添加测试用例执行完成后测试数据回滚功能 @ContextConfiguration(locations = {"/applicationContext.xml"}) // 当前项目spring的启动xml文件 public class TestUpdate extends BaseTestCase { @Override public void beforeTest(Map context) { // Map context context中的数据为excel中解析出来的数据,且还未执行测试调用,key对应的是excel第一列标题列 // value 对应的是excel填写的值 // excel中第二列数据的 CASE_INDEX 编号为 1 第三列数据的 CASE_INDEX 编号为 2 第一列数据最为后续所有列数据的标题列 if ("1".equals(context.get(Constants.CASE_INDEX).toString())) { // 当为excel中的第⼀列⼊参数据时 // 对 HttpServletRequest 的 getAttribute ⽅法进⾏mock MockUtils.mockProxy(request, "getAttribute", "pkucestestkey", "accessKey"); } else if ("2".equals(context.get(Constants.CASE_INDEX).toString())) { // 当为excel中的第⼆列⼊参数据时 } // 在beforeTest方法中对 context的内容可以进行任何操作,如删除某个值,增加某个值,修改某个值 // 如:可以从数据库、文件、其他接口调用返回的结果中获取值,并放入到context中给当前这个接口作为入参使用 } /** * moke录制标签,加入了之后,会自动录制mybatis执行情况,之后可以改为replay模式会重放 * @Mode(TestMethodMode.REC) * @AutoAssert( checkMode = CheckMode.REC)注解实现智能化断言录制 * @AutoAssert( checkMode = CheckMode.REPLAY)注解实现智能化断言回放 * @AutoTest( autoTestMode = AutoTestMode.XXXXX)注解实现自动化测试 * @Test( dataProvider = Data.SINGLE(测试用例串行执行),Data.PARALLEL(测试用例并行执行)) */ @Test(dataProvider = Data.SINGLE,enabled = true) public void testCase(Map context, Object result) { // Map context 这里的context和 beforeTest方法中的context是有些区别的,除了包含beforeTest方法中context的所有内容之外 // 框架在执行过程中产生的很多中间数据会被放入 testCase 方法的context中 // Object result 为 我们调用接口的返回结果对象 当前这个测试类,测试的是 UserServiceImpl 的update方法,这个方法无返回值 // 这里对应的 result就没有返回值 if ("1".equals(context.get(Constants.CASE_INDEX).toString())) { // 执行第一个条测试用户的返回结果断言 } else if ("2".equals(context.get(Constants.CASE_INDEX).toString())) { // 执行第二个条测试用户的返回结果断言 } } } ``` ### 数据初始化使⽤说明 当在执⾏测试之前想对数据库进⾏测试前的测试数据准备时,可以在测试类中注⼊对应的mapper,然后在beforeTest⽅法中使⽤对应的mapper为数据库初始化对应的测试数据。 ### 断⾔说明 断⾔⽅式可以有两种选择⽅式,⼀种是在Java类中的testCase⽅法中写对应的断⾔代码,如上图所示,另⼀种是在对应的测试⽤例的excel中填写断⾔,excel中有⼀个名称为exceptResult的sheet 简单的断言可以再excel中进行断言,本人更推进在测试类的testCase方法中进行详细的断言编写 #### 返回结果对象Json字符串如下 ```json { "code":"1000", "data":{ "result":[ { "symbol":"BTC", "quantity":"xxxx", "price":"1000" } ], "timestamp":0 }, "msg":"success" } ``` #### excel中的断⾔填写示例如下 image-20211019153320896 #### 数据库断⾔说明 数据库断⾔和数据库初始化使⽤⽅式类似,在对应的测试类中注⼊对应的mapper,然后在testCase⽅法中调⽤对应的mapper进⾏数据库的操作然后进⾏数据库断⾔ ### 测试类中的**beforeTest(Map context)**使⽤说明 此⽅法中的context参数中的内容就excel中每⼀列的内容,在真正调⽤被测试⽅法之前会先执⾏beforeTest⽅法,故可以在beforeTest⽅法中任意对context中的数据进⾏处理,⽐如添加、删除、或者执⾏数据库查询后把查询的结果放⼊context中,等等操作 ps:只有想不到没有做不到 ## Mock工具 ### **SpringBean**、普通类**Mock** ```java @Transactional // 添加测试用例执行完成后测试数据回滚功能 @ContextConfiguration(locations = {"/applicationContext.xml"}) // 当前项目spring的启动xml文件 public class TestDelete extends BaseTestCase { @Autowired private ITopicService topicService; @Override public void beforeTest(Map context) { // mock Bean MockUtils.mock(topicService, "findOneTopicById", "mock返回值", "mock⼊参"); } /** * moke录制标签,加入了之后,会自动录制mybatis执行情况,之后可以改为replay模式会重放 * @Mode(TestMethodMode.REC) * @AutoAssert( checkMode = CheckMode.REC)注解实现智能化断言录制 * @AutoAssert( checkMode = CheckMode.REPLAY)注解实现智能化断言回放 * @AutoTest( autoTestMode = AutoTestMode.XXXXX)注解实现自动化测试 * @Test( dataProvider = Data.SINGLE(测试用例串行执行),Data.PARALLEL(测试用例并行执行)) */ @Test(dataProvider = Data.SINGLE,enabled = true) public void testCase(Map context, Object result) { } } ``` ### 动态代理类、接⼝**Mock**,如**mybatis**的**mapper** ```java @Transactional // 添加测试用例执行完成后测试数据回滚功能 @ContextConfiguration(locations = {"/applicationContext.xml"}) // 当前项目spring的启动xml文件 public class TestDelete extends BaseTestCase { @Capturing private HttpSession session; @Autowired private UserMapper userMapper; @Override public void beforeTest(Map context) { // 动态代理类、接⼝Mock,如mybatis的mapper MockUtils.mock(session, "getAttribute", 1602578701956947969L, "userNo"); MockUtils.mock(userMapper, "getUserByUserName", "mock返回值", "mock⼊参"); } /** * moke录制标签,加入了之后,会自动录制mybatis执行情况,之后可以改为replay模式会重放 * @Mode(TestMethodMode.REC) * @AutoAssert( checkMode = CheckMode.REC)注解实现智能化断言录制 * @AutoAssert( checkMode = CheckMode.REPLAY)注解实现智能化断言回放 * @AutoTest( autoTestMode = AutoTestMode.XXXXX)注解实现自动化测试 * @Test( dataProvider = Data.SINGLE(测试用例串行执行),Data.PARALLEL(测试用例并行执行)) */ @Test(dataProvider = Data.SINGLE,enabled = true) public void testCase(Map context, Object result) { } } ``` ### ⼯具类、⽆法实列化的类、静态⽅法**Mock** ```java @Transactional // 添加测试用例执行完成后测试数据回滚功能 @ContextConfiguration(locations = {"/applicationContext.xml"}) // 当前项目spring的启动xml文件 public class TestDelete extends BaseTestCase { @Override public void beforeTest(Map context) { // mock⼯具类 MockUtils.mock(CheckDataUtil.class, "updateUserCheck", "mock返回值", "mock⼊ 参"); } /** * moke录制标签,加入了之后,会自动录制mybatis执行情况,之后可以改为replay模式会重放 * @Mode(TestMethodMode.REC) * @AutoAssert( checkMode = CheckMode.REC)注解实现智能化断言录制 * @AutoAssert( checkMode = CheckMode.REPLAY)注解实现智能化断言回放 * @AutoTest( autoTestMode = AutoTestMode.XXXXX)注解实现自动化测试 * @Test( dataProvider = Data.SINGLE(测试用例串行执行),Data.PARALLEL(测试用例并行执行)) */ @Test(dataProvider = Data.SINGLE,enabled = false) public void testCase(Map context, Object result) { } } ``` ## **Mybatis**调⽤执⾏录制与回放功能 要实现mybatis调⽤的录制与回放功能⾸先需要配置mybatis插件,配置⽅式如下: ```xml ``` 然后需要在测试类的testCase⽅法上加上 @Mode(TestMethodMode.REC) 添加上录制注解后执⾏⼀下测试⽤例,执⾏完后就会把mybatis调⽤结果录制下来,然后在把注解值改为:@Mode(TestMethodMode.REPLAY) 下次在执⾏测试⽤例时就不会真正去调⽤数据库了。 # dubbo接⼝测试使⽤说明 TODO