教你如何更好的编写JAVA单元测试

如何更好的编写JAVA单元测试

如各位希望转载或引用,请注明出处,尊重原创,谢谢。如有疑问或错误,欢迎邮件沟通。

gitHub地址:https://github.com/thinkingfioa
邮箱地址:[email protected]
博客地址: https://blog.csdn.net/thinking_fioa
gitHub项目地址:https://github.com/thinkingfioa/tech-summary
gitHub项目代码地址:https://github.com/thinkingfioa/tech-summary-code

本文重点

单元测试覆盖率往往是检验一个系统的可靠性指标,优秀的单元测试能帮助系统提早发现问题。JAVA语言提供了非常好的单元测试框架,本文将重点介绍: 如何编写单元测试Mock+PowerMock使用

1. 背景

开发过程中,很多重要的业务和关键性代码,都需要单元测试覆盖,这样能保证质量。

好的单元测试用例,能有效提高软件的质量,也方便后期的代码重构和维护。但是一般来说编写单元测试工作量很大,单元测试代码维护成本也很高,因为你业务逻辑发生了改变,代码结构发生了改变,不可能不会修改单元测试。

通常单元测试的代码,阅读难度也很高,需要理解具体的业务逻辑。建议编写单元测试时,当需要使用其他类的时候,尽量使用Mock方法,做到单元测试依赖越少,后续修改和理解就更简单。

2. 编写单元测试的原则

2.1 单元测试类包路径管理

建议单元测试中的test包路径目录结构与main包中的目录结构保持一致。

2.2 单元测试类名称管理

建议每个单元测试类测试功能单一,仅针对性测试指定类的方法。比如文件Father.java中类名称为Father,那么我们在test新建一个相同的包结构目录,并在新建后的目录下新建FatherTest.java文件,类名为FatherTest。

单元测试中每个测试方法以testXXX()开头

Father.java
package org.thinking.fioa;

public class Father {
  
  public void growUp() throws Exception {
    
  }
}
FatherTest.java
package org.thinking.fioa;

public class FatherTest {
  
  public void testGrowUp() throws Exception {
    
  }
}

3. POM依赖

JAVA语言下单元测试比不可少三个依赖包,需要配置到pom.xml下。powermock + mockito + junit。


   <dependency>
     <groupId>org.powermockgroupId>
     <artifactId>powermock-module-junit4artifactId>
     <version>2.0.2version>
     <scope>testscope>
   dependency>
   <dependency>
     <groupId>org.powermockgroupId>
     <artifactId>powermock-api-mockito2artifactId>
     <version>2.0.2version>
     <scope>testscope>
   dependency>
   <dependency>
     <groupId>junitgroupId>
     <artifactId>junitartifactId>
     <version>4.12version>
     <scope>testscope>
   dependency>
   <dependency>
     <groupId>org.mockitogroupId>
     <artifactId>mockito-coreartifactId>
     <version>2.20.1version>
     <scope>testscope>
   dependency>

4. 基础知识

Java使用的Junit4常用的annotation

4.1 静态方法

@BeforeClass ----- 针对所有测试,只执行一次。方法签名必须是static void
@AfterClass ----- 针对所有测试,只执行一次。方法签名必须是static void

4.2 非静态方法

@Before ----- 初始化方法。每个@Test测试方法前都会执行一次
@Test ----- 测试方法。每个@Test测试方法都会创建一个实例对象
@After ----- 释放资源。每个@Test测试方法后都会执行一次

4.3 执行顺序

@BeforeClass -> {类的构造函数 -> @Before -> @Test -> @After} , 类的构造函数 -> {@Before -> @Test -> @After} … -> @AfterClass

其中每个@Test方法执行前会创建新的XxxTest实例, 单个@Test方法执行前后会执行@Before和@After方法

4.4 断言的常方法

assertEquals(100, x) ----- 相等
assertArrayEquals(100, x) ----- 数组相等
assertNull(x) ----- 断言为null
assertTrue(x) ----- 断言为true
assertNotEquals ----- 断言不相等
expected = Exception.class ----- 异常测试
timeout=1000 ----- 超时时间

5. 单元测试编写参考用例

详细代码可参考tech-summary-code

下面举例介绍四大类单元测试方法,这四类单元测试用例能基本满足大家日常编写单元测试的功能

序号 名称 说明
1 基础单元测试 基础用例
2 使用单例设计模式 单例是开发中最长使用的设计模式
3 依赖其他类 面向对象语言,封装是一大特性
4 类对象都是私有属性 类的属性都是私有,或者方法是私有的,可通过反射方法来编写单元测试

5.1 基础单元测试用例

基础单元测试是最被经常使用的

5.1.1 类CommonMethod.java

测试类CommonMethod.java有如下的对象公开方法,为每个方法编写单元测试

public class CommonMethod {

  public boolean success() {
    return true;
  }

  public int age() {
    return 100;
  }

  public int[] arrayOfInt() {
    return new int[]{1, 2, 3, 4};
  }

  public Object isNull() {
    return null;
  }

  public void throwException() {
    throw new NullPointerException();
  }

