开发者社区官方技术圈

阿里云开发者社区官方技术圈,用户产品功能发布、用户反馈收集等。

阿里云开发者社区官方技术圈,用户产品功能发布、用户反馈收集等。
  1. 来源于自己组装 直接利用 JSON 编辑器或者纯文本编辑器,自己一个字段一个字段地编写 JSON 资 源数据。 注意: 这种方式容易出现 JSON 格式错误及字符串转义问题。

  2. 来源于代码生成 做为程序员,能够用程序生成 JSON 资源数据,就绝不手工组装 JSON 资源数据。下 面,便是利用 Fastjson 的 JSON.toJSONString 方法生成 JSON 资源数据。 执行该程序后,生成的 JSON 资源数据如下: 注意: 这种方式能够避免 JSON 格式错误及字符串转义问题。

  3. 来源于线上日志 如果是事后补充单元测试,首先想到的就是利用线上日志。比如: 从上面的日志中,我们可以得到方法 userDAO.queryByCompanyId 的请求参数 companyId 取值为“1”,返回结果为: “[{“id”:1,”name”:”Changyi”,”title”:”Java Developer”…},{“id”:2,”name”:”Tester”,”title”:”Java Tester”…},…]” 注意: 要想得到现成的 JSON 资源数据,就必须输出完整的 JSON 数据内容。但是,由于 JSON 数据内容过大,一般不建议全部输出。所以,从线上日志中也不一定能够拿到 现成的 JSON 资源数据。

  4. 来源于集成测试 集成测试,就是把整个或部分项目环境运行起来,能够连接数据库、Redis、MetaQ、 HSF 等所依赖的第三方服务环境,然后测试某一个方法的功能是否能够达到预期。 执行上面集成测试用例,输出的日志内容如下: 上面日志中,userList 后面的就是我们需要的 JSON 资源数据。 我们也可以用集成测试得到方法内部的方法调用的参数值和返回值,具体方法如下: • 首先,在源代码中添加日志输出语句; • 然后,执行单元测试用例,得到对应的方法调用参数值和返回值; • 最后,删除源代码中日志输出语句,恢复源代码为原来的样子。

  5. 来源于测试过程 有一些数据,是由被测方法生成的,比如:方法返回值和调用参数。针对这类数据, 可以在测试过程中生成,然后逐一进行数据核对,最后整理成 JSON 资源文件。 被测方法: 测试用例: 执行单元测试后,提示以下问题: 上面的错误信息中,后面括号中的就是我们需要需要的 JSON 资源数据。 注意: 一定要进行数据核对,这有可能是错误代码生成的错误数据。用错误数据去验证生 成它的代码,当然不会测试出其中的问题。

以上内容摘自《Java工程师必读手册》电子书,点击https://developer.aliyun.com/ebook/download/7780 可下载完整版

游客c3gxxcx6cqeyo 评论 0
  1. 测试类命名 按照行业惯例,测试类的命名应以被测试类名开头并以 Test 结尾。比如:UserService (用户服务类)的测试类需要命名为 UserServiceTest(用户服务测试类)。 单元测试类应该放在被测试类的同一工程的“src/test/java”目录下,并且要放在 被测试类的同一包下。注意,单元测试类不允许写在业务代码目录下,否则在编译 时没法过滤这些测试用例。

  2. 测试方法命名 按照行业规范,测试方法命名应以 test 开头并以被测试方法结尾。比如:batchCreate (批量创建)的测试方法需要命名为 testBatchCreate(测试:批量创建), queryByCompanyId ( 根据公司 标 识 查 询 ) 的测试方法需要 命 名 为 testQueryByCompanyId(测试:根据公司标识查询)。 当一个方法对应多个测试用例时,就需要创建多个测试方法,原有测试方法命名已 经不能满足需求了。有人建议在原有的测试方法命名的基础上,添加 123 等序号表 示不同的用例。比如:testBatchCreate1(测试:批量创建 1)、testBatchCreate2 (测试:批量创建 2)……但是,这种方法不能明确每个单元测试的用意。 这里,作者建议在原有的测试方法命名的基础上,添加“With+条件”来表达不同的 测试用例方法。

1) 按照结果命名 • testBatchCreateWithSuccess(测试:批量创建-成功); • testBatchCreateWithFailure(测试:批量创建-失败); • testBatchCreateWithException(测试:批量创建-异常);

2) 按照参数命名 • testBatchCreateWithListNull(测试:批量创建-列表为 NULL); • testBatchCreateWithListEmpty(测试:批量创建-列表为空); • testBatchCreateWithListNotEmpty(测试:批量创建-列表不为空);

