Janker

V1

2022/09/04阅读:18主题:全栈蓝

面试官:你们团队要求写单测吗?

面试官:你们团队要求写单测吗?

千里之行,始于足下

闲言碎语

大家好,我是janker。

曾几何时我也是个意气风发的少年。

遇见bug敢说:这不是我的bug!

测试提出bug时:这不是bug!设计如此。。。恨不得直接去找测试去疯狂对线。

客户提出问题:他使用姿势有问题!

仿佛测试发现你的bug都是隔壁小伙写的。

而现在,我已经是奔三的老码农。

遇见bug只能说:我先看看!

测试提出bug时:可能是哪里没配置也可能是部署有问题。。。我先看下原因

客户提出问题:稍等我看下,”心里慌的不行,个别case倒还好,要是全局问题,就芭比Q了。。。“

仿佛测试测出来的 bug 都可能与自己有关系。

程序员专用表情包,你都用过几个!

岁月是把杀猪刀,老了码农,白了头发,残了身体,消磨了意志。

不是我们怂了,是我们成熟了。从历史的案例中概率说明了一切。

正文

其实在我们编码中,代码质量一直是影响我们交付的一个比较重要的因素。我们这个行业里有很多诡异的事情,所有人都赞同单元测试非常重要,然而很少人做单元测试

首先我们先来看几个问题。

开发需要写单元测试吗?

答案是必然需要的,有很多公司非常准确地把单元测试叫做“开发自测”,并且非常准确地认识到了这个活动的重要性:程序员只要认真测一测自己写的代码,bug就能减少90%。至于时下流行的敏捷么,我毫不夸张说一句:一切没有充分单元测试覆盖的敏捷都是伪敏捷。别的啥都不说了,没有充分的单元测试覆盖,你持续集成跑啥?持续集成不能保障软件质量,靠测试人员跟在屁股后面人肉回归么?

但是就是这么一件非常有意义、所有人都重视的事情,我们这个行业里,我客气点说,80%的企业落不了地。这难道不是一个值得玩味的文化现象么?

为什么会出现这种情况呢?其实我有一个想法:因为整个行业都被软件工程教材给误导了。

软件教材上说,代码应该有单元测试。这没错,因为他只是描述了一个结果状态。如果做才能打到这种结果状态他没有说。什么时候写单元测试?如何写?什么时候运行单元测试?等等这些每天的工作细节,教材里面都没有。

单元测试通常被认为是编码阶段的附属工作。可以在编码开始之前或源代码生成之后进行单元测试的设计。设计信息的评审可以指导建立测试用例。每个测试用例都应与一组预期结果联系在一起。——《软件工程:实践者的研究方法》(第7版)

实践者们看了这个,能知道怎么动手写单元测试么?于是大家就只好靠猜。想必一定是先写好产品代码再写单元测试来测它吧?一定是这样的!卡桑,我这就可以报效祖国去了吧!

然后一写发现满不是这么回事。因为在写代码的时候并没有考虑这代码要怎么测,所以写完了以后要测发现很难,找不到接缝,测不动。这时候交付压力又紧逼着,唉,要不先放着改天再测吧。当然我们都知道,这个改天再做的事就再也不会做。

软件工程教材里没讲的那一半,单元测试怎么能落地的过程,到底是怎么回事呢?单元测试的过程中,有没有什么技巧呢?

我们从以下几个方面介绍一下单元测试。

到底什么是单元测试?为什么做单元测试?应该怎么做单元测试?

WHAT?

首先我们先简单了解一下到底什么是单元测试?

下面分享一篇鹅厂程序员对单元测试的理解以及如何做单元测试。

在我们谈到单元测试,大都清楚是测试函数符合预期,国外很多大公司都将单测执行的很好,国内成功的案例则相对有限。在本文中,笔者将在腾讯新闻项目中亲身经历单测从无到有的实践过程梳理为可读可参考的经验分享出来。在实践的过程我发现,单测可以推动产品质量转为优秀,推动实行它的过程更需要对它有真实的认识以及一套方法论。

我曾经认为,单元测试面向的是一个函数。任何走出一个函数的测试,都不是单元测试。 其实,对“单元”的定义取决于自己。如果你正在使用函数式编程,一个单元最有可能指的是一个函数。你的单元测试将使用不同的参数调用这个函数,并断言它返回了期待的结果;在面向对象语言里,下至一个方法,上至一个类都可以是一个单元(从一个单一的方法到一整个的类都可以是一个单元)。意图很重要(“意图”二字是本文中第一次提到,它很重要) 我们有单元测试、增量测试、集成测试、回归测试、冒烟测试等等,名字非常多。谷歌看到这种“百家争鸣”的现象,创立了自己的命名方式,只分为小型测试中型测试大型测试