  public void timeout() throws InterruptedException {
    Thread.sleep(500L);
  }
}
5.1.2 测试类CommonMethodTest.java
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

public class CommonMethodTest {

  private CommonMethod method;

  /**
   * 仅执行一次
   */
  @BeforeClass
  public static void setUpBeforeClass() {
    System.out.println("Before Class.");
  }

  /**
   * 仅执行一次
   */
  @AfterClass
  public static void setUpAfterClass() {
    System.out.println("After Class.");
  }

  @Before
  public void setUpBefore() {
    System.out.println("Before.");
    method = new CommonMethod();
  }

  @After
  public void setUpAfter() {
    System.out.println("After.");
  }

  @Test
  public void testSuccess() {
    System.out.println("testSuccess.");
    assertTrue(method.success());
  }

  @Test
  public void testAge() {
    System.out.println("testAge.");
    assertEquals(100, method.age());
  }

  @Test
  public void testArrayOfInt() {
    System.out.println("testArrayOfInt.");
    int[] copyArray = {1, 2, 3, 4};
    assertArrayEquals(copyArray, method.arrayOfInt());
  }

  @Test
  public void testIsNull() {
    System.out.println("testIsNull.");
    assertNull(method.isNull());
  }

  @Test(expected = NullPointerException.class)
  public void testThrowException() {
    System.out.println("testThrowException.");
    method.throwException();
  }

  @Test(timeout = 1000)
  public void testTimeout() throws InterruptedException {
    System.out.println("testTimeout.");
    method.timeout();
  }
}

5.2 Mock一个单例对象

项目中最常使用的设计模式就是单例模式,开发某个类时可能需要依赖这些单例模式。编写该类单元测试时,建议使用Mock方法构建另一个单例对象供单元测试使用,而不是直接使用代码中的单例对象。

5.2.1 编写的类

类Line.java方法midPoint()方法使用到了单例对象MathInstance.getInstance()。我们使用Mock方式创建单例对象MathInstance。

public class Line {

  private final Point p1;
  private final Point p2;

  public Line(Point p1, Point p2) {
    this.p1 = p1;
    this.p2 = p2;
  }

  public Point midPoint() {
    if (p1 == null || p2 == null) {
      throw new NullPointerException("p1 or p2 is null");
    }
    // 使用到单例模式
    return MathInstance.getInstance().midPoint(p1, p2);
  }
}

public class Point {

  private int x;
  private int y;

  public Point(int x, int y) {
    this.x = x;
    this.y = y;
  }

  public int getX() {
    return x;
  }

  public int getY() {
    return y;
  }
}

public class MathInstance {

  public static MathInstance getInstance() {
    return SingleInstanceHolder.INSTANCE;
  }

  public Point midPoint(Point p1, Point p2) {
    throw new UnsupportedOperationException("HelloWorld not supported");
  }

  private static class SingleInstanceHolder {

    private static final MathInstance INSTANCE = new MathInstance();
  }
}
5.2.2 单元测试类
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.powermock.api.mockito.PowerMockito.mockStatic;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.powermock.modules.junit4.PowerMockRunner;

@RunWith(PowerMockRunner.class)
@PrepareForTest({MathInstance.class})
@PowerMockIgnore({"javax.management.*", "javax.script.*"})
public class LineTest {

  private Line line;

  @Mock
  private Point p1;
  @Mock
  private Point p2;
  @Mock
  private Point mid;

  /**
  * Mock一个单例对象
  **/
  @Mock
  private MathInstance mathInstance;

  @Before
  public void setUp() {
    when(mid.getX()).thenReturn(5);
    when(mid.getX()).thenReturn(50);
    line = new Line(p1, p2);

    // 单例静态方法进行打桩,供单元测试使用
    mockStatic(MathInstance.class);
    when(MathInstance.getInstance()).thenReturn(mathInstance);
    when(mathInstance.midPoint(p1, p2)).thenReturn(mid);
  }

  @Test(expected = NullPointerException.class)
  public void testMidPointOfNull() {
    Line localLine = new Line(null, null);
    localLine.midPoint();
  }

  @Test
  public void testMidPoint() {
    assertEquals(mid, line.midPoint());
    verify(mathInstance, times(1)).midPoint(p1, p2);
  }
}

5.3 依赖别的类的单元测试

我们编写类A时候,可能会使用到类B的对象,也就是类A会将类B封装到自己的内部,作为自己的私有属性。

通常有两种方式来实现这样的封装:

  1. 类A的构造函数中有一个参数是类B,通过传参传入
  2. 类A中直接创建类B的对象
5.3.1 通过构造函数传参数

通过构造函数传参,建议直接Mock类B的对象。这样我们可以非常方便的为类B对象打桩。

5.3.1.1 编写的类

类PowerController通过构造函数参入参数PowerService对象

public class PowerController {

  private final PowerService service;
  
  // 参数传入
  public PowerController(PowerService service) {
    this.service = service;
  }