3) 按照意图命名 • testBatchCreateWithNormal(测试:批量创建-正常); • testBatchCreateWithGray(测试:批量创建-灰度); • testBatchCreateWithException(测试:批量创建-异常);

当然,还有形成其它的测试方法命名方式,也可以把不同的测试方法命名方式混用, 只要能清楚地表达出这个测试用例的涵义即可。

  1. 测试类资源目录命名 这里,作者建议的资源目录命名方式为——以 test 开头且以被测试类名结尾。比如: UserService(用户服务类)的测试资源目录可以命名为 testUserService。 那么,这个资源目录应该放在哪儿了?作者提供了 2 个选择: • 放在“src/test/java”目录下,跟测试类放在同一目录下——这是作者最喜欢的 方式; • 放在“src/test/resources”目录下,跟测试类放在同一目录下——建议 IDEA 用 户采用这种方式。

  2. 测试方法资源目录命名 在前面的小节中,我们针对测试方法进行了规范命名。这里,我们可以直接拿来使 用——即用测试方法名称来命名测试目录。当然,这些测试方法资源目录应该放在 测试类资源目录下。比如:测试类 UserServiceTest(用户服务测试类)的测试方法 testBatchCreateWithSuccess(测试:批量创建-成功)的测试资源目录就是 testUserService/testBatchCreateWithSuccess。 另外,也可以采用“测试方法名称”+“测试条件名称”二级目录的命名方式。比如: 测试类UserServiceTest(用户服务测试类)的测试方法testBatchCreateWithSuccess ( 测试: 批 量 创 建 - 成 功 ) 的测试 资 源 目 录 就 是 testUserService/testBatchCreate/success。 这里,作者首推的是第一种方式,因为测试方法名称和资源目录名称能够保持一致。

  3. 测试资源文件命名 在被测试代码中,所有参数、变量都已经有了命名。所以,建议优先使用这些参数 和变量的名称,并加后缀“.json”标识文件格式。如果这些资源文件名称冲突,可 以 添加前 缀 以 示 区 分 , 比 如 : userCreateList 的 资 源 文 件 名 称 为 “userCreateList.json”。 另外,在测试用例代码中,把这些测试资源文件加载后,反序列化为对应的数据对 象,这些数据对象的变量名称也应该跟资源文件名称保持一致。

  4. 测试资源文件存储 在测试资源目录和名称定义好之后,就需要存入测试资源文件了。存储方式总结如 下: • 如果是测试类下所有测试用例共用的资源文件,建议存储在测试类资源目录下, 比如:testUserService; • 如果是测试用例独有的资源文件,建议存储在测试方法资源目录下,比如: testUserService/testBatchCreateWithSuccess; • 如果是某一被测方法所有的测试用例共用的资源文件,建议存储在不带任何修 饰的测试方法资源目录下,比如:testUserService/testBatchCreate; • 如果测试类资源目录下只有一个测试方法资源目录,可以去掉这个测试方法资 源目录,把所有资源文件存储在测试类资源目录下。 注意: 这里的资源文件不光是 JSON 资源文件,但也可以是其它类型的资源文件。

  5. 文件名称过长 由于资源目录名称较长(大概超过 50 个字符),可能会导致 git 检出代码时出现以 下错误: 或者,在添加文件时出现以下错误: 可以通过以下 git 设置参数解决: 当然,测试用例名称和资源目录名称没必要太长,可以进行一些精简使其小于等于 50 个字符。

  6. JSON 资源文件格式 关于 JSON 资源文件是否格式化的建议:不要格式化 JSON 资源文件内容,否则会占 用更多的代码行数,还会导致无法直接进行文本比较。

以上内容摘自《Java工程师必读手册》电子书,点击https://developer.aliyun.com/ebook/download/7780 可下载完整版

游客c3gxxcx6cqeyo 评论 0

1

回答

1