小型测试:针对单个函数的测试,关注其内部逻辑,mock所有需要的服务。小型测试带来优秀的代码质量、良好的异常处理、优雅的错误报告

中型测试:验证两个或多个制定的模块应用之间的交互

大型测试:也被称为“系统测试”或“端到端测试”。大型测试在一个较高层次上运行,验证系统作为一个整体是如何工作的。

资源 小型测试 中型测试 大型测试
网络访问 仅访问localhost
数据库访问
访问文件
访问用户界面
使用外部服务 不鼓励,可mock
多线程
使用sleep语句
使用系统属性设置
运行时间限制(毫秒) 60 300 900+
强制时间限制(分钟) 1 5 15
小型测试 中型测试 大型测试
对应测试类型 单元测试 单元测试 + 逻辑层测试
(泛单元或分层测试)
UI 测试或接口测试

结论:我们的单元测试,既可以针对一个函数写case,也可以按照函数的调用关系串起来写case。

金字塔模型

在金字塔模型之前,流行的是冰淇淋模型。包含了大量的手工测试、端到端的自动化测试及少量的单元测试。造成的后果是,随着产品壮大,手工回归测试时间越来越长,质量很难把控;自动化case频频失败,每一个失败对应着一个长长的函数调用,到底哪里出了问题?单元测试少的可怜,基本没作用。

Mike Cohn 在他的着作《Succeeding with Agile》一书中提出了“测试金字塔”这个概念。这个比喻非常形象,它让你一眼就知道测试是需要分层的。它还告诉你每一层需要写多少测试。 测试金字塔本身是一条很好的经验法则,我们最好记住Cohn在金字塔模型中提到的两件事:

  • 编写不同粒度的测试
  • 层次越高,你写的测试应该越少

WHY?

从上图我们可以发现单测其实就是整个测试流程的基石,如果单测做的好,集成测试就相对顺利一些,进而自动化测试以及手工回归测试发现的那些低级错误就会少一些。

下面这张图,来自微软的统计数据:bug在单元测试阶段被发现,平均耗时3.25小时,如果漏到系统测试阶段,要花费11.5小时。

微软公司测试的数据统计结果
微软公司测试的数据统计结果

下面这张图,旨在说明两个问题:85%的缺陷都在代码设计阶段产生,而发现bug的阶段越靠后,耗费成本就越高,指数级别的增高。所以,在早期的单元测试就能发现bug,省时省力,一劳永逸,何乐而不为呢

测试中最容易出现缺陷的阶段
测试中最容易出现缺陷的阶段

单元测试特别耗时?

不能一刀切,不能只盯着单测阶段的耗时。 单测确实会增加开发量、增加开发时长。但是这是一个长期积累的过程,初始阶段肯定是痛苦的,但是长期来说提升交付效率与质量完全是OK的。

在《单元测试的艺术》这本书提到一个案例:找了开发能力相近的两个团队,同时开发相近的需求。进行单测的团队在编码阶段时长增长了一倍,从7天到14天,但是,这个团队在集成测试阶段的表现非常顺畅,bug量小,定位bug迅速等。最终的效果,整体交付时间和缺陷数,均是单测团队最少。

单测,存在即合理。一方面,需要把单测放在整个迭代周期来观测其效果;一方面,写单测也是技术活,写得好的同学,时间少代码质量高(也即,不是说写了单测,就能写好单测)

谁来写单测呢?

  • 开发同学写单测
  • 测试同学具有写单测的能力。重点在于开发脚手架、分层测试/端到端测试

增量还是存量

  • 单测case针对增量代码
  • 当存量代码出现大规模重构,后者质量暴露出极大风险时,都是推动补全单测的好时机

单元测试的阶段

一. 广义的单元测试,我们指这三部分的有机组合:

  • code review
  • 静态代码扫描
  • 单元测试用例编写

HOW?

注意下面的用法过于详细,收藏一下,便于以后查看。

如何做单元测试呢?就拿Java来说。编写Java单元测试用例,其实就是把“复杂的问题要简单化”——即把一段复杂的代码拆解成一系列简单的单元测试用例;写好Java单元测试用例,其实就是把“简单的问题要深入化”——即学习一套方法、总结一套模式并应用到实践中。这里,作者根据日常的工作经验,总结了一些Java单元测试技巧,以供大家交流和学习。

1. 准备环境

PowerMock是一个扩展了其它如EasyMockmock框架的、功能更加强大的框架。PowerMock使用一个自定义类加载器和字节码操作来模拟静态方法、构造方法、final类和方法、私有方法、去除静态初始化器等等。

1.1 引入PowerMock

为了引入PowerMock包,需要在pom.xml文件中加入下列maven依赖:

<dependency>
  <groupId>org.powermock</groupId>
  <artifactId>powermock-module-junit4</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.2 集成SpringMVC项目

在SpringMVC项目中,需要在pom.xml文件中加入JUnit的maven依赖:

<dependency>
  <groupId>junit</groupId>
  <artifactId>junit</artifactId>
  <version>4.13.2</version>
  <scope>test</scope>
</dependency>

1.3 集成SpringBoot项目

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
</dependency>

1.4 一个简单的测试用例

public class ListTest {
    @Test
    public void testSize() {
        Integer expected = 100;
        List list = PowerMockito.mock(List.class);
        PowerMockito.when(list.size()).thenReturn(expected);
        Integer actual = list.size();
        Assert.assertEquals("返回值不相等", expected, actual);
    }
}

2. mock语句

2.1 mock方法

声明: T PowerMockito.mock(Class clazz); 用途: 可以用于模拟指定类的对象实例。 当模拟非final类(接口、普通类、虚基类)的非final方法时,不必使用@RunWith@PrepareForTest注解。当模拟final类或final方法时,必须使用@RunWith和@PrepareForTest注解。注解形如: @RunWith(PowerMockRunner.class) @PrepareForTest({TargetClass.class})

  • 2.1.1 模拟非final类普通方法
@Getter
@Setter
@ToString
public class Rectangle implements Sharp {
    private double width;
    private double height;
    @Override
    public double getArea() {
        return width * height;
    }
}

public class RectangleTest {
    @Test
    public void testGetArea() {
        double expectArea = 100.0D;
        Rectangle rectangle = PowerMockito.mock(Rectangle.class);
        PowerMockito.when(rectangle.getArea()).thenReturn(expectArea);
        double actualArea = rectangle.getArea();
        Assert.assertEquals("返回值不相等", expectArea, actualArea, 1E-6D);
    }
}

2.1.2. 模拟final类或final方法

@Getter
@Setter
@ToString
public final class Circle {
    private double radius;
    public double getArea() {
        return Math.PI * Math.pow(radius, 2);
    }
}

@RunWith(PowerMockRunner.class)
@PrepareForTest(
{Circle.class})
public class CircleTest 
{
    @Test
    public void testGetArea() {
        double expectArea = 3.14D;
        Circle circle = PowerMockito.mock(Circle.class);
        PowerMockito.when(circle.getArea()).thenReturn(expectArea);
        double actualArea = circle.getArea();
        Assert.assertEquals("返回值不相等", expectArea, actualArea, 1E-6D);
    }
}

2.1 mock Static方法

声明: PowerMockito.mockStatic(Class clazz); 用途: 可以用于模拟类的静态方法,必须使用“@RunWith”和“@PrepareForTest”注解。

@RunWith(PowerMockRunner.class)
@PrepareForTest(
{StringUtils.class})
public class StringUtilsTest 
{
    @Test
    public void testIsEmpty() {
        String string = "abc";
        boolean expected = true;
        PowerMockito.mockStatic(StringUtils.class);
        PowerMockito.when(StringUtils.isEmpty(string)).thenReturn(expected);
        boolean actual = StringUtils.isEmpty(string);
        Assert.assertEquals("返回值不相等", expected, actual);
    }
}

3. spy语句

如果一个对象,我们只希望模拟它的部分方法,而希望其它方法跟原来一样,可以使用PowerMockito.spy方法代替PowerMockito.mock方法。于是,通过when语句设置过的方法,调用的是模拟方法;而没有通过when语句设置的方法,调用的是原有方法。

3.1. spy类

声明: PowerMockito.spy(Class clazz); 用途: 用于模拟类的部分方法。 案例:

public class StringUtils {
    public static boolean isNotEmpty(final CharSequence cs) {
        return !isEmpty(cs);
    }
    public static boolean isEmpty(final CharSequence cs) {
        return cs == null || cs.length() == 0;
    }
}

@RunWith(PowerMockRunner.class)
@PrepareForTest(
{StringUtils.class})
public class StringUtilsTest 
{
    @Test
    public void testIsNotEmpty() {
        String string = null;
        boolean expected = true;
        PowerMockito.spy(StringUtils.class);
        PowerMockito.when(StringUtils.isEmpty(string)).thenReturn(!expected);
        boolean actual = StringUtils.isNotEmpty(string);
        Assert.assertEquals("返回值不相等", expected, actual);
    }
}

3.2. spy对象

声明: T PowerMockito.spy(T object); 用途: 用于模拟对象的部分方法。 案例:

public class UserService {
    private Long superUserId;
    public boolean isNotSuperUser(Long userId) {
        return !isSuperUser(userId);
    }
    public boolean isSuperUser(Long userId) {
        return Objects.equals(userId, superUserId);
    }
}

