深入JUnit:Java测试的全面指南

前言

软件开发交付之前需要经过系统的测试,测试完全通过才能正式交付并进入运营阶段。有的时候为了减少测试和开发之间的推诿,在正式测试之前很多公司都要求开发自测,保证测试场景覆盖率超过一定标准才能测试,今天我们就来使用 Junit 来进行代码测试以及 Mock。
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。这是基础,所以围绕着单元测试,我从网上搜集和总结了相关的概念,以助你完善体系

项目搭建

Junit

  • Junit 是 Java 程序主流测试框架,主要通过 @Test 开发测试用例。我们可以使用不同的注解完成不同场景的测试。禁用测试用例,组合测试用例,随机使用测试参数等等场景。这里不细细展开。

Mockito

  • mock 就是生成数据,开发中依赖是常有的事,比如我在开发 A 类,它依赖 B 功能。常规的测试这个时候就需要去构建 B 对象才能完成 A 的整体功能测试。
  • mock 也不是随便去生成内容填充的。最基本我要的是整型你总不能生成非数字内容给我吧,这样肯定是会出错的或者导致不可预估的问题。
  • 那么 mock 的作用也就不言而喻了,他就是充当一个解偶的角色,我不需要特别关心 B 的实现细节。我只需要 mock 出 B 对象内部部分我所需要的东西。这样就能完整的跑完我的 A 部分功能。比如我在测试 A 的时候需要 B 里的 name 属性为 zxhtom 。那么我只需要 mock 出一个名叫 zxhtom 的 B 就行了。

mock 的必要性

  • 还是上面的场景,比如 B 对象依赖 C 对象。C 对象很复杂。里面属性很多。而且依赖链也特别的长。这个时候如果我们通过 New 去构建肯定不现实,尤其是 Spring 这种容器化管理 Bean 的创建都是初始化阶段完成的,我们完全无感,根本不知道他的创建流程。这个时候我们通过 Mock 去创建 B 对象就很好的避免依赖链复杂的问题了。
  • 还比如 A 调用 B 类的时候我们正常会进行异常处理。加入 A-->B 是通过 Feign 调用的,之间存在网络通信问题,我们常常也会捕获连接异常问题,或者捕获调用超时异常。这种场景我们是无法模拟的,mock 却可以帮我们解决这个痛点。

集成

xml 复制代码
<dependencies>
	<dependency>
		<groupId>junit</groupId>
		<artifactId>junit</artifactId>
		<version>4.12</version>
		<scope>test</scope>
	</dependency>
	<!-- https://mvnrepository.com/artifact/org.mockito/mockito-core -->
	<dependency>
		<groupId>org.mockito</groupId>
		<artifactId>mockito-core</artifactId>
		<version>3.7.7</version>
		<scope>test</scope>
	</dependency>
</dependencies>
  • 因为我们是 Spring 项目。所以我们通过 Spring 也能引入依赖。
xml 复制代码
<dependencies>
  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
  </dependency>
</dependencies>

Demo

  • 我们在 shuiliandong 项目下新增如下一个结构的模块
java 复制代码
package com.yapai.shuiliandong.mocktest.dto;

import java.util.Random;

public class DemoDao {
    public int getDemoStatus(){
        return new Random().nextInt();
    }
}
java 复制代码
package com.yapai.shuiliandong.mocktest.service;

import com.yapai.shuiliandong.mocktest.dto.DemoDao;

public class DemoService {

    private DemoDao demoDao;

    public DemoService(DemoDao demoDao) {
        this.demoDao = demoDao;
    }

    public int getDemoStatus(){
        return demoDao.getDemoStatus();
    }
}
java 复制代码
package com.yapai.shuiliandong.mocktest;

import com.yapai.shuiliandong.mocktest.dto.DemoDao;
import com.yapai.shuiliandong.mocktest.service.DemoService;
import org.junit.Assert;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

/**
 * Hello World Test.
 */
public class HelloWorldTest {