回答

  1. 序列化对象 利用 JSON.toJSONString 方法序列化对象

  2. 序列化数组 利用 JSON.toJSONString 方法序列化数组

  3. 序列化集合 利用 JSON.toJSONString 方法序列化集合(继承至 Collection,比如 List、Set 等集 合)

  4. 序列化映射 利用 JSON.toJSONString 方法序列化映射: 其中,为了保证每次序列化的映射字符串一致,需要指定序列化参数 MapSortField 进行排序。

  5. 序列化模板对象 利用 JSON.toJSONString 方法序列化模板对象

  6. 序列化指定属性字段 利用 JSON.toJSONString 方法序列化指定属性字段,主要通过设置属性预过滤器 (SimplePropertyPreFilter)的包含属性字段列表(includes)实现。主要应用于只 想验证某些字段的情况,比如只验证跟测试用例有关的字段。 指定所有类的属性字段: 利用 JSON.toJSONString 方法序列化指定所有类的属性字段: 指定单个类的属性字段: 利用 JSON.toJSONString 方法序列化指定单个类的属性字段: 指定多个类的属性字段: 利用 JSON.toJSONString 方法序列化指定多个类的属性字段:

  7. 序列化字段排除属性字段 利用 JSON.toJSONString 方法序列化过滤属性字段,主要通过设置属性预过滤器 (SimplePropertyPreFilter)的排除属性字段列表(excludes)实现。主要应用于不 想验证某些字段的情况,比如排除无法验证的随机属性字段。 排除所有类的属性字段: 利用 JSON.toJSONString 方法序列化排除所有类的属性字段: 排除单个类的属性字段: 利用 JSON.toJSONString 方法序列化排除单个类的属性字段: 排除多个类的属性字段: 利用 JSON.toJSONString 方法序列化排除多个类的属性字段:

  8. 自定义序列化 对应一些类对象,需要序列化为特殊格式文本,就必须自定义序列化器。比如: Geometry 序列化文本,通常采用 WKT(Well-known text)表示,便于用户快速阅 读理解。 全局配置序列化器: 通过 JSON 序列化全局配置指定类序列化器: 注意: 这种方式不支持类继承,必须指定到具体类。比如要序列化 Point 对象,就必须配 置 Point 类的序列化器。 特定配置序列化器: 通过 JSON 序列化特定配置指定类序列化器: 注意: 这种方式不支持类继承,必须指定到具体类。比如要序列化 Point 对象,就必须配 置 Point 类的序列化器。 注解配置序列化器:

通过 JSON 序列化注解配置指定类序列化器: 其中:GeometrySerializer 为自定义类,这里就不贴出具体实现了。

以上内容摘自《Java工程师必读手册》电子书,点击https://developer.aliyun.com/ebook/download/7780 可下载完整版

游客c3gxxcx6cqeyo 评论 0

1

回答

游客lmkkns5ck6auu 2022-10-26 333浏览量 回答数 1
  1. 初始化集合时,尽量指定集合大小 Java 代码高效之道 334 Java 集合初始化时都会指定一个默认大小,当默认大小不再满足数据需求时就会扩 容,每次扩容的时间复杂度有可能是 O(n)。所以,尽量指定预知的集合大小,就 能避免或减少集合的扩容次数。

  2. 不要使用循环拷贝集合,尽量使用 JDK 提供的方法拷贝集合 JDK 提供的方法可以一步指定集合的容量,避免多次扩容浪费时间和空间。同时, 这些方法的底层也是调用 System.arraycopy 方法实现,进行数据的批量拷贝效率 更高。

  3. 尽量使用 Arrays.asList 转化数组为列表 原理与“不要使用循环拷贝集合,尽量使用 JDK 提供的方法拷贝集合”类似。

  4. 直接迭代需要使用的集合 直接迭代需要使用的集合,无需通过其它操作获取数据。

  5. 不要使用 size 方法检测空,必须使用 isEmpty 方法检测空 使用 size 方法来检测空逻辑上没有问题,但使用 isEmpty 方法使得代码更易读,并 且可以获得更好的性能。任何 isEmpty 方法实现的时间复杂度都是 O(1),但是某 些 size 方法实现的时间复杂度有可能是 O(n)。

  6. 非随机访问的 List,尽量使用迭代代替随机访问 对于列表,可分为随机访问和非随机访问两类,可以用是否实现 RandomAccess 接 口判断。随机访问列表,直接通过 get 获取数据不影响效率。而非随机访问列表, 通过 get 获取数据效率极低。

其实,不管列表支不支持随机访问,都应该使用迭代进行遍历。 7. 尽量使用 HashSet 判断值存在 在 Java 集合类库中,List 的 contains 方法普遍时间复杂度是 O(n),而 HashSet 的时间复杂度为 O(1)。如果需要频繁调用 contains 方法查找数据,可以先将 List 转换成 HashSet。

  1. 避免先判断存在再进行获取 如果需要先判断存在再进行获取,可以直接获取并判断空,从而避免了二次查找操 作。

以上内容摘自《Java工程师必读手册》电子书,点击https://developer.aliyun.com/ebook/download/7780 可下载完整版

游客c3gxxcx6cqeyo 评论 0

1