@RunWith(PowerMockRunner.class)
public class UserServiceTest 
{
    @Test
    public void testIsNotSuperUser() {
        Long userId = 1L;
        boolean expected = false;
        UserService userService = PowerMockito.spy(new UserService());
        PowerMockito.when(userService.isSuperUser(userId)).thenReturn(!expected);
        boolean actual = userService.isNotSuperUser(userId);
        Assert.assertEquals("返回值不相等", expected, actual);
    }
}

4. when语句

4.1. when().thenReturn()模式

声明:

PowerMockito.when(mockObject.someMethod(someArgs)).thenReturn(expectedValue);
PowerMockito.when(mockObject.someMethod(someArgs)).thenThrow(expectedThrowable);
PowerMockito.when(mockObject.someMethod(someArgs)).thenAnswer(expectedAnswer);
PowerMockito.when(mockObject.someMethod(someArgs)).thenCallRealMethod();

用途: 用于模拟对象方法,先执行原始方法,再返回期望的值、异常、应答,或调用真实的方法。

4.1.1 返回值期望

public class ListTest {
    @Test
    public void testGet() {
        int index = 0;
        Integer expected = 100;
        List<Integer> mockList = PowerMockito.mock(List.class);
        PowerMockito.when(mockList.get(index)).thenReturn(expected);
        Integer actual = mockList.get(index);
        Assert.assertEquals("返回值不相等", expected, actual);
    }
}

4.1.2 返回期望异常

public class ListTest {
    @Test(expected = IndexOutOfBoundsException.class)
    public void testGet() 
{
        int index = -1;
        Integer expected = 100;
        List<Integer> mockList = PowerMockito.mock(List.class);
        PowerMockito.when(mockList.get(index)).thenThrow(new IndexOutOfBoundsException());
        Integer actual = mockList.get(index);
        Assert.assertEquals("返回值不相等", expected, actual);
    }
}

4.1.3 返回期望应答

public class ListTest {
    @Test
    public void testGet() {
        int index = 1;
        Integer expected = 100;
        List<Integer> mockList = PowerMockito.mock(List.class);
        PowerMockito.when(mockList.get(index)).thenAnswer(invocation -> {
            Integer value = invocation.getArgument(0);
            return value * 100;
        });
        Integer actual = mockList.get(index);
        Assert.assertEquals("返回值不相等", expected, actual);
    }
}

4.1.4 调用真实方法

public class ListTest {
    @Test
    public void testGet() {
        int index = 0;
        Integer expected = 100;
        List<Integer> oldList = new ArrayList<>();
        oldList.add(expected);
        List<Integer> spylist = PowerMockito.spy(oldList);
        PowerMockito.when(spylist.get(index)).thenCallRealMethod();
        Integer actual = spylist.get(index);
        Assert.assertEquals("返回值不相等", expected, actual);
    }
}

4.2. doReturn().when()模式

声明:

PowerMockito.doReturn(expectedValue).when(mockObject).someMethod(someArgs);
PowerMockito.doThrow(expectedThrowable).when(mockObject).someMethod(someArgs);
PowerMockito.doAnswer(expectedAnswer).when(mockObject).someMethod(someArgs);
PowerMockito.doNothing().when(mockObject).someMethod(someArgs);
PowerMockito.doCallRealMethod().when(mockObject).someMethod(someArgs);

用途: 用于模拟对象方法,直接返回期望的值、异常、应答,或调用真实的方法,无需执行原始方法。 注意: 千万不要使用以下语法:

PowerMockito.doReturn(expectedValue).when(mockObject.someMethod(someArgs));
PowerMockito.doThrow(expectedThrowable).when(mockObject.someMethod(someArgs));
PowerMockito.doAnswer(expectedAnswer).when(mockObject.someMethod(someArgs));
PowerMockito.doNothing().when(mockObject.someMethod(someArgs));
PowerMockito.doCallRealMethod().when(mockObject.someMethod(someArgs));

虽然不会出现编译错误,但是在执行时会抛出UnfinishedStubbingException异常。

4.2.1 返回期望值

public class ListTest {
    @Test(expected = IndexOutOfBoundsException.class)
    public void testGet() 
{
        int index = -1;
        Integer expected = 100;
        List<Integer> mockList = PowerMockito.mock(List.class);
        PowerMockito.doThrow(new IndexOutOfBoundsException()).when(mockList).get(index);
        Integer actual = mockList.get(index);
        Assert.assertEquals("返回值不相等", expected, actual);
    }
}

4.2.2. 返回期望异常

public class ListTest {
    @Test(expected = IndexOutOfBoundsException.class)
    public void testGet() 
{
        int index = -1;
        Integer expected = 100;
        List<Integer> mockList = PowerMockito.mock(List.class);
        PowerMockito.doThrow(new IndexOutOfBoundsException()).when(mockList).get(index);
        Integer actual = mockList.get(index);
        Assert.assertEquals("返回值不相等", expected, actual);
    }
}

