IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 开发测试 -> Java单元测试实践 -> 正文阅读

[开发测试]Java单元测试实践

一、背景

单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。针对Java程序而言,单元测试则是对某个类进行测试,主要以public方法作为入口,方法中调用了其他类的方法,则需要进行屏蔽(mock)。

单元测试是由程序员自己来完成,最终受益的也是程序员自己。可以这么说,程序员有责任编写功能代码,同时也就有责任为自己的代码编写单元测试。执行单元测试,就是为了证明这段代码的行为和我们期望的一致。

单元测试与集成测试的区别

  • 测试对象不同。单元测试对象是实现了具体功能的程序单元;集成测试对象是概要设计规划中的模块及模块间的组合。
  • 测试方法不同。单元测试中的主要方法是基于代码的白盒测试;集成测试中主要使用基于功能的黑盒测试。
  • 测试时间不同。集成测试晚于单元测试。
  • 测试内容不同。单元测试主要是模块内程序的逻辑、功能、参数传递、变量引用、出错处理及需求和设计中具体要求方面的测试;集成测试主要验证各个接口、接口之间的数据传递关系,及模块组合后能否达到预期效果。

单元测试的误解

  1. 它浪费了太多的时间,逻辑太复杂写起来比较浪费时间。
  2. 我是个很棒的程序员, 我是不是可以不进行单元测试?
  3. 不管怎样,集成测试将会抓住所有的Bug。
  4. 运行一次单元测试要等待很久,反馈太慢。

First原则

  • Fast : 测试要非常快,毫秒级,每秒能完成多个单元测试,这样开发人员可以对每一个小更改运行测试,而不用中断思绪去等待测试运行。
  • Isolated : 测试能够清楚的隔离一个失败,不同的测试用例之间是隔离的。一个测试不会依赖另一个测试。不同测试的故障是相互隔离的。
  • Repeatable : 测试应该可以重复运行,且每次都以同样的方式成功或失败。
  • Self-verifying : 测试要无歧义的表达成功或失败,自我验证而不是人工判断。
  • Timely : 测试是及时的,频繁、小规模的修改代码,及时的运行测试。

什么时候用Mock

  • 被测试单元所依赖的第三方类运行结果不稳定(运行时间太长、网络异常)

  • 被测试单元所依赖的模块返回值比较难模拟,构造比较复杂

  • 被测试单元所依赖的模块返回结果不确定

  • 被测试单元所依赖的模块尚未开发完成

二、单元测试模板

准备执行,和校验

  • 准备数据-》Given

    这个部分创建我们将要测试方法的输入参数,或者Mock函数的返回值(mock的方法也会在这个部分中准备,因为mock属于测试执行的准备工作)。通常单元测试用例中,这个部分应该是最长,也是最复杂的。

  • 执行-》When

    这里一般只Call测试方法,这里标明了测试目的,因为这个部分的代码一般是最短的了。

  • 验证-》Then

    这个部分,执行环节的所有结果在这里得以声明。除此之外,也可以确认方法是否被执行。总之,主要的点都在这里进行Check。

三、单元测试命名

  • 类命名规则:测试类与被测试类的命名应保持一致,通常情况下,测试类的名称为:被测试类名称+Test后缀。
    如:GameService的测试类命名为:GameServiceTest

  • 包路径规则:package的路径主要与被测试类的路径保持一致,同时在合适的地方增加一个层级,用于区分“单元测试”、“集成测试”。

    如:有个被测试的类的全路径是:com.iccboy.project.scene.GameService,则测试的包路径是:com.iccboy.project.unit.scene.GameServiceTest

  • 方法命名规则:测试方法应表述业务含义,这样就能使得测试类可以成为文档。测试方法名可以足够长,以便于清晰的表述业务。为了更好地辨别方法名表述的含义,建议采用Ruby风格的命名方法,即下划线分隔方法的每个单词。建议测试方法名以should开头,此时,默认的主语为被测试类。为了更容易定位到被测试方法,命名也可以是 被测试方法名_should_xxx_when_xxx

should_return_0A0B_when_no_number_guessed_correctly
guessHistory_should_record_every_guess_result
should_throw_OutOfRangeAnswerException_which_is_not_between_0_and_9
play_should_end_game_and_display_sucessful_message_when_number_is_correct_in_first_round

四、Mock

  • Mockito:EasyMock之后流行的mock工具。相对EasyMock学习成本低,而且具有非常简洁的API,验证语法简洁,测试代码的可读性很高。StackOverflow 社区将 Mockito 评为 Java 的最佳模拟框架。
  • PowerMock: 这个工具是在EasyMock和Mockito上扩展出来的,目的是为了解决EasyMock和Mockito不能解决的问题,比如对static, final, private方法均不能mock。其实测试架构设计良好的代码,一般并不需要这些功能,但如果是在已有项目上增加单元测试,老代码有问题且不能改时,就不得不使用这些功能了。