回答

  1. 利用三元表达式 注意:对于包装类型的算术计算,需要注意避免拆包时的空指针问题。

  2. 利用 for-each 语句 从 Java 5 起,提供了 for-each 循环,简化了数组和集合的循环遍历。For-each 循 环允许你无需保持传统 for 循环中的索引就可以遍历数组,或在使用迭代器时无需 在 while 循环中调用 hasNext 方法和 next 方法就可以遍历集合。

  3. 利用 try-with-resource 语句 所有实现 Closeable 接口的“资源”,均可采用 try-with-resource 进行简化。

  4. 利用 return 关键字 利用 return 关键字,可以提前函数返回,避免定义中间变量。

  5. 利用 static 关键字 利用 static 关键字,可以把字段变成静态字段,也可以把函数变为静态函数,调用 时就无需初始化类对象。

  6. 利用 lambda 表达式 Java 8 发布以后,lambda 表达式大量替代匿名内部类的使用,在简化了代码的同 时,更突出了原有匿名内部类中真正有用的那部分代码。

  7. 利用方法引用 方法引用(::),可以简化 lambda 表达式,省略变量声明和函数调用。

  8. 利用静态导入 静态导入(import static),当程序中大量使用同一静态常量和函数时,可以简化静 态常量和函数的引用。 注意:静态引入容易造成代码阅读困难,所以在实际项目中应该警慎使用。

  9. 利用 unchecked 异常 Java 的异常分为两类:Checked 异常和 Unchecked 异常。Unchecked 异常继承了 RuntimeException,特点是代码不需要处理它们也能通过编译,所以它们称作 Unchecked 异常。利用 Unchecked 异常,可以避免不必要的 try-catch 和 throws 异常处理。

以上内容摘自《Java工程师必读手册》电子书,点击https://developer.aliyun.com/ebook/download/7780 可下载完整版

游客c3gxxcx6cqeyo 评论 0
  1. 模板方法模式 模板方法模式(Template Method Pattern)定义一个固定的算法框架,而将算法的 一些步骤放到子类中实现,使得子类可以在不改变算法框架的情况下重定义该算法 的某些步骤。 @Repository public class UserValue { /** 值操作 / @Resource(name = "stringRedisTemplate") private ValueOperations<String, String> valueOperations; /* 值模式 / private static final String KEY_FORMAT = "Value:User:%s"; /* 设置值 / public void set(Long id, UserDO value) { String key = String.format(KEY_FORMAT, id); valueOperations.set(key, JSON.toJSONString(value)); } /* 获取值 / public UserDO get(Long id) { String key = String.format(KEY_FORMAT, id); String value = valueOperations.get(key); return JSON.parseObject(value, UserDO.class); } ... } @Repository public class RoleValue { /* 值操作 / @Resource(name = "stringRedisTemplate") private ValueOperations<String, String> valueOperations; /* 值模式 / private static final String KEY_FORMAT = "Value:Role:%s"; /* 设置值 / public void set(Long id, RoleDO value) { String key = String.format(KEY_FORMAT, id); valueOperations.set(key, JSON.toJSONString(value)); } /* 获取值 */ public RoleDO get(Long id) { String key = String.format(KEY_FORMAT, id); String value = valueOperations.get(key); return JSON.parseObject(value, RoleDO.class); } ... }

