• 单元测试实战(五)普通类的测试


    为鼓励单元测试,特分门别类示例各种组件的测试代码并进行解说,供开发人员参考。

    本文中的测试均基于JUnit5

    单元测试实战(一)Controller 的测试

    单元测试实战(二)Service 的测试    

    单元测试实战(三)JPA 的测试    

    单元测试实战(四)MyBatis-Plus 的测试

    ​​​​​​​单元测试实战(五)普通类的测试

    单元测试实战(六)其它

    概述

    普通类或曰POJO的测试,是最简单的一种情况,大多数情况下只使用JUnit即可。万一有不易实例化的外部依赖,也可以用Mockito的@Mock来模拟。这类测试一般应脱离Spring上下文来进行。

    需要的话,在每个测试之前应清理/重置测试数据,一般为方法参数或待测类实例;断言应主要检查待测类的行为是否符合预期。

    依赖

    大多数普通类测试只依赖JUnit,但作为一般实践,我们通常也带上Spring Boot自己的测试工具集。

    1. <dependency>
    2. <groupId>org.springframework.bootgroupId>
    3. <artifactId>spring-boot-starter-testartifactId>
    4. <scope>testscope>
    5. dependency>
    6. <dependency>
    7. <groupId>org.junit.jupitergroupId>
    8. <artifactId>junit-jupiter-apiartifactId>
    9. <scope>testscope>
    10. dependency>

    示例

    以下是一个BigDecimal的包装类,实现了一些BigDecimal的最佳实践;该类是我们的待测试类:

    1. package com.aaa.sdk.utils;
    2. import java.math.BigDecimal;
    3. import java.math.RoundingMode;
    4. import java.text.DecimalFormat;
    5. import java.util.Objects;
    6. /**
    7. * BigDecimal的包装类,封装了以下实践:
    8. *
    9. 不允许null值(使用工厂方法创建实例时会直接报错)。
  • *
  • 避免double构造传参造成精度丢失。
  • *
  • 封装equals,强制使用compareTo来比较。
  • *
  • 封装格式化输出(补零对齐,默认两位)。
  • *
  • 封装加减乘除,尤其除法,强制使用BigDecimal的三参方法避免无限小数报错。
  • *
  • 直观的判断正数、负数、零的方法。
  • *
  • *
  • * 本类设计为不可变对象,非读方法(加减乘除、取反、四舍五入等)都会产生新对象。
  • *
  • * @author ioriogami
  • *
  • */
  • public class DecimalWrapper implements Comparable {
  • /**
  • * 默认精度,保留两位。
  • */
  • public static final int DEFAULT_SCALE = 2;
  • private final BigDecimal decimal;
  • private DecimalWrapper(BigDecimal decimal) {
  • Objects.requireNonNull(decimal);
  • this.decimal = decimal;
  • }
  • // 以下工厂方法
  • /**
  • * 通过一个BigDecimal对象构造DecimalWrapper实例。
  • * @param decimal 本wrapper代表的BigDecimal对象
  • */
  • public static DecimalWrapper of(BigDecimal decimal) {
  • return new DecimalWrapper(decimal);
  • }
  • /**
  • * 通过一个String实例构造DecimalWrapper实例。
  • * @param s 字符串数值
  • */
  • public static DecimalWrapper of(String s) {
  • return of(new BigDecimal(s));
  • }
  • /**
  • * 通过一个double实例构造DecimalWrapper实例。
  • * @param d double数值
  • */
  • public static DecimalWrapper of(double d) {
  • return of(new BigDecimal(String.valueOf(d)));
  • }
  • /**
  • * 通过一个float实例构造。
  • * @param f float数值
  • */
  • public static DecimalWrapper of(float f) {
  • return of(new BigDecimal(String.valueOf(f)));
  • }
  • // 以下一般接口
  • /**
  • * 获取底层的BigDecimal对象
  • */
  • public BigDecimal toBigDecimal() {
  • return decimal;
  • }
  • /**
  • * 获取double值。
  • */
  • public Double toDouble() {
  • return decimal.doubleValue();
  • }
  • @Override
  • public String toString() {
  • return decimal.toPlainString();
  • }
  • @Override
  • public int hashCode() {
  • return decimal.hashCode();
  • }
  • @Override
  • public boolean equals(Object obj) {
  • if (obj instanceof DecimalWrapper) {
  • DecimalWrapper other = (DecimalWrapper) obj;
  • return decimal.compareTo(other.decimal) == 0;
  • }
  • return false;
  • }
  • @Override
  • public int compareTo(DecimalWrapper that) {
  • if (that == null) return 1;
  • return this.decimal.compareTo(that.decimal);
  • }
  • // 以下格式化输出
  • /**
  • * 四舍五入保留两位。
  • * @return 一个新的DecimalWrapper对象
  • */
  • public DecimalWrapper round() {
  • return round(DEFAULT_SCALE, RoundingMode.HALF_UP);
  • }
  • /**
  • * 四舍五入保留i位。
  • * @param scale 精度,即i,保留几位。
  • * @return 一个新的DecimalWrapper对象
  • */
  • public DecimalWrapper round(int scale) {
  • return round(scale, RoundingMode.HALF_UP);
  • }
  • /**
  • * m舍n入保留i位。
  • *
  • * @param scale 精度,即i,保留几位。
  • * @param mode 舍入策略(m和n)。
  • * @return 一个新的DecimalWrapper对象
  • */
  • public DecimalWrapper round(int scale, RoundingMode mode) {
  • return of(decimal.setScale(scale, mode));
  • }
  • /**
  • * 获得四舍五入保留两位(强制补0)后的字符串。
  • */
  • public String format() {
  • return new DecimalFormat("0.00").format(
  • decimal.setScale(DEFAULT_SCALE, RoundingMode.HALF_UP));
  • }
  • /**
  • * 获得四舍五入保留i位(强制补0)后的字符串。
  • * @param scale 精度,即i,保留几位。
  • */
  • public String format(int scale) {
  • return format(scale, RoundingMode.HALF_UP);
  • }
  • /**
  • * 获得m舍n入保留scale位(强制补0)后的字符串。不会影响本身的值。
  • * @param scale 精度,即i,保留几位。
  • * @param mode 舍入策略(m和n),若为null则默认四舍五入。
  • * @return 格式化后的字符串。
  • */
  • public String format(int scale, RoundingMode mode) {
  • if (scale <= 0) throw new IllegalArgumentException("精度必须大于0");
  • if (mode == null) mode = RoundingMode.HALF_UP;
  • StringBuilder buff = new StringBuilder("0.");
  • for (int i = 0; i < scale; i++) {
  • buff.append("0");
  • }
  • return new DecimalFormat(buff.toString()).format(decimal.setScale(scale, mode));
  • }
  • // 以下加减乘除、取反
  • /**
  • * 加法。
  • */
  • public DecimalWrapper add(DecimalWrapper other) {
  • if (other == null) throw new IllegalArgumentException("操作数为null,无法进行加法运算。");
  • return of(decimal.add(other.decimal));
  • }
  • /**
  • * 减法。
  • */
  • public DecimalWrapper subtract(DecimalWrapper other) {
  • if (other == null) throw new IllegalArgumentException("操作数为null,无法进行减法运算。");
  • return of(decimal.subtract(other.decimal));
  • }
  • /**
  • * 乘法。
  • */
  • public DecimalWrapper multiply(DecimalWrapper other) {
  • if (other == null) throw new IllegalArgumentException("操作数为null,无法进行乘法运算。");
  • return of(decimal.multiply(other.decimal));
  • }
  • /**
  • * 除法。
  • */
  • public DecimalWrapper divide(DecimalWrapper other) { // 使用三参除法,避免结果为无限小数时报错。
  • return divide(other, DEFAULT_SCALE, RoundingMode.HALF_UP);
  • }
  • /**
  • * 除法,指定精度和舍入策略。
  • */
  • public DecimalWrapper divide(DecimalWrapper other, int scale, RoundingMode mode) {
  • if (other == null) throw new IllegalArgumentException("操作数为null,无法进行除法运算。");
  • if (scale <= 0) throw new IllegalArgumentException("精度必须大于0");
  • if (mode == null) mode = RoundingMode.HALF_UP;
  • return of(decimal.divide(other.decimal, scale, mode));
  • }
  • /**
  • * 取反。
  • */
  • public DecimalWrapper negate() {
  • return of(decimal.negate());
  • }
  • /**
  • * 判断是否零值,不管到底是0.0、0.00还是0.0000..
  • */
  • public boolean isZero() {
  • return decimal.signum() == 0;
  • }
  • /**
  • * 判断是否正数。
  • */
  • public boolean isPositive() {
  • return decimal.signum() == 1;
  • }
  • /**
  • * 判断是否正数。
  • */
  • public boolean isNegative() {
  • return decimal.signum() == -1;
  • }
  • }
  • 以下是对DecimalWrapper进行测试的测试类:

    1. package com.aaa.sdk.utils;
    2. import java.math.BigDecimal;
    3. import java.math.RoundingMode;
    4. import org.junit.jupiter.api.Test;
    5. import static org.junit.jupiter.api.Assertions.*;
    6. class DecimalWrapperTest {
    7. @Test
    8. void testConstructBigDecimal() {
    9. DecimalWrapper d = DecimalWrapper.of(new BigDecimal("1.11"));
    10. assertEquals("1.11", d.format());
    11. }
    12. @Test
    13. void testConstructDouble() {
    14. DecimalWrapper d = DecimalWrapper.of(1.11);
    15. assertEquals("1.11", d.format());
    16. }
    17. @Test
    18. void testConstructString() {
    19. DecimalWrapper d = DecimalWrapper.of("1.11");
    20. assertEquals("1.11", d.format());
    21. }
    22. @Test
    23. void testConstructFloat() {
    24. DecimalWrapper d = DecimalWrapper.of(1.1f);
    25. assertEquals("1.10", d.format());
    26. }
    27. @Test
    28. void testConstructNullParam() {
    29. try {
    30. DecimalWrapper.of((String) null);
    31. fail("should not get here!");
    32. } catch (NullPointerException npe) {}
    33. try {
    34. DecimalWrapper.of((Double) null);
    35. fail("Should not get here!");
    36. } catch (NullPointerException npe) {}
    37. try {
    38. DecimalWrapper.of((Float) null);
    39. fail("Should not get here!");
    40. } catch (NullPointerException npe) {}
    41. try {
    42. DecimalWrapper.of((BigDecimal) null);
    43. fail("Should not get here!");
    44. } catch (NullPointerException npe) {}
    45. }
    46. @Test
    47. void testComparison() {
    48. DecimalWrapper d1 = DecimalWrapper.of(1.1);
    49. DecimalWrapper d2 = DecimalWrapper.of(1.2);
    50. DecimalWrapper d3 = DecimalWrapper.of(1.0);
    51. assertTrue(d1.compareTo(d2) < 0);
    52. assertTrue(d2.compareTo(d1) > 0);
    53. assertTrue(d3.compareTo(d1) < 0);
    54. assertTrue(d3.compareTo(d2) < 0);
    55. DecimalWrapper d4 = DecimalWrapper.of("1.00");
    56. assertTrue(d3.compareTo(d4) == 0);
    57. DecimalWrapper d5 = null;
    58. assertTrue(d3.compareTo(d5) > 0);
    59. }
    60. @Test
    61. void testToDecimal() {
    62. DecimalWrapper d1 = DecimalWrapper.of(1.11);
    63. assertEquals(d1.toBigDecimal(), new BigDecimal("1.11"));
    64. }
    65. @Test
    66. void testToDouble() {
    67. DecimalWrapper d1 = DecimalWrapper.of(1.11);
    68. assertEquals(d1.toDouble(), 1.11);
    69. }
    70. @Test
    71. void testEquals() {
    72. DecimalWrapper d1 = DecimalWrapper.of(1.12345);
    73. DecimalWrapper d2 = DecimalWrapper.of(1.12345f);
    74. DecimalWrapper d3 = DecimalWrapper.of("1.12345");
    75. DecimalWrapper d4 = DecimalWrapper.of(new BigDecimal("1.12345"));
    76. assertEquals(d1, d2);
    77. assertEquals(d2, d3);
    78. assertEquals(d3, d4);
    79. }
    80. @Test
    81. void testRoundDefault() {
    82. DecimalWrapper d1 = DecimalWrapper.of(1.12385);
    83. DecimalWrapper d2 = DecimalWrapper.of(1.12385f);
    84. DecimalWrapper d3 = DecimalWrapper.of("1.12385");
    85. DecimalWrapper d4 = DecimalWrapper.of(new BigDecimal("1.12385"));
    86. assertEquals(d1.round(), DecimalWrapper.of("1.12"));
    87. assertEquals(d2.round(), DecimalWrapper.of("1.12"));
    88. assertEquals(d3.round(), DecimalWrapper.of("1.12"));
    89. assertEquals(d4.round(), DecimalWrapper.of("1.12"));
    90. }
    91. @Test
    92. void testRound() {
    93. DecimalWrapper d1 = DecimalWrapper.of(1.12385);
    94. DecimalWrapper d2 = DecimalWrapper.of(1.12385f);
    95. DecimalWrapper d3 = DecimalWrapper.of("1.12385");
    96. DecimalWrapper d4 = DecimalWrapper.of(new BigDecimal("1.12385"));
    97. assertEquals(d1.round(3), DecimalWrapper.of("1.124"));
    98. assertEquals(d2.round(3), DecimalWrapper.of("1.124"));
    99. assertEquals(d3.round(3), DecimalWrapper.of("1.124"));
    100. assertEquals(d4.round(3), DecimalWrapper.of("1.124"));
    101. assertEquals(d4.round(3, RoundingMode.DOWN), DecimalWrapper.of("1.123"));
    102. }
    103. @Test
    104. void testFormat() {
    105. DecimalWrapper d1 = DecimalWrapper.of(1.12385);
    106. assertEquals(d1.format(), "1.12");
    107. assertEquals(d1.format(3), "1.124");
    108. assertEquals(d1.format(3, RoundingMode.DOWN), "1.123");
    109. }
    110. @Test
    111. void testAdd() {
    112. DecimalWrapper d1 = DecimalWrapper.of(1.12385);
    113. DecimalWrapper d2 = DecimalWrapper.of(1.12385);
    114. assertEquals(DecimalWrapper.of("2.24770"), d1.add(d2));
    115. DecimalWrapper d3 = DecimalWrapper.of(0);
    116. assertEquals(DecimalWrapper.of("1.12385"), d3.add(d2));
    117. assertTrue(d3.add(d3).isZero());
    118. }
    119. @Test
    120. void testSubtract() {
    121. DecimalWrapper d1 = DecimalWrapper.of(2.24770);
    122. DecimalWrapper d2 = DecimalWrapper.of(1.12385);
    123. assertEquals(DecimalWrapper.of("1.12385"), d1.subtract(d2));
    124. DecimalWrapper d3 = DecimalWrapper.of(0); // change to: 0.0
    125. assertEquals(DecimalWrapper.of("1.12385"), d2.subtract(d3));
    126. assertTrue(d2.subtract(d2).isZero());
    127. assertTrue(d3.subtract(d3).isZero());
    128. }
    129. @Test
    130. void testMultiply() {
    131. DecimalWrapper d1 = DecimalWrapper.of(1.12385);
    132. DecimalWrapper d2 = DecimalWrapper.of(1.12385);
    133. assertEquals(DecimalWrapper.of("1.2630388225"), d1.multiply(d2));
    134. DecimalWrapper d3 = DecimalWrapper.of(0.00);
    135. assertTrue(d2.multiply(d3).isZero());
    136. DecimalWrapper d4 = DecimalWrapper.of(-1);
    137. assertEquals(DecimalWrapper.of("-1.12385"), d1.multiply(d4));
    138. }
    139. @Test
    140. void testDivide() {
    141. DecimalWrapper d1 = DecimalWrapper.of(10.0);
    142. DecimalWrapper d2 = DecimalWrapper.of(3);
    143. assertEquals(DecimalWrapper.of(3.33), d1.divide(d2));
    144. DecimalWrapper d3 = DecimalWrapper.of(0);
    145. assertTrue(d3.multiply(d1).isZero());
    146. try {
    147. d1.divide(d3);
    148. fail("divide by zero, should not get here!");
    149. } catch (Exception e){}
    150. }
    151. @Test
    152. void testNegate() {
    153. DecimalWrapper d1 = DecimalWrapper.of(-1.12385);
    154. assertTrue(d1.isNegative());
    155. assertFalse(d1.isZero());
    156. assertFalse(d1.isPositive());
    157. DecimalWrapper d2 = d1.negate();
    158. assertTrue(d2.isPositive());
    159. assertFalse(d2.isZero());
    160. assertFalse(d2.isNegative());
    161. assertTrue(d1.add(d2).isZero());
    162. }
    163. }

    测试类说明:

    测试类的代码和被测类的代码都比较直观,不需做过多说明。

    在该类中,我们为被测类的每个公共方法都创建了测试方法,且只用到了JUnit的@Test和Assertions;如果需要事前准备和事后清理工作,还可以加上@BeforeEach、@AfterEach、@BeforeAll、@AfterAll等方法。

    第37行的testConstructNullParam是负面测试,测试当传入null值是不允许的,会抛出NullPointerException。

    第101行的testRoundDefault、第113行的testRound,是对同一个方法的不同情况(精度)的测试。

    私有方法的测试

    私有方法不需要测试。对私有方法的测试,应通过调用它的公有方法的测试来进行。直接测试私有方法往往是一种代码“坏味道”。

    虽然但是,有时候确实想给私有方法写一个测试,也不是做不到:

    1. package com.aaa.api.auth.filter;
    2. import com.aaa.sdk.utils.Utils;
    3. import org.junit.jupiter.api.Test;
    4. import java.lang.reflect.InvocationTargetException;
    5. import java.lang.reflect.Method;
    6. import static org.junit.jupiter.api.Assertions.*;
    7. class AuthNFilterTest {
    8. @Test
    9. void testExtractUsernameFromToken() throws InvocationTargetException, IllegalAccessException {
    10. String token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...";
    11. AuthNFilter filter = new AuthNFilter();
    12. Method m = Utils.doGetMethod(AuthNFilter.class, "extractUsernameFromToken", String.class);
    13. //long start = System.currentTimeMillis();
    14. String user = (String) m.invoke(filter, token);
    15. //System.out.println("User is " + user + ", used " + (System.currentTimeMillis() - start) + " ms");
    16. assertEquals("ioriogami", user);
    17. }
    18. }

    第15行,我们new了一个待测试对象。

    第16行,我们用反射获取了AuthNFilter类的extractUsernameFromToken私有方法(该方法接受一个String类型的参数),然后在第18行对其进行调用。

     如需对构造方法、私有方法、静态方法、final类和方法进行mock,可以使用powermock提供的增强版Mockito:PowerMockito

    总结

    普通类的测试以JUnit简单测试为主,一般不需要Spring上下文。

    每一个public方法都有至少一个测试;对不同的情况/分支,建议有多个测试;最好也有负面测试。当然,一个完美的测试类应该测试每个方法的正面行为、负面行为、边缘情况;并应尽可能覆盖所有分支。但在有限的时间内,我们应识别最重要的方面进行测试。

    单元测试针对单个类,原则上测试类与被测类一对一。当然也可以针对一组紧密相关的类/接口编写单元测试,比如pagewindow是一个实现无界分页的小模块,由几个接口和类组成,它的单元测试就是针对这一组类的。

    Mock的原理是采用字节码生成的方式对要mock的类进行sub-class,然后生成这个子类的对象,以此达到对其行为进行订制的目的。显然,这种方式会受到Java继承机制的限制:静态类/方法、final类/方法、构造器、私有方法等都不能继承,因此就难以订制。

    Mockito在2.1.0中加入了对final类/方法的支持。见 https://www.cnblogs.com/yuluoxingkong/p/14813558.html

    powermock提供的增强版Mockito:PowerMockito 则对这些方面提供了全面的支持。

  • 相关阅读:
    Windows命令--批处理的用法
    React入门(上)
    DP. 数字三角形模型
    985测试工程师被吊打,学历和经验到底谁更重要?
    对GROUP BY的增强
    2024上海国际智慧城市展览会(世亚智博会)智慧城市,数字中国
    Paddle CrowdNet 人群密度估计
    螺丝扭断力试验机SJ-12
    【LeetCode】1796. 字符串中第二大的数字
    【天衍系列 01】深入理解Flink的 FileSource 组件:实现大规模数据文件处理
  • 原文地址:https://blog.csdn.net/ioriogami/article/details/134481518