需要引入maven依赖

<!-- 单元测试 -->
```xml
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-module-junit4</artifactId>
    <version>2.0.9</version>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <artifactId>objenesis</artifactId>
            <groupId>org.objenesis</groupId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-module-junit4-rule</artifactId>
    <version>2.0.9</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-api-mockito2</artifactId>
    <version>2.0.9</version>
    <scope>test</scope>
</dependency>

1. 启用 Mockito 注解

使用 MockitoJUnitRunner 注解 JUnit 测试,如以下示例所示:

import org.mockito.junit.MockitoJUnitRunner;

@RunWith(MockitoJUnitRunner.class)
public class GameServiceTest {
    ...
}

如果需要 Mock 静态方法,需要使用 PowerMock 代替 Mockito。使用 @RunWith(PowerMockRunner.class)和 @PrepareForTest 注解。如以下示例所示:

import org.powermock.modules.junit4.PowerMockRunner;

@RunWith(PowerMockRunner.class)
@PrepareForTest({RedissioUtil.class, GenerateUtil.class}) // RedissioUtil.class, GenerateUtil.class 包含 static 方法
public class GameServiceTest {
    ...
}

开发中,直接使用 @RunWith(PowerMockRunner.class) 即可。

2.@Mock 注解

Mockito 中使用最广泛的注释是 @Mock。 可以使用 @Mock 来创建和注入 Mock 实例。

@Mock
private List<String> mockedList;
 
@Test
public void whenUseMockAnnotation_thenMockIsInjected() {
    Mockito.when(mockedList.size()).thenReturn(100);
    assertEquals(100, mockedList.size());
}

3.@InjectMocks 注解

如何使用 @InjectMocks 注解,将 Mock 字段自动注入到测试对象中。在以下示例中,使用 @InjectMocks 将 mock 的 wordMap 注入到 MyDictionary dic:

@Mock
private Map<String, String> wordMap;
 
@InjectMocks
private MyDictionary dic = new MyDictionary();

//或者
//@InjectMocks
//private MyDictionary dic;
 
@Test
public void whenUseInjectMocksAnnotation_thenCorrect() {
    Mockito.when(wordMap.get("aWord")).thenReturn("aMeaning");
 
    assertEquals("aMeaning", dic.getMeaning("aWord"));
}

这是 MyDictionary 类:

public class MyDictionary {
    Map<String, String> wordMap;
 
    public MyDictionary() {
        wordMap = new HashMap<String, String>();
    }
    public void add(final String word, final String meaning) {
        wordMap.put(word, meaning);
    }
    public String getMeaning(final String word) {
        return wordMap.get(word);
    }
}

说明:

@Mock: 创建一个Mock.

@InjectMocks: 创建一个实例,其余用@Mock(或@Spy)注解创建的mock将被注入到用该实例中。

注意:必须使用@RunWith(MockitoJUnitRunner.class)Mockito.initMocks(this)进行mocks的初始化和注入。

4.When/Then,Mock 预期结果

// Mock mockedList
@Mock
LinkedList mockedList;

// 构造预期结果
when(mockedList.get(0)).thenReturn("first");
when(mockedList.get(1)).thenThrow(new RuntimeException());

// 打印结果 "first"
System.out.println(mockedList.get(0));

// 会抛出 runtime exception
System.out.println(mockedList.get(1));

// 打印结果 "null",因为 get(999) 没有进行 Mock
System.out.println(mockedList.get(999));
// 静态引入Mockito相关的方法
import static org.mockito.Mockito.*;

5.ArgumentMatchers 参数匹配器

5.1 内置参数匹配器

Mockito 通过 equals()方法,来对方法参数进行验证。

when(flowerService.analyze("poppy")).thenReturn("Flower");

在上面的示例中,仅当 flowerService 收到字符串“poppy”时才返回字符串“Flower”。

但有时我们需要更加灵活的参数需求,更大范围的值或预先未知的值做出验证。比如,匹配任何的String类型的参数等等。参数匹配器就是一个能够满足这些需求的工具。

Mockito框架中的Matchers类内建了很多参数匹配器。这些内建的参数匹配器如,anyInt()匹配任何int类型参数,anyString()匹配任何字符串,anySet()匹配任何Set, any(T t)匹配任何T类型的对象等。下面通过例子来说明如何使用内建的参数匹配器:

when(flowerService.analyze(anyString())).thenReturn("Flower");

现在,由于anyString参数匹配器,无论我们传递什么值,结果都将是相同的。

注意:如果一个方法具有多个参数,则不可能仅对某些参数使用 ArgumentMatchers。 Mockito要求您通过匹配器或精确值提供所有参数。

错误事例:

abstract class FlowerService {
    public abstract boolean isABigFlower(String name, int petals);
}

@Mock
FlowerService mock;
 
when(mock.isABigFlower("poppy", anyInt())).thenReturn(true);

如果只输入字符串”poppy”是会报错的,必须使用 Matchers 类内建的 eq 匹配器:

when(mock.isABigFlower(eq("poppy"), anyInt())).thenReturn(true);

eq:等于给定值的参数。

// 静态引入Mockito相关的方法
import static org.mockito.ArgumentMatchers.*;
5.2 自定义参数匹配器

有时我们还是需要更灵活的匹配,所以需要自定义参数匹配器。

自定义参数匹配器的时候需要继承 ArgumentMatcher 抽象类,并实现 matches 方法,在方法中定义规则即可。

下面是自定义的参数匹配器是用于匹配分页参数的 PageableMatcher:

public class PageableMatcher implements ArgumentMatcher<Pageable> {

    private Pageable pageable;

    public PageableMatcher(Pageable pageable) {
        this.pageable = pageable;
    }

    @Override
    public boolean matches(Pageable pageable) {
        return this.pageable.getPageNumber() == pageable.getPageNumber() &&
                this.pageable.getPageSize() == pageable.getPageSize();
    }
}

matches 的逻辑用于比较 pageNumber 与 pageSize 是否与构造函数中传入的 pageNumber 与 pageSize 相等。

使用自定义参数匹配器 PageableMatcher 的例子如下:

when(repository.find(argThat(new PageableMatcher(pageable))))
    .thenReturn(list);

argThat(Matcher<T> matcher)方法用来应用自定义的规则,可以传入任何实现 Matcher 接口的实现类。

6.不同情况的Mock

被mock的方法有以下情况

6.1 有返回值方法
@Mock
private List<String> mockList;

//int size();
when(mockList.size()).thenReturn(9);
// E get(int index);
when(mockList.get(0)).thenReturn("A");
when(mockList.get(1)).thenReturn("B");
6.2 void方法
//void add(int index, E element);
doNothing().when(mockList).add(eq(2), anyString());
6.3 抛异常
when(mockList.get(3)).thenThrow(new IndexOutOfBoundsException());
doThrow(new IndexOutOfBoundsException()).when(mockList).get(3);
6.4 静态方法

静态方法的 Mock 需使用 PowerMockito,Mockito(3.4版本之前) 不支持静态方法的 Mock。

import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

@RunWith(PowerMockRunner.class)
@PrepareForTest({JSON.class})


PowerMockito.mockStatic(JSON.class);
PowerMockito.when(JSON.toJSONString(any())).thenReturn("{\"name\":\"iccboy\"}");
6.5 静态void方法
class XxxUtil {
    public static void clean(String id){
        System.out.println("clean id is:" + id);
    }
}

// -----

@RunWith(PowerMockRunner.class)
@PrepareForTest({XxxUtil.class})

@Test
public void demo5_mock_void_static_method() throws Exception {
    PowerMockito.mockStatic(XxxUtil.class);
    PowerMockito.doNothing().when(XxxUtil.class, "clean", anyString());
}
6.6 私有方法
public class Calculator {
    private int sumXX(int a, int b) {
		return a + b;
	}
    
    public int callSumXX(int a, int b){
    	return sumXX(a, b);
    }
}


@PrepareForTest({Calculator.class})

PowerMockito.when(calculatorMock, "sumXX", 1, 2).thenReturn(2);

6.7 @Value的mock

通过Spring提供的反射工具类来实现

public class GameService{
	
	@Value("${game.times}")
	private Integer times;
}

// ---------

import org.springframework.test.util.ReflectionTestUtils;

@RunWith(MockitoJUnitRunner.class)
public class GameServiceTest{
	
	@InjectMocks
	private GameService gameService;
	
	@Test
	public void mock_atvalue() {
		 ReflectionTestUtils.setField(messageBizServiceImpl, "times", 99);
	}
}

ReflectionTestUtils.setField() 的源码如下:

/**
 * targetObject – 设置字段的目标对象
 * name – 要设置的字段名称
 * value – 要设置的值
 */
public static void setField(Object targetObject, String name, 
    @Nullable Object value) {
    setField(targetObject, name, value, null);
}

7. 参数的捕获

如果要获取被mock的方法在被调用时方法参数的值,可以通过``ArgumentCaptor(或者:@Captor)进行参数的捕获。

class GameService {
    public void saveLifeValue(Integer LifeValue){
        System.out.println("保存生命值:" + LifeValue);
    }

    public void killMonster(Monster LifeValue){
        LifeValue.setDie(true);
    }
}

class Monster {
    private Boolean isDie;

    public void setDie(Boolean isDie) {
        this.isDie = isDie;
    }
}

// ---
@Mock
private GameService gameService;

@Test
public void demo6_mock_arg_captor() {
    ArgumentCaptor<Integer> LifeValueCaptor = ArgumentCaptor.forClass(Integer.class);
    doNothing().when(gameService).saveLifeValue(LifeValueCaptor.capture());
    gameService.saveLifeValue(99);
    System.out.println(LifeValueCaptor.getValue());
}


@Captor
private ArgumentCaptor<Integer> LifeValueCaptor2;

@Test
public void demo6_mock_arg_captor_2() {
    doNothing().when(gameService).saveLifeValue(LifeValueCaptor2.capture());
    gameService.saveLifeValue(99);
    System.out.println(LifeValueCaptor2.getValue());
}

8. 参数值的修改

被mock的方法,如果方法逻辑中对参数(引用)进行了修改,此时可以通过Answer进行操作

@Test
public void demo7_mock_answer() {
    doAnswer(invocation -> {
        Method method = invocation.getMethod();
        System.out.println("mock method is : " + method.getName());
        Monster monster = invocation.getArgument(0);
        monster.setBlood(0);
        monster.setDie(true);
        return monster;
    }).when(gameService).killMonster(any());

    Monster monster = new Monster();
    monster.setBlood(100);
    monster.setDie(false);
    System.out.println("调用方法前:" + monster);
    Monster monster1 = gameService.killMonster(monster);
    System.out.println("调用方法后:" + monster1);
}

// 输出:
调用方法前:isDie:false, blood:100
mock method is : killMonster
调用方法后:isDie:true, blood:0

9. spy

通过spy可以监视真实对象,同时可以进行mock

https://blog.csdn.net/b1480521874/article/details/100972837

class Calculator {
    private int sumXX(int a, int b) {
        return a + b;
    }

    public int callSumXX(int a, int b){
        return sumXX(a, b);
    }
}

//---
@PrepareForTest({Calculator.class})

@Test
public void demo8_mock_private_method() throws Exception {
    Calculator calculatorMock = PowerMockito.spy(new Calculator());
    PowerMockito.when(calculatorMock, "sumXX", 1, 2).thenReturn(2);
    assertThat(calculatorMock.callSumXX(1, 2)).isEqualTo(2);
}


--
@Spy
private Calculator calculatorMock2 = new Calculator();    

@PrepareForTest({Calculator.class})

@Test
public void demo8_mock_private_method_2() throws Exception {
    PowerMockito.when(calculatorMock2, "sumXX", 1, 2).thenReturn(2);
    assertThat(calculatorMock2.callSumXX(1, 2)).isEqualTo(2);
}

spy与mock的区别

1.默认行为不同

对于未指定mock的方法,spy默认会调用真实的方法,有返回值的返回真实的返回值,而mock默认不执行,有返回值的,默认返回null

2.使用方式不同

Spy中用when…thenReturn私有方法总是被执行,预期是私有方法不应该执行,因为很有可能私有方法就会依赖真实的环境。
Spy中用doReturn…when才会不执行真实的方法。

mock中用 when…thenReturn 私有方法不会执行。

3.代码统计覆盖率不同
@spy使用的真实的对象实例,调用的都是真实的方法,所以通过这种方式进行测试,在进行sonar覆盖率统计时统计出来是有覆盖率;
@mock出来的对象可能已经发生了变化,调用的方法都不是真实的,在进行sonar覆盖率统计时统计出来的Calculator类覆盖率为0.00%。

https://www.cnblogs.com/zendwang/p/mockito-mock-spy-usage.html

五、断言

断言工具介绍

验证行为、验证结果、验证参数、超时验证、调用顺序验证

import static org.assertj.core.api.Assertions.*;

https://www.cnblogs.com/bodhitree/p/9456515.html

六、Maven执行单元测试及集成测试

  开发测试 最新文章
pytest系列——allure之生成测试报告(Wind
某大厂软件测试岗一面笔试题+二面问答题面试
iperf 学习笔记
关于Python中使用selenium八大定位方法
【软件测试】为什么提升不了?8年测试总结再
软件测试复习
PHP笔记-Smarty模板引擎的使用
C++Test使用入门
【Java】单元测试
Net core 3.x 获取客户端地址
上一篇文章      下一篇文章      查看所有文章
加:2022-01-25 10:52:56  更:2022-01-25 10:53:34 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/18 4:32:09-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码