public abstract class AbstractDynamicValue<I, V> { /** 值操作 / @Resource(name = "stringRedisTemplate") private ValueOperations<String, String> valueOperations; /* 设置值 / public void set(I id, V value) { valueOperations.set(getKey(id), JSON.toJSONString(value)); } /* 获取值 / public V get(I id) { return JSON.parseObject(valueOperations.get(getKey(id)), getValueClass()); } ... /* 获取主键 / protected abstract String getKey(I id); /* 获取值类 / protected abstract Class getValueClass(); } @Repository public class UserValue extends AbstractValue<Long, UserDO> { / * 获取主键 / @Override protected String getKey(Long id) { return String.format("Value:User:%s", id); } /* 获取值类 / @Override protected Class getValueClass() { return UserDO.class; } } @Repository public class RoleValue extends AbstractValue<Long, RoleDO> { / * 获取主键 / @Override protected String getKey(Long id) { return String.format("Value:Role:%s", id); } /* 获取值类 */ @Override protected Class getValueClass() { return RoleDO.class; } }

  1. 访问者模式 访问者模式(Visitor),封装一些作用于某种数据结构的各元素的操作,它可以在不 改变数据结构的前提下定义作用于这些元素的新的操作。 普通: public interface DataHandler { /** 解析数据 */ public T parseData(Record record);

    /** 存储数据 */ public boolean storeData(List dataList); } public long executeFetch(String tableName, int batchSize, DataHandler dataHandler) throws Exception { // 构建下载会话 DownloadSession session = buildSession(tableName); // 获取数据数量 long recordCount = session.getRecordCount(); if (recordCount == 0) { return 0; } // 进行数据读取 long fetchCount = 0L; try (RecordReader reader = session.openRecordReader(0L, recordCount, true)) { // 依次读取数据 Record record; List dataList = new ArrayList<>(batchSize); while ((record = reader.read()) != null) { // 解析添加数据 T data = dataHandler.parseData(record); if (Objects.nonNull(data)) { dataList.add(data); } // 批量存储数据 if (dataList.size() == batchSize) { boolean isContinue = dataHandler.storeData(dataList); fetchCount += batchSize; dataList.clear(); if (!isContinue) { break; } } } // 存储剩余数据 if (CollectionUtils.isNotEmpty(dataList)) { dataHandler.storeData(dataList); fetchCount += dataList.size(); dataList.clear(); } } // 返回获取数量

    return fetchCount; } /** 使用案例 / long fetchCount = odpsService.executeFetch("user", 5000, new DataHandler() { /* 解析数据 */ @Override public T parseData(Record record) { UserDO user = new UserDO(); user.setId(record.getBigint("id")); user.setName(record.getString("name")); return user; }

    /** 存储数据 */ @Override public boolean storeData(List dataList) { userDAO.batchInsert(dataList); return true; } }); 精简: public long executeFetch(String tableName, int batchSize, Function<Record, T> dataParser, Function<List , Boolean> dataStorage) throws Exception { // 构建下载会话 DownloadSession session = buildSession(tableName); // 获取数据数量 long recordCount = session.getRecordCount(); if (recordCount == 0) { return 0; } // 进行数据读取 long fetchCount = 0L; try (RecordReader reader = session.openRecordReader(0L, recordCount, true)) { // 依次读取数据 Record record; List dataList = new ArrayList<>(batchSize); while ((record = reader.read()) != null) { // 解析添加数据 T data = dataParser.apply(record); if (Objects.nonNull(data)) { dataList.add(data); } // 批量存储数据 if (dataList.size() == batchSize) { Boolean isContinue = dataStorage.apply(dataList); fetchCount += batchSize; dataList.clear(); if (!Boolean.TRUE.equals(isContinue)) { break; } } } // 存储剩余数据 if (CollectionUtils.isNotEmpty(dataList)) { dataStorage.apply(dataList); fetchCount += dataList.size();

    dataList.clear(); } } // 返回获取数量 return fetchCount; } /** 使用案例 */ long fetchCount = odpsService.executeFetch("user", 5000, record -> { UserDO user = new UserDO(); user.setId(record.getBigint("id")); user.setName(record.getString("name")); return user; }, dataList -> { userDAO.batchInsert(dataList); return true; });

普通的访问者模式,实现时需要定义 DataHandler 接口,调用时需要实现
DataHandler 匿名内部类,代码较多较繁琐。而精简后的访问者模式,充分利用了
函数式编程,实现时无需定义接口,直接使用 Function 接口;调用时无需实现匿名
内部类,直接采用 lambda 表达式,代码较少较简洁。
3. 代理模式
Spring 中最重要的代理模式就是 AOP(Aspect-Oriented Programming,面向切面
的编程),是使用 JDK 动态代理和 CGLIB 动态代理技术来实现的。
普通:

精简 1:
基于@ControllerAdvice 的异常处理:
精简 2:
基于 AOP 的异常处理:

以上内容摘自《Java工程师必读手册》电子书,点击https://developer.aliyun.com/ebook/download/7780
可下载完整版
游客c3gxxcx6cqeyo 评论 0
  1. 利用数组简化 对于固定上下限范围的 if-else 语句,可以用数组+循环来简化。

  2. 利用 Map 简化 对于映射关系的 if-else 语句,可以用 Map 来简化。此外,此规则同样适用于简化 映射关系的 switch 语句。 已经把方法简化为一行代码,其实都没有封装方法的必要了。

  3. 利用容器类简化 Java 不像 Python 和 Go,方法不支持返回多个对象。如果需要返回多个对象,就必 须自定义类,或者利用容器类。常见的容器类有 Apache 的 Pair 类和 Triple 类,Pair 类支持返回 2 个对象,Triple 类支持返回 3 个对象。

  4. 利用 ThreadLocal 简化 ThreadLocal 提供了线程专有对象,可以在整个线程生命周期中随时取用,极大地 方便了一些逻辑的实现。用 ThreadLocal 保存线程上下文对象,可以避免不必要的 参数传递。

由于 DateFormat 的 format 方法线程非安全(建议使用替代方法),在线程中频繁 初始化 DateFormat 性能太低,如果考虑重用只能用参数传入 DateFormat。可能你会觉得以下的代码量反而多了,如果调用工具方法的地方比较多,就可以省 下一大堆 DateFormat 初始化和传入参数的代码。 注意: ThreadLocal 有一定的内存泄露的风险,尽量在业务代码结束前调用 remove 方法 进行数据清除。