    @Test
    public void helloWorldTest() {
        // mock DemoDao instance
        DemoDao mockDemoDao = Mockito.mock(DemoDao.class);

        // 使用 mockito 对 getDemoStatus 方法打桩
        Mockito.when(mockDemoDao.getDemoStatus()).thenReturn(1);

        // 调用 mock 对象的 getDemoStatus 方法,结果永远是 1
        Assert.assertEquals(1, mockDemoDao.getDemoStatus());

        // mock DemoService
        DemoService mockDemoService = new DemoService(mockDemoDao);
        Assert.assertEquals(1, mockDemoService.getDemoStatus() );
    }
}
  • 最后我们运行 Run with Coverage
  • 理论上我们就能够看到针对 DemoDaoDemoService 两个类的测试报告了。
  • 可以简单粗暴的看出 DemoDao 的测试场景不足才 0%。因为我们上面的测试用例就是来测试 DemoService 的。 DemoService 100%已经达到了这个测试用例的作用了。大家测试用例尽量能够职责单一。

默认值

  • mockito 中为了方便我们测试,都为我们准备的默认数据。比如 A 类中存在 int 类型属性,如果没有指定的话那就是 0。
  • 除了属性之外方法也是。针对有返回类型的方法 mock 时会返回该类型的默认值。
java 复制代码
List mockList = mock(List.class);
// 接口的默认值:和类方法一致,都是默认返回值
Assert.assertEquals(0, mockList.size());
Assert.assertEquals(null, mockList.get(0));

// 注意:调用 mock 对象的写方法,是没有效果的
mockList.add("a");
Assert.assertEquals(0, mockList.size());      // 没有指定 size() 方法返回值,这里结果是默认值
Assert.assertEquals(null, mockList.get(0));   // 没有指定 get(0) 返回值,这里结果是默认值

// mock值测试
when(mockList.get(0)).thenReturn("a");          // 指定 get(0)时返回 a
Assert.assertEquals(0, mockList.size());        // 没有指定 size() 方法返回值,这里结果是默认值
Assert.assertEquals("a", mockList.get(0));      // 因为上面指定了 get(0) 返回 a,所以这里会返回 a
Assert.assertEquals(null, mockList.get(1));     // 没有指定 get(1) 返回值,这里结果是默认值

注解 MOCK

  • 每次针对需要 mock 的对象都需要使用 mock() 方法总觉得有些麻烦,就好像 Java 对象第一次使用的时候都需要 new XXX() 才可以。为了像 Spring 一样注解式使用 bean , mockito 也为我们提供了便捷方式。@Mock == @Autoriwed。但是想要使用该注解的前提是我们需要测试依赖 MockIto 的环境。
java 复制代码
/**
 * Mock Annotation
 */
@RunWith(MockitoJUnitRunner.class)
public class MockAnnotationTest {

    @Mock
    private Random random;

    @Test
    public void test() {
        when(random.nextInt()).thenReturn(100);
        Assert.assertEquals(100, random.nextInt());
    }
}

多次打桩

  • 在 mock 过程中专业名词应该叫数据打桩。如果我们针对同一个对象进行多次打桩,那么将会覆盖式打桩,这也和目前主流语言赋值思维相同。
java 复制代码
@RunWith(MockitoJUnitRunner.class)
public class ParameterTest {

    @Mock
    private List<String> testList;

    @Test
    public void test01() {

        // 模糊匹配
        when(testList.get(anyInt())).thenReturn("c");
        Assert.assertEquals("c", testList.get(0));
        Assert.assertEquals("c", testList.get(1));
        // 精确匹配 0
        when(testList.get(0)).thenReturn("a");
        Assert.assertEquals("a", testList.get(0));

        // 精确匹配 0
        when(testList.get(0)).thenReturn("b");
        Assert.assertEquals("b", testList.get(0));


    }
}

模拟异常

  • 为了能够使得我们的代码更加的健壮,异常情况也是我们需要模拟的。比如我们下面这段代码