4.2.3. 返回期望应答

public class ListTest {
    @Test
    public void testGet() {
        int index = 1;
        Integer expected = 100;
        List<Integer> mockList = PowerMockito.mock(List.class);
        PowerMockito.doAnswer(invocation -> {
            Integer value = invocation.getArgument(0);
            return value * 100;
        }).when(mockList).get(index);
        Integer actual = mockList.get(index);
        Assert.assertEquals("返回值不相等", expected, actual);
    }
}

4.2.4. 模拟无返回值

public class ListTest {
    @Test
    public void testClear() {
        List<Integer> mockList = PowerMockito.mock(List.class);
        PowerMockito.doNothing().when(mockList).clear();
        mockList.clear();
        Mockito.verify(mockList).clear();
    }
}

4.2.5. 调用真实方法

public class ListTest {
    @Test
    public void testGet() {
        int index = 0;
        Integer expected = 100;
        List<Integer> oldList = new ArrayList<>();
        oldList.add(expected);
        List<Integer> spylist = PowerMockito.spy(oldList);
        PowerMockito.doCallRealMethod().when(spylist).get(index);
        Integer actual = spylist.get(index);
        Assert.assertEquals("返回值不相等", expected, actual);
    }
}

4.3. 两种模式的主要区别

两种模式都用于模拟对象方法,在mock实例下使用时,基本上是没有差别的。但是,在spy实例下使用时,when().thenReturn()模式会执行原方法,而doReturn().when()模式不会执行原方法。 测试服务类:

@Slf4j
@Service
public class UserService {
    public long getUserCount() {
        log.info("调用获取用户数量方法");
        return 0L;
    }
}

使用when().thenReturn()模式:

@RunWith(PowerMockRunner.class)
public class UserServiceTest 
{
    @Test
    public void testGetUserCount() {
        Long expected = 1000L;
        UserService userService = PowerMockito.spy(new UserService());
        PowerMockito.when(userService.getUserCount()).thenReturn(expected);
        Long actual = userService.getUserCount();
        Assert.assertEquals("返回值不相等", expected, actual);
    }
}

在测试过程中,将会打印出"调用获取用户数量方法"日志。

使用doReturn().when()模式:

@RunWith(PowerMockRunner.class)
public class UserServiceTest 
{
    @Test
    public void testGetUserCount() {
        Long expected = 1000L;
        UserService userService = PowerMockito.spy(new UserService());
        PowerMockito.doReturn(expected).when(userService).getUserCount();
        Long actual = userService.getUserCount();
        Assert.assertEquals("返回值不相等", expected, actual);
    }
}

在测试过程中,不会打印出"调用获取用户数量方法"日志。

4.4. whenNew模拟构造方法

声明:

PowerMockito.whenNew(MockClass.class).withNoArguments().thenReturn(expectedObject);
PowerMockito.whenNew(MockClass.class).withArguments(someArgs).thenReturn(expectedObject);

用途: 用于模拟构造方法。 案例:

public final class FileUtils {
    public static boolean isFile(String fileName) {
        return new File(fileName).isFile();
    }
}

@RunWith(PowerMockRunner.class)
@PrepareForTest(
{FileUtils.class})
public class FileUtilsTest 
{
    @Test
    public void testIsFile() throws Exception {
        String fileName = "test.txt";
        File file = PowerMockito.mock(File.class);
        PowerMockito.whenNew(File.class).withArguments(fileName).thenReturn(file);
        PowerMockito.when(file.isFile()).thenReturn(true);
        Assert.assertTrue("返回值为假", FileUtils.isFile(fileName));
    }
}

注意:需要加上注解@PrepareForTest({FileUtils.class}),否则模拟方法不生效。

5. 参数匹配器

在执行单元测试时,有时候并不关心传入的参数的值,可以使用参数匹配器。

5.1. 参数匹配器(any)

Mockito提供Mockito.anyInt()Mockito.anyStringMockito.any(Class clazz)等来表示任意值。

public class ListTest {
    @Test
    public void testGet() {
        int index = 1;
        Integer expected = 100;
        List<Integer> mockList = PowerMockito.mock(List.class);
        PowerMockito.when(mockList.get(Mockito.anyInt())).thenReturn(expected);
        Integer actual = mockList.get(index);
        Assert.assertEquals("返回值不相等", expected, actual);
    }
}

5.2. 参数匹配器(eq)

当我们使用参数匹配器时,所有参数都应使用匹配器。 如果要为某一参数指定特定值时,就需要使Mockito.eq()方法。