以上内容摘自《Java工程师必读手册》电子书,点击https://developer.aliyun.com/ebook/download/7780 可下载完整版

游客c3gxxcx6cqeyo 评论 0

1

回答

  1. 利用注解初始化 Mockito 提供@Mock 注解来模拟类实例,提供@Captor 注解来初始化参数捕获器。 由于这些注解实例是通过测试框架进行初始化的,所以不会产生类型转换警告。

  2. 利用临时类或接口 我们无法获取泛型类或接口的 class 实例,但是很容易获取具体类的 class 实例。这 个解决方案的思路是——先定义继承泛型类的具体子类,然后 mock、spy、forClass 以及 any 出这个具体子类的实例,然后把具体子类实例转换为父类泛型实例。

  3. 利用 CastUtils.cast 方法 SpringData 包中提供一个 CastUtils.cast 方法,可以用于类型的强制转换。这个解 决方案的思路是——利用 CastUtils.cast 方法屏蔽类型转换警告。

  4. 利用类型自动转换方法 在 Mockito 中,提供形式如下的方法——泛型类型只跟返回值有关,而跟输入参数 无关。这样的方法,可以根据调用方法的参数类型自动转换,而无需手动强制类型 转换。如果手动强制类型转换,反而会产生类型转换警告。

  5. 利用 doReturn-when 语句代替 when-thenReturn 语句 Mockito 的 when-thenReturn 语句需要对返回类型强制校验,而 doReturn-when 语句不会对返回类型强制校验。利用这个特性,可以利用 doReturn-when 语句代 替 when-thenReturn 语句解决类型转换警告。

  6. 利用 Whitebox.invokeMethod 方法代替 Method.invoke 方法 JDK 提供的 Method.invoke 方法返回的是 Object 类型,转化为具体类型时需要强制 转换,会产生类型转换警告。而 PowerMock 提供的 Whitebox.invokeMethod 方法 返回类型可以自动转化,不会产生类型转换警告。

  7. 利用 instanceof 关键字 在具体类型强制转换时,建议利用 instanceof 关键字先判断类型,否则会产生类型 转换警告。

  8. 利用 Class.cast 方法 在泛型类型强制转换时,会产生类型转换警告。可以采用泛型类的 cast 方法转换, 从而避免产生类型转换警告。

  9. 避免不必要的类型转换 有时候,没有必要进行类型转换,就尽量避免类型转换。比如:把 Object 类型转换 为具体类型,但又把具体类型当 Object 类型使用,就没有必要进行类型转换。像这 种情况,可以合并表达式或定义基类变量,从而避免不必要的类型转化。

以上内容摘自《Java工程师必读手册》电子书,点击https://developer.aliyun.com/ebook/download/7780 可下载完整版

游客c3gxxcx6cqeyo 评论 0

在编写单元测试用例时,或多或少会遇到一些问题,大多数是由于对测试框架特性 不熟悉导致,比如:

• Mockito 不支持对静态方法、构造方法、final 方法、私有方法的模拟; • Mockito 的 any 相关的参数匹配方法并不支持可空参数和空参数; • 如果为 Mock 方法或 Mock 方法参数不匹配时,会返回默认值(基础类型为 0, 对象类型为 null); • 采用 Mockito 的参数匹配方法或 Argument 的 captor 方法时,其它参数不能直 接用常量或变量,必须使用 Mockito 的 eq 方法包装; • 使用 when-then 语句模拟 Spy 对象方法会先执行真实方法,应该使用 do-when 语句; • PowerMock 对静态方法、构造方法、final 方法、私有方法的模拟需要把对应的 类添加到@PrepareForTest 注解中; • PowerMock 模拟 JDK 的静态方法、构造方法、final 方法、私有方法时,需要把 使用这些方法的类加入到@PrepareForTest 注解中,从而导致单元测试覆盖率 不被统计; • PowerMock 使用自定义的类加载器来加载类,可能导致系统类加载器认为有类 型转换问题;需要加上@PowerMockIgnore({"javax.crypto.*"})注解,来告诉 PowerMock 这个包不要用 PowerMock 的类加载器加载,需要采用系统类加载 器来加载; • 如 果 遇 到 Mock 对 象 静 态 常 量 初 始 化 失 败 ,可以 利 用 注 解 @SuppressStaticInitializationFor 抑制静态常量初始化。

以上内容摘自《Java工程师必读手册》电子书,点击https://developer.aliyun.com/ebook/download/7780 可下载完整版

游客c3gxxcx6cqeyo 评论 0

1

回答