java 复制代码
// 这里的name是外部传递过来的
try {
   aaa.doSth();
} catch (ParseException e) {
    throw new BusinessException("存在子集禁止删除"+Integer.valueOf(name));
}
  • 当发生 snum>0 时我们就会抛出异常。比如这个场景很难触发,必须得上正式环境由真实客户使用才能触发,那该怎么办?也许你会认为这么简单的一段代码不需要测试。但是很有可能外部传递的 name 这个时候是个字符串。那么当 snum>0 我们的代码就会报错。这样对于业务来说提示信息完全变味了。
  • 回到刚才那个问题,我们也可以编写个 main 方法,然后在 main 方法中调用。但是这种办法是有局限性的。必须保证该方法内部使用的对象都是简单对象,比如 Request 对象我们就很难去实现。
  • 这就是 mock 的好处。 mockito 不仅为我们 mock 数据,还可以为我们打桩异常
java 复制代码
@Test
public void throwTest1() {

	Random mockRandom = mock(Random.class);
	when(mockRandom.nextInt()).thenThrow(new RuntimeException("异常"));

	try {
		mockRandom.nextInt();
		Assert.fail();  // 上面会抛出异常,所以不会走到这里
	} catch (Exception ex) {
		Assert.assertTrue(ex instanceof RuntimeException);
		Assert.assertEquals("异常", ex.getMessage());
	}
}
  • 上述 Random#nextInt 是有返回类型的,如果我们想打桩无返回类型的方法该怎么模拟呢?
java 复制代码
@Test
public void test() {

	// 这种写法可以达到效果
	doThrow(new RuntimeException("异常")).when(exampleService).hello();

	try {
		exampleService.hello();
		Assert.fail();
	} catch (RuntimeException ex) {
		Assert.assertEquals("异常", ex.getMessage());
	}

}

doAnswer

  • 在上面 doThrow 的使用中我们了解到是针对 void 方法。实际上 void 方法默认对应的是 doNothings . 他主要是帮助我们简化某方法,比如 a-->b 方法,我们测试 a 方法的时候不希望哪部的 b 方法执行。就可以通过 doNothings 执行。 void 方法默认是 doNothings .相反我们想让 void 方法执行可以通过 doCallRealMethod 来配置。
  • 除了方法是否执行以外,方法内部还存在变量。有的时候我们测试的方法内部也特别的复杂。虽然这种不友好,但是你无法避免碰到这种代码。针对这种情况,要么重新优化代码将逻辑分为不同的方法进行不同的单元测试,要么就只能一根筋进行测试,而且这样无法准确的模拟出各种场景。

Spy

  • spy 的参数是对象示例,mock 的参数是 class。
  • 被 spy 的对象,调用其方法时默认会走真实方法。mock 对象不会。

Plus

  • PowerMock 是 Mockito 的一种增强,他们的 PowerMock 可以调用 Mockito 的方法,但是对于 Mocktio 不能 Mock 的对象或者方法,我们可以使用 PowerMock 来实现。比如 Mockito 不能用于 static Method, final method, 枚举类, private method,这些我们都可以用 powermock 来实现,当 Powermock 和 mockito 结合使用的时候,我们需要考虑兼容性的问题。两者的版本需要兼容

总结

被测系统被测系统 (System under test, SUT)

表示正在被测试的系统, 目的是测试系统能否正确操作. 根据测试类型的不同, SUT 指代的内容也不同, 例如 SUT 可以是一个类甚至是一整个系统.

测试依赖组件(DOC)

被测系统所依赖的组件, 例如进程 UserService 的单元测试时, UserService 会依赖 UserDao, 因此 UserDao 就是 DOC.

测试替身(Test Double)