@RunWith(PowerMockRunner.class)
@PrepareForTest(
{StringUtils.class})
public class StringUtilsTest 
{
    @Test
    public void testStartWith() {
        String string = "abc";
        String prefix = "b";
        boolean expected = true;
        PowerMockito.spy(StringUtils.class);
        PowerMockito.when(StringUtils.startsWith(Mockito.anyString(), Mockito.eq(prefix))).thenReturn(expected);
        boolean actual = StringUtils.startsWith(string, prefix);
        Assert.assertEquals("返回值不相等", expected, actual);
    }
}

5.3. 附加匹配器

MockitoAdditionalMatchers类提供了一些很少使用的参数匹配器,我们可以进行参数大于(gt)、小于(lt)、大于等于(geq)、小于等于(leq)等比较操作,也可以进行参数与(and)、或(or)、非(not)等逻辑计算等。

public class ListTest {
    @Test
    public void testGet() {
        int index = 1;
        Integer expected = 100;
        List<Integer> mockList = PowerMockito.mock(List.class);
        PowerMockito.when(mockList.get(AdditionalMatchers.geq(0))).thenReturn(expected);
        PowerMockito.when(mockList.get(AdditionalMatchers.lt(0))).thenThrow(new IndexOutOfBoundsException());
        Integer actual = mockList.get(index);
        Assert.assertEquals("返回值不相等", expected, actual);
    }
}

6. verify语句

验证是确认在模拟过程中,被测试方法是否已按预期方式与其任何依赖方法进行了交互。

格式:

Mockito.verify(mockObject[,times(int)]).someMethod(somgArgs);

用途:

用于模拟对象方法,直接返回期望的值、异常、应答,或调用真实的方法,无需执行原始方法。

案例:

6.1. 验证调用方法

public class ListTest {
    @Test
    public void testGet() {
        List<Integer> mockList = PowerMockito.mock(List.class);
        PowerMockito.doNothing().when(mockList).clear();
        mockList.clear();
        Mockito.verify(mockList).clear();
    }
}

6.2. 验证调用次数

public class ListTest {
    @Test
    public void testGet() {
        List<Integer> mockList = PowerMockito.mock(List.class);
        PowerMockito.doNothing().when(mockList).clear();
        mockList.clear();
        Mockito.verify(mockList, Mockito.times(1)).clear();
    }
}

times外,Mockito还支持atLeastOnceatLeastonlyatMostOnceatMost等次数验证器。

6.3. 验证调用顺序

public class ListTest {
    @Test
    public void testAdd() {
           List<Integer> mockedList = PowerMockito.mock(List.class);
        PowerMockito.doReturn(true).when(mockedList).add(Mockito.anyInt());
        mockedList.add(1);
        mockedList.add(2);
        mockedList.add(3);
        InOrder inOrder = Mockito.inOrder(mockedList);
        inOrder.verify(mockedList).add(1);
        inOrder.verify(mockedList).add(2);
        inOrder.verify(mockedList).add(3);
    }
}

6.4. 验证调用参数

public class ListTest {
    @Test
    public void testArgumentCaptor() {
        Integer[] expecteds = new Integer[] {123};
        List<Integer> mockedList = PowerMockito.mock(List.class);
        PowerMockito.doReturn(true).when(mockedList).add(Mockito.anyInt());
        for (Integer expected : expecteds) {
            mockedList.add(expected);
        }
        ArgumentCaptor<Integer> argumentCaptor = ArgumentCaptor.forClass(Integer.class);
        Mockito.verify(mockedList, Mockito.times(3)).add(argumentCaptor.capture());
        Integer[] actuals = argumentCaptor.getAllValues().toArray(new Integer[0]);
        Assert.assertArrayEquals("返回值不相等", expecteds, actuals);
    }
}

6.5. 确保验证完毕

Mockito提供Mockito.verifyNoMoreInteractions方法,在所有验证方法之后可以使用此方法,以确保所有调用都得到验证。如果模拟对象上存在任何未验证的调用,将会抛出NoInteractionsWanted异常。

public class ListTest {
    @Test
    public void testVerifyNoMoreInteractions() {
        List<Integer> mockedList = PowerMockito.mock(List.class);
        Mockito.verifyNoMoreInteractions(mockedList); // 执行正常
        mockedList.isEmpty();
        Mockito.verifyNoMoreInteractions(mockedList); // 抛出异常
    }
}

备注:Mockito.verifyZeroInteractions方法与Mockito.verifyNoMoreInteractions方法相同,但是目前已经被废弃。

6.6. 验证静态方法

Mockito没有静态方法的验证方法,但是PowerMock提供这方面的支持。