Junit 测试框架中 Assert 类就是断言工具类,主要验证单元测试中实际数据对象与 期望数据对象一致。在调用被测方法时,需要对返回值和异常进行验证;在验证方 法调用时,也需要对捕获的参数值进行验证。

  1. 验证数据对象空值 1) 验证数据对象为空 通过 Junit 提供的 Assert.assertNull 方法验证数据对象为空。 2) 验证数据对象非空 通过 Junit 提供的 Assert.assertNotNull 方法验证数据对象非空。

  2. 验证数据对象布尔值 1) 验证数据对象为真 通过 Junit 提供的 Assert.assertTrue 方法验证数据对象为真。 2) 验证数据对象为假 通过 Junit 提供的 Assert.assertFalse 方法验证数据对象为假。

  3. 验证数据对象引用 在单元测试用例中,对于一些参数或返回值对象,不需要验证对象具体取值,只需 要验证对象引用是否一致。 1) 验证数据对象一致 Junit 提供的 Assert.assertSame 方法验证数据对象一致。 2) 验证数据对象不一致 Junit 提供的 Assert.assertNotSame 方法验证数据对象一致。

  4. 验证数据对象值 Junit 提供 Assert.assertEquals、Assert.assertNotEquals、Assert.assertArrayEquals 方法组,可以用来验证数据对象值是否相等。 1) 验证简单数据对象 对于简单数据对象(比如:基础类型、包装类型、实现了 equals 的数据类型……), 可以直接通过 Junit 的 Assert.assertEquals 和 Assert.assertNotEquals 方法组进行 验证。 2) 验证简单数组或集合对象 对于简单数组对象(比如:基础类型、包装类型、实现了 equals 的数据类型……), 可以直接通过 Junit 的 Assert.assertArrayEquals 方法组进行验证。对于简单集合对 象,也可以通过 Assert.assertEquals 方法验证。 3) 验证复杂数据对象 对于复杂的 JavaBean 数据对象,需要验证 JavaBean 数据对象的每一个属性字段。 4) 验证复杂数组或集合对象 对于复杂的 JavaBean 数组和集合对象,需要先展开数组和集合对象中每一个 JavaBean 数据对象,然后验证 JavaBean 数据对象的每一个属性字段。 5) 通过序列化验证数据对象 如上一节例子所示,当数据对象过于复杂时,如果采用 Assert.assertEquals 依次验 证每个 JavaBean 对象、验证每一个属性字段,测试用例的代码量将会非常庞大。 这里,推荐使用序列化手段简化数据对象的验证,比如利用 JSON.toJSONString 方 法把复杂的数据对象转化为字符串,然后再使用 Assert.assertEquals 方法进行验证 字符串。但是,序列化值必须具备有序性、一致性和可读性。 通常使用 JSON.toJSONString 方法把 Map 对象转化为字符串,其中 key-value 的顺 序具有不确定性,无法用于验证两个对象是否一致。这里,JSON 提供序列化选项 SerializerFeature.MapSortField(映射排序字段),可以用于保证序列化后的 key�value 的有序性。 6) 验证数据对象私有属性字段 有时候,单元测试用例需要对复杂对象的私有属性字段进行验证。而 PowerMockito 提供的 Whitebox.getInternalState 方法,获取轻松地获取到私有属性字段值。

  5. 验证异常对象内容 异常作为 Java 语言的重要特性,是 Java 语言健壮性的重要体现。捕获并验证异常 数据内容,也是测试用例的一种。 1) 通过@Test 注解验证异常对象 Junit 的注解@Test 提供了一个 expected 属性,可以指定一个期望的异常类型,用 来捕获并验证异常。但是,这种方式只能验证异常类型,并不能验证异常原因和消 息。 2) 通过@Rule 注解验证异常对象 如果想要验证异常原因和消息,就需求采用@Rule 注解定义 ExpectedException 对 象,然后在测试方法的前面声明要捕获的异常类型、原因和消息。 3) 通过 Assert.assertThrows 验证异常对象 在最新版的 JUnit 中,提供了一个更为简洁的异常验证方式——Assert.assertThrows 方法。

以上内容摘自《Java工程师必读手册》电子书,点击https://developer.aliyun.com/ebook/download/7780 可下载完整版

游客c3gxxcx6cqeyo 评论 0

1

回答