一个实际的系统会依赖多个外部对象, 但是在进行单元测试时, 我们会用一些功能较为简单的并且其行为和实际对象类似的假对象来作为 SUT 的依赖对象, 以此来降低单元测试的复杂性和可实现性. 在这里, 这些假对象就被称为测试替身(Test Double). 测试替身有如下 5 种类型:

  • Test stub 为 SUT 提供数据的假对象,我们举一个例子来展示什么是 Test stub.假设我们的一个模块需要从 HTTP 接口中获取商品价格数据, 这个获取数据的接口被封装为 getPrice 方法. 在对这个模块进行测试时, 我们显然不太可能专门开一个 HTTP 服务器来提供此接口, 而是提供一个带有 getPrice 方法的假对象, 从这个假对象中获取数据. 在这个例子中, 提供数据的假对象就叫做 Test stub.
  • Fake object 实现了简单功能的一个假对象. Fake object 和 Test stub 的主要区别就是 Test stub 侧重于用于提供数据的假对象, 而 Fake object 没有这层含义.使用 Fake object 的最主要的原因就是在测试时某些组件不可用或运行速度太慢, 因而使用 Fake object 来代替它们.
  • Mock object 用于模拟实际的对象, 并且能够校验对这个 Mock object 的方法调用是否符合预期.实际上, Mock object 是 Test stub 或 Fake object 一种, 但是 Mock object 有 Test stub/Fake object 没有的特性, Mock object 可以很灵活地配置所调用的方法所产生的行为, 并且它可以追踪方法调用, 例如一个 Mock Object 方法调用时传递了哪些参数, 方法调用了几次等.
  • Dummy object 在测试中并不使用的, 但是为了测试代码能够正常编译/运行而添加的对象. 例如我们调用一个 Test Double 对象的一个方法, 这个方法需要传递几个参数, 但是其中某个参数无论是什么值都不会影响测试的结果, 那么这个参数就是一个 Dummy object. Dummy object 可以是一个空引用, 一个空对象或者是一个常量等.简单的说, Dummy object 就是那些没有使用到的, 仅仅是为了填充参数列表的对象.
  • Test Spy 可以包装一个真实的 Java 对象, 并返回一个包装后的新对象. 若没有特别配置的话, 对这个新对象的所有方法调用, 都会委派给实际的 Java 对象.mock 和 spy 的区别是: mock 是无中生有地生出一个完全虚拟的对象, 它的所有方法都是虚拟的; 而 spy 是在现有类的基础上包装了一个对象, 即如果我们没有重写 spy 的方法, 那么这些方法的实现其实都是调用的被包装的对象的方法.

Test fixture

  • 所谓 test fixture, 就是运行测试程序所需要的先决条件(precondition). 即对被测对象进行测试时锁需要的一切东西(The test fixture is everything we need to have in place to exercise the SUT). 这个东西不单单指的是数据, 同时包括对被测对象的配置, 被测对象所需要的依赖对象等. JUnit4 之前是通过 setUp, TearDown 方法完成, 在 JUnit4这, 我们可以使用@Before 代替 setUp 方法, @After 代替 tearDown 方法.注意, @Before 在每个测试方法运行前都会被调用, @After 在每个测试方法运行后都会被调用.因为 @Before 和 @After 会在每个测试方法前后都会被调用, 而有时我们仅仅需要在测试前进行一次初始化, 这样的情况下, 可以使用@BeforeClass 和@AfterClass 注解.

测试用例(Test case)

  • 在 JUnit 3中, 测试方法都必须以 test 为前缀, 且必须是 public void 的, JUnit 4之后, 就没有这个限制了, 只要在每个测试方法标注 @Test 注解, 方法签名可以是任意的.

测试套件

  • 通过 TestSuit 对象将多个测试用例组装成一个测试套件, 测试套件批量运行.通过@RunWith 和@SuteClass 两个注解, 我们可以创建一个测试套件. 通过@RunWith 指定一个特殊的运行器, 几 Suite.class 套件运行器, 并通过@SuiteClasses 注解, 将需要进行测试的类列表作作为参数传入.

放松一刻

Honesty is the best policy. --- Benjamin Franklin

相关推荐
customer082 分钟前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
全栈开发圈4 分钟前
新书速览|Java网络爬虫精解与实践
java·开发语言·爬虫
WaaTong6 分钟前
《重学Java设计模式》之 单例模式
java·单例模式·设计模式
面试鸭8 分钟前
离谱!买个人信息买到网安公司头上???
java·开发语言·职场和发展
沈询-阿里1 小时前
java-智能识别车牌号_基于spring ai和开源国产大模型_qwen vl
java·开发语言
AaVictory.1 小时前
Android 开发 Java中 list实现 按照时间格式 yyyy-MM-dd HH:mm 顺序
android·java·list
Yaml41 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理