  public void saveUser(List<String> userList) {
    if (null == userList || userList.isEmpty()) {
      throw new IllegalArgumentException("userList is empty");
    }
    service.saveUser(userList);
  }

  public int deleteUser() {
    return service.deleteUser();
  }
}

public class PowerService {

  public void saveUser(List<String> userList) {
    throw new UnsupportedOperationException("not supported");
  }

  public int deleteUser() {
    throw new UnsupportedOperationException("not supported");
  }
}
5.3.1.2 单元的类
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.powermock.modules.junit4.PowerMockRunner;

@RunWith(PowerMockRunner.class)
public class PowerControllerTest {

  private static final String USER_NAME = "thinking_fioa";

  /**
  * Mock一个参数
  **/
  @Mock
  private PowerService service;

  private PowerController controller;

  @Before
  public void setUp() {
    controller = new PowerController(service);
  }

  @Test
  public void testSaveUser() {
    List<String> userList = new ArrayList<>();
    userList.add(USER_NAME);
    controller.saveUser(userList);
    verify(service, times(1)).saveUser(anyList());
  }

  @Test(expected = IllegalArgumentException.class)
  public void testSaveUserOfEmpty() {
    List<String> userList = new ArrayList<>();
    controller.saveUser(userList);
  }

  @Test
  public void testDeleteUser() {
    // 为Mock的对象打桩
    when(service.deleteUser()).thenReturn(987);
    assertEquals(987, controller.deleteUser());
  }
}
5.3.2 对象内部直接创建类B

如果是在类A中直接创建出类B的对象,而不是通过构造函数传参。我们需要使用PowerMockito.whenNew来实现打桩。

5.3.2.1 编写的类
public class ConstructorMethod {

  private final InnerService service;

  public ConstructorMethod() {
    // 内部直接创建
    service = new InnerService();
  }

  public void sayWords(List<String> words) {
    if (null == words || words.isEmpty()) {
      throw new IllegalArgumentException("words is empty");
    }
    for (String word : words) {
      service.sayWord(word);
    }
  }

  public int removeWords() {
    return service.removeWords();
  }
}

public class InnerService {

  public void sayWord(String word) {
    throw new UnsupportedOperationException("not supported");
  }

  public int removeWords() {
    throw new UnsupportedOperationException("not supported");
  }
}
5.3.2.2 单元测试的类
import static org.junit.Assert.assertNotEquals;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.powermock.api.mockito.PowerMockito.whenNew;

import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

@RunWith(PowerMockRunner.class)
@PrepareForTest({ConstructorMethod.class})
@PowerMockIgnore({"javax.management.*", "javax.script.*"})
public class ConstructorMethodTest {

  private static final String WORD = "ppp";
  private static final String WORD2 = "courage";

  private ConstructorMethod method;

  @Mock
  private InnerService service;

  @Before
  public void setUp() throws Exception {
    // 打桩类InnerService的构造函数方法
    whenNew(InnerService.class).withAnyArguments().thenReturn(service);
    method = new ConstructorMethod();
  }

  @Test
  public void testSayWords() {
    List<String> words = new ArrayList<>();
    words.add(WORD);
    words.add(WORD2);
    method.sayWords(words);
    verify(service, times(words.size())).sayWord(anyString());
  }

  @Test(expected = IllegalArgumentException.class)
  public void testSaveUserOfEmpty() {
    List<String> words = new ArrayList<>();
    method.sayWords(words);
  }

  @Test
  public void testDeleteUser() {
    // 打桩
    when(service.removeWords()).thenReturn(1987);
    assertNotEquals(987, method.removeWords());
  }
}

5.4 基于反射的单元测试

Java语言开发时,通常会将属性设置为private,这种情况下,可以通过反射方式来实现赋值,供单元测试使用。

5.4.1 编写的类
public class ReflectMethod {

  private String name;

  private int age;

  private ReflectMethod(String name, int age) {
    throw new UnsupportedOperationException("not ready");
  }

  public ReflectMethod() {
  }

  /**
   * 成年人
   *
   * @return
   */
  public boolean isAdult() {
    return age >= 18 && null != name;
  }
}
5.4.2 单元测试的类
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import org.junit.Test;
import org.powermock.reflect.Whitebox;

public class ReflectMethodTest {

  private static final String NAME = "thinking_fioa";

  private static final int AGE = 19;

  @Test(expected = UnsupportedOperationException.class)
  public void testConstructor() throws Exception {
    Whitebox.invokeConstructor(ReflectMethod.class, "ppp", 12);
  }

  @Test
  public void testAdult() {
    ReflectMethod method = new ReflectMethod();
    // 反射赋值
    Whitebox.setInternalState(method, "name", NAME);
    Whitebox.setInternalState(method, "age", AGE);

    assertTrue(method.isAdult());
  }

  @Test
  public void testNotAdult() {
    ReflectMethod method = new ReflectMethod();
    Whitebox.setInternalState(method, "name", NAME);
    Whitebox.setInternalState(method, "age", 14);

    assertFalse(method.isAdult());
  }
}

参考资料

你可能感兴趣的:(java,单元测试,单元测试,PowerMock,Mock)