在单元测试中,验证是确认模拟的依赖方法是否按照预期被调用或未调用的过程。 Mockito 提供了许多方法来验证依赖方法调用,给我们编写单元测试用例带来了很 大的帮助。

  1. 根据参数验证方法调用 1) 验证无参数方法调用 2) 验证指定参数方法调用 3) 验证任意参数方法调用 4) 验证可空参数方法调用 5) 验证必空参数方法调用 6) 验证可变参数方法调用 对于一些变长度参数方法,可以按实际参数个数进行验证: 也可以用 Mockito.any()进行通用验证:

  2. 验证方法调用次数 1) 验证方法默认调用 1 次 2) 验证方法从不调用 3) 验证方法调用 n 次 4) 验证方法调用至少 1 次 5) 验证方法调用至少 n 次 6) 验证方法调用最多 1 次 7) 验证方法调用最多 n 次 8) 验证方法调用指定 n 次 Mockito 允许按顺序进行验证方法调用,未被验证到的方法调用将不会被标记为已 验证。 9) 验证对象及其方法调用 1 次 用于验证对象及其方法调用 1 次,如果该对象还有别的方法被调用或者该方法调用 了多次,都将导致验证方法调用失败。

  3. 验证方法调用并捕获参数值 Mockito 提供 ArgumentCaptor 类来捕获参数值,通过调用 forClass(Class clazz) 方法来构建一个 ArgumentCaptor 对象,然后在验证方法调用时来捕获参数,最后 获取到捕获的参数值并验证。如果一个方法有多个参数都要捕获并验证,那就需要 创建多个 ArgumentCaptor 对象。

以上内容摘自《Java工程师必读手册》电子书,点击https://developer.aliyun.com/ebook/download/7780 可下载完整版

游客c3gxxcx6cqeyo 评论 0
  1. 定义对象阶段 第 1 步是定义对象阶段,主要包括定义被测对象、模拟依赖对象(类成员)、注入 依赖对象(类成员)3 大部分。 1) 定义被测对象 在编写单元测试时,首先需要定义被测对象,或直接初始化、或通过 Spy 包装…… 其实,就是把被测试服务类进行实例化。 2) 模拟依赖对象(类成员) 在一个服务类中,我们定义了一些类成员对象——服务(Service)、数据访问对象 (DAO)、参数(Value)等。在 Spring 框架中,这些类成员对象通过@Autowired、 @Value 等方式注入,它们可能涉及复杂的环境配置、依赖第三方接口服务……但是, 在单元测试中,为了解除对这些类成员对象的依赖,我们需要对这些类成员对象进 行模拟。 3) 注入依赖对象(类成员) 当模拟完这些类成员对象后,我们需要把这些类成员对象注入到被测试类的实例中。 以便在调用被测试方法时,可能使用这些类成员对象,而不至于抛出空指针异常。

  2. 模拟方法阶段 第 2 步是模拟方法阶段,主要包括模拟依赖对象(参数或返回值)、模拟依赖方法 2 大部分。 1) 模拟依赖对象(参数或返回值) 通常,在调用一个方法时,需要先指定方法的参数,然后获取到方法的返回值。所 以,在模拟方法之前,需要先模拟该方法的参数和返回值。 2) 模拟依赖方法 在模拟完依赖的参数和返回值后,就可以利用 Mockito 和 PowerMock 的功能,进 行依赖方法的模拟。如果依赖对象还有方法调用,还需要模拟这些依赖对象的方法。

  3. 调用方法阶段 第 3 步是调用方法阶段,主要包括模拟依赖对象(参数)、调用被测方法、验证参 数对象(返回值)3 步。 1) 模拟依赖对象(参数) 在调用被测方法之前,需要模拟被测方法的参数。如果这些参数还有方法调用,还 需要模拟这些参数的方法。 2) 调用被测方法 在准备好参数对象后,就可以调用被测试方法了。如果被测试方法有返回值,需要 定义变量接收返回值;如果被测试方法要抛出异常,需要指定期望的异常。 3) 验证数据对象(返回值) 在调用被测试方法后,如果被测试方法有返回值,需要验证这个返回值是否符合预 期;如果被测试方法要抛出异常,需要验证这个异常是否满足要求。

  4. 验证方法阶段 第 4 步是验证方法阶段,主要包括验证依赖方法、验证数据对象(参数)、验证依 赖对象 3 步。 1) 验证依赖方法 作为一个完整的测试用例,需要对每一个模拟的依赖方法调用进行验证。 2) 验证数据对象(参数) 对应一些模拟的依赖方法,有些参数对象是被测试方法内部生成的。为了验证代码 逻辑的正确性,就需要对这些参数对象进行验证,看这些参数对象值是否符合预期。 3) 验证依赖对象 作为一个完整的测试用例,应该保证每一个模拟的依赖方法调用都进行了验证。正 好,Mockito 提供了一套方法,用于验证模拟对象所有方法调用都得到了验证。

以上内容摘自《Java工程师必读手册》电子书,点击https://developer.aliyun.com/ebook/download/7780 可下载完整版

游客c3gxxcx6cqeyo 评论 0

公告

阿里云开发者社区官方技术圈,用户产品功能发布、用户反馈收集等。

展开