@RunWith(PowerMockRunner.class)
@PrepareForTest(
{StringUtils.class})
public class StringUtilsTest 
{
    @Test
    public void testVerifyStatic() {
        PowerMockito.mockStatic(StringUtils.class);
        String expected = "abc";
        StringUtils.isEmpty(expected);
        PowerMockito.verifyStatic(StringUtils.class);
        ArgumentCaptor<String> argumentCaptor = ArgumentCaptor.forClass(String.class);
        StringUtils.isEmpty(argumentCaptor.capture());
        Assert.assertEquals("参数不相等", argumentCaptor.getValue(), expected);
    }
}

7. 私有属性

7.1. ReflectionTestUtils.setField方法

在用原生JUnit进行单元测试时,我们一般采用ReflectionTestUtils.setField方法设置私有属性值。

@Service
public class UserService {
    @Value("${system.userLimit}")
    private Long userLimit;
    public Long getUserLimit() {
        return userLimit;
    }
}

public class UserServiceTest {
    @Autowired
    private UserService userService;
    @Test
    public void testGetUserLimit() {
        Long expected = 1000L;
        ReflectionTestUtils.setField(userService, "userLimit", expected);
        Long actual = userService.getUserLimit();
        Assert.assertEquals("返回值不相等", expected, actual);
    }
}

注意:在测试类中,UserService实例是通过@Autowired注解加载的,如果该实例已经被动态代理,ReflectionTestUtils.setField方法设置的是代理实例,从而导致设置不生效。

7.2. Whitebox.setInternalState方法

现在使用PowerMock进行单元测试时,可以采用Whitebox.setInternalState方法设置私有属性值。

@Service
public class UserService {
    @Value("${system.userLimit}")
    private Long userLimit;
    public Long getUserLimit() {
        return userLimit;
    }
}

@RunWith(PowerMockRunner.class)
public class UserServiceTest 
{
    @InjectMocks
    private UserService userService;
    @Test
    public void testGetUserLimit() {
        Long expected = 1000L;
        Whitebox.setInternalState(userService, "userLimit", expected);
        Long actual = userService.getUserLimit();
        Assert.assertEquals("返回值不相等", expected, actual);
    }
}

注意:需要加上注解@RunWith(PowerMockRunner.class)

8. 私有方法

8.1. 模拟私有方法

8.1.1. 通过when实现

public class UserService {
    private Long superUserId;
    public boolean isNotSuperUser(Long userId) {
        return !isSuperUser(userId);
    }
    private boolean isSuperUser(Long userId) {
        return Objects.equals(userId, superUserId);
    }
}

@RunWith(PowerMockRunner.class)
@PrepareForTest(
{UserService.class})
public class UserServiceTest 
{
    @Test
    public void testIsNotSuperUser() throws Exception {
        Long userId = 1L;
        boolean expected = false;
        UserService userService = PowerMockito.spy(new UserService());
        PowerMockito.when(userService, "isSuperUser", userId).thenReturn(!expected);
        boolean actual = userService.isNotSuperUser(userId);
        Assert.assertEquals("返回值不相等", expected, actual);
    }
}

8.1.2. 通过stub实现

通过模拟方法stub(存根),也可以实现模拟私有方法。但是,只能模拟整个方法的返回值,而不能模拟指定参数的返回值。

@RunWith(PowerMockRunner.class)
@PrepareForTest(
{UserService.class})
public class UserServiceTest 
{
    @Test
    public void testIsNotSuperUser() throws Exception {
        Long userId = 1L;
        boolean expected = false;
        UserService userService = PowerMockito.spy(new UserService());
        PowerMockito.stub(PowerMockito.method(UserService.class, "isSuperUser", Long.class)).toReturn(!expected);
        boolean actual = userService.isNotSuperUser(userId);
        Assert.assertEquals("返回值不相等", expected, actual;
    }
}

8.3. 测试私有方法

@RunWith(PowerMockRunner.class)
public class UserServiceTest9 
{
    @Test
    public void testIsSuperUser() throws Exception {
        Long userId = 1L;
        boolean expected = false;
        UserService userService = new UserService();
        Method method = PowerMockito.method(UserService.class, "isSuperUser", Long.class);
        Object actual = method.invoke(userService, userId);
        Assert.assertEquals("返回值不相等", expected, actual);
    }
}

8.4. 验证私有方法

@RunWith(PowerMockRunner.class)
@PrepareForTest(
{UserService.class})
public class UserServiceTest10 
{
    @Test
    public void testIsNotSuperUser() throws Exception {
        Long userId = 1L;
        boolean expected = false;
        UserService userService = PowerMockito.spy(new UserService());
        PowerMockito.when(userService, "isSuperUser", userId).thenReturn(!expected);
        boolean actual = userService.isNotSuperUser(userId);
        PowerMockito.verifyPrivate(userService).invoke("isSuperUser", userId);
        Assert.assertEquals("返回值不相等", expected, actual);
    }
}

这里,也可以用Method那套方法进行模拟和验证方法。

9. 主要注解

PowerMock为了更好地支持SpringMVC/SpringBoot项目,提供了一系列的注解,大大地简化了测试代码。

9.1. @RunWith注解

@RunWith(PowerMockRunner.class)

指定JUnit 使用 PowerMock 框架中的单元测试运行器。

9.2. @PrepareForTest注解

@PrepareForTest({ TargetClass.class })

当需要模拟final类、final方法或静态方法时,需要添加@PrepareForTest注解,并指定方法所在的类。如果需要指定多个类,在{}中添加多个类并用逗号隔开即可。

9.3. @Mock注解

@Mock注解创建了一个全部Mock的实例,所有属性和方法全被置空(0或者null)。

9.4. @Spy注解

@Spy注解创建了一个没有Mock的实例,所有成员方法都会按照原方法的逻辑执行,直到被Mock返回某个具体的值为止。

注意:@Spy注解的变量需要被初始化,否则执行时会抛出异常。

9.5. @InjectMocks注解

@InjectMocks注解创建一个实例,这个实例可以调用真实代码的方法,其余用@Mock@Spy注解创建的实例将被注入到用该实例中。

@Service
public class UserService {
    @Autowired
    private UserDAO userDAO;
    public void modifyUser(UserVO userVO) {
        UserDO userDO = new UserDO();
        BeanUtils.copyProperties(userVO, userDO);
        userDAO.modify(userDO);
    }
}

@RunWith(PowerMockRunner.class)
public class UserServiceTest 
{
    @Mock
    private UserDAO userDAO;
    @InjectMocks
    private UserService userService;
    @Test
    public void testCreateUser() {
        UserVO userVO = new UserVO();
        userVO.setId(1L);
        userVO.setName("changyi");
        userVO.setDesc("test user");
        userService.modifyUser(userVO);
        ArgumentCaptor<UserDO> argumentCaptor = ArgumentCaptor.forClass(UserDO.class);
        Mockito.verify(userDAO).modify(argumentCaptor.capture());
        UserDO userDO = argumentCaptor.getValue();
        Assert.assertNotNull("用户实例为空", userDO);
        Assert.assertEquals("用户标识不相等", userVO.getId(), userDO.getId());
        Assert.assertEquals("用户名称不相等", userVO.getName(), userDO.getName());
        Assert.assertEquals("用户描述不相等", userVO.getDesc(), userDO.getDesc());
    }
}

9.6. @Captor注解

@Captor注解在字段级别创建参数捕获器。但是,在测试方法启动前,必须调用MockitoAnnotations.openMocks(this)进行初始化。

@Service
public class UserService {
    @Autowired
    private UserDAO userDAO;
    public void modifyUser(UserVO userVO) {
        UserDO userDO = new UserDO();
        BeanUtils.copyProperties(userVO, userDO);
        userDAO.modify(userDO);
    }
}

@RunWith(PowerMockRunner.class)
public class UserServiceTest 
{
    @Mock
    private UserDAO userDAO;
    @InjectMocks
    private UserService userService;
    @Captor
    private ArgumentCaptor<UserDO> argumentCaptor;
    @Before
    public void beforeTest() {
        MockitoAnnotations.openMocks(this);
    }
    @Test
    public void testCreateUser() {
        UserVO userVO = new UserVO();
        userVO.setId(1L);
        userVO.setName("changyi");
        userVO.setDesc("test user");
        userService.modifyUser(userVO);
        Mockito.verify(userDAO).modify(argumentCaptor.capture());
        UserDO userDO = argumentCaptor.getValue();
        Assert.assertNotNull("用户实例为空", userDO);
        Assert.assertEquals("用户标识不相等", userVO.getId(), userDO.getId());
        Assert.assertEquals("用户名称不相等", userVO.getName(), userDO.getName());
        Assert.assertEquals("用户描述不相等", userVO.getDesc(), userDO.getDesc());
    }
}

9.7. @PowerMockIgnore注解

为了解决使用PowerMock后,提示ClassLoader错误。

总结一下

编码的尽头,就是不断的修复以往的bug,并产生新的bug,无限的循环往复。测试是开发中一个非常重要的方面,可以在很大程度上决定一个应用程序的命运。良好的测试在早期可以发现应用程序的问题,但是较差的测试往往总是导致故障或者停机。然而单元测试就可以比较早的发现代码中的问题,早发现,早解决,对于系统迭代来说,长远来看是可以提升效率的。

知其然,知其所以然,忙时做业绩,闲时修内功。

我是janker。 咱们下期见。

分类:

后端

标签:

Java

作者介绍

Janker
V1