JAVA

Mockito 学习笔记(推荐)

前言


因为单元测试的需要,经常会需要mock掉一些外部引用或者数据库接入等内容。接触过JMockit、Mockito、PowerMock等工具,但最终还是选择了Mockito。

相较之下。JMockit经常被认为是比较全面的,除了常见mock掉类暴露的方法,类内的私有方法,构造方法,静态方法,甚至用final修饰的方法都能很好的mock掉。

而Mockito相对了其他两种,其语法十分的易于理解,可读性很好。这也是我喜欢Mockito的原因,而PowerMock则类似于Mockito的超集,但Maven引入PowerMock的时候,也就自动引入了Mockito。

Mockito常被人认为功能不够强大,对于静态方法,构造方法等内容,需要借助PowerMock才能执行。但那已经是几年前的事情了。2016年推出了Mockito2.x,已经解决了这些问题。

Mockito的源码中注释十分全面。其org.mockito.Mockito类中注释了大量相关内容,仔细阅读对Mockito的使用很有帮助。

以下是个人对Mockito学习的一些简易总结,也是简单记录以供后续有需查阅。

1、mock公有方法


public class AppleTest {

	@Test
	public void test_mock_public_method() {

		Apple apple = mock(Apple.class);
		when(apple.getColor()).thenReturn("green");
		Assertions.assertEquals("green", apple.getColor());

	}
}

class Apple {

	public String getColor() {
		return "red";
	}

}

语法比较简单,创建mock对象,然后类似when…thenReturn…语法修改方法的返回,也可使用thenThrow抛出异常。

2、mock静态方法


public class AppleTest {

	@Test
	public void test_mock_static_method() {

		try (MockedStatic<Apple> mockedStatic = Mockito.mockStatic(Apple.class)) {

			when(Apple.getColor()).thenReturn("green");

			Assertions.assertEquals("green", Apple.getColor());

		}

	}

}

class Apple {

	public static String getColor() {

		return "red";

	}

}

官方建议是通过try-resouce语句包裹内容。因为采用的是ThreadLocal保存对静态方法的控制,try-resouce能保证mock使用结束后调用MockStatic.close()清除。也可调用Mockito.framework().clearInlineMocks(); 一次性清除全部Mock对象。

3、Mock构造方法


public class AppleTest {

	@Test
	public void test_mock_construction_method() {

		try (MockedConstruction<Banana> mockConstruction

				= mockConstructionWithAnswer(Banana.class, invocation -> "green")) {

			Apple apple = new Apple();

			Assertions.assertEquals("green", apple.getColor());

		}

	}

}

class Apple {

	public String getColor() {

		return new Banana().getColor();

	}

}

class Banana {

	public String getColor() {

		return "yellow";

	}

}

重要:普通的依赖注入,都是比较好mock的。但编写的代码里若是new了其他对象,就会比较难mock,mock构造函数可以解决这个问题,如果mock构造方法的对象行为比较复杂的话,也可以先提前构造mock对象来处理。

Banana mockBanana = mock(Banana.class);

when(mockBanana.getColor()).thenReturn("green");

try(MockedConstruction<Banana>mockConstruction

            = mockConstructionWithAnswer(Banana.class,

        invocation ->invocation.getMethod().invoke(mockBanana,invocation.getArguments()))){

    Apple apple = new Apple();

    Assertions.assertEquals("green",apple.getColor());

}

另一个mock构造函数的例子

final Foo foo = mock(Foo.class);
    MockedConstruction<Foo> mockedConstruction = mockConstructionWithAnswer(Foo.class, new Answer() {
        @Override
        public Object answer(InvocationOnMock invocation) throws Throwable {
            if (invocation.getMethod().equals(Foo.class.getMethod("getFooName"))) {
                return "mocked value";
            }
            return foo;
        }
    });
final Foo foo = mock(Foo.class);
    MockedConstruction<Foo> mockedConstruction = mockConstructionWithAnswer(Foo.class, new Answer() {
        @Override
        public Object answer(InvocationOnMock invocation) throws Throwable {
            // 当mock对象的方法被调用时,会进入到这个方法里面。通过判断方法名字,返回mock的value
            if (invocation.getMethod().getName().equals("getFooName"))) {
                return "mocked value";
            }
            return foo;
        }
    });
// mock当调用对象某个方法时出异常
final Foo foo = mock(Foo.class);
    MockedConstruction<Foo> mockedConstruction = mockConstructionWithAnswer(Foo.class, new Answer() {
        @Override
        public Object answer(InvocationOnMock invocation) throws Throwable {
            // 当mock对象的方法被调用时,会进入到这个方法里面。通过判断方法名字,返回mock的value
            if (invocation.getMethod().getName().equals("getFooName"))) {
                throw new java.lang.RuntimeException();
            }
            return foo;
        }
    });

4、Mock私有方法


这个其实是不支持的。截止当前最新的3.11.1版本。Mockito本身并不支持mock私有方法。其官方是有说明原因的,其最后一点:

    Finally... Mocking private methods is a hint that there is something wrong with Object Oriented understanding. In OO you want objects (or roles) to collaborate, not methods. Forget about pascal & procedural code.Think in objects.
    https://github.com/mockito/mockito/wiki/Mockito-And-Private-Methods

这一点还是比较认同的,如果需要mock私有方法,那大概率程序在面向对象的设计上存在问题。但其实这很不好说,说不定过两个版本Mockito就打脸了~

5使用@Mock/@InjectMocks注解


Mockito也提供了注解的方式来实现对依赖的打桩以及注入,也就是@Mock和@InjectMocks注解。

import org.mockito.InjectMocks; 
import org.mockito.Mock;  
import org.mockito.MockitoAnnotations;

	public class PortfolioTest {
		@InjectMocks
		Portfolio portfolio;
		@Mock
		StockService stockService;

		@BeforeEach
		public void setUp() {
			MockitoAnnotations.initMocks(this);
		}
	}
  • @Mock注解:Mockito 通过 @mock 注解来创建 mock 对象,可以用来代替Mockito.mock()方法。
  • @InjectMocks:创建一个实例,并将@Mock(或@Spy)注解创建的mock注入到用该实例中。

在使用了这两个注解之后,setup()方法也发生了变化。额外增加了以下这样一行代码。

	public void setUp() {
		MockitoAnnotations.initMocks(this);
	}
// 上面这个方法其实等价于这个
	public void setUp() {

		portfolio = new Portfolio();

		stockService = mock(StockService.class);

		portfolio.setStockService(stockService);

	}

也就是说,通过@Mock/@InjectMocks 以及initMocks方法的配合,Mockito实现了

  • @Mock将外部依赖StockService 进行了mock
  • @InjectMocks通过调用Portfolio类的无参构造方法完成了portfolio的实例化,并通过Portfolio类提供的setStockService()方法,用setter注入的方式,将前述被mock的stockService注入进portfolio。

6. mockito怎么mock一个类的私有属性


@InjectMocks
    private UserServiceImpl userService;

    @Test
    public void test() throws NoSuchFieldException{
        Field apiField = UserServiceImpl.class.getDeclaredField("username");
        // 已经为UserServiceImpl类的username属性成功设置值:1
        FieldSetter.setField(userService, apiField, "1");
     } 

补充说明


良好的架构设计下,掌握以上的内容,就可以写出覆盖率很高的测试案例了。但Mockito提供的不仅如此。

  • 调用Mockito的mock方法,创建的对象,默认是对其所有方法,原方法返回为基本类型的,则返回默认值,否则返回null。
public class AppleTest {

    @Test

    public void test_mock_and_spy_method(){

        Apple mockApple = mock(Apple.class);

        Apple spyApple = spy(Apple.class);

        Assertions.assertNull(mockApple.getColor());

        Assertions.assertEquals("red",spyApple.getColor());

    }

}

class Apple{

    public String getColor(){

        return "red";

    }

}

而spy方法则是对实际对象的mock,可以传入具体的对象,也可以如上代码传入类,当传入类时,则自动构造对象。对于没有mock操作的方法,spy出来的对象默认调用原方法。

  • 测试案例结束后,经常需要校验mock对象是否被正常调起。在Mockito1.x的时候,经常会在代码调用结束后,使用verify检查。另一方面,代码中存在大量的并不会被使用的mock对象,可能是从其他地方复制过来的,也可能一开始构思错误,使得测试代码并不简洁。
  • 但从Mockito2.1开始,mockito趋向严格。当使用
@ExtendWith(MockitoExtension.class)

或是Junit4的MockitoJunitRunner时,默认采用严格模式,当出现不必要的桩时,则抛出异常。

org.mockito.exceptions.misusing.UnnecessaryStubbingException:

Unnecessarystubbings detected.

Clean & maintainabletest code requires zero unnecessary code.

当然也可更改严格程度,但推荐还是应该保留测试代码的整洁性。

  • 当然即使有严格的校验mock对象被调起过,但实际使用的时候,有时会有一些更具体的验证。Mockito也提供了一系列方法。主要是下面这些:
// 验证调用了1次getColor方法

verify(apple,times(1)).getColor();

// 验证至少调用了1次getColor方法

verify(apple,atLeastOnce()).getColor();

// 验证从来没有调用过getWeight方法

verify(apple,never()).getWeight();

// 验证调用getColor方法耗时不超过100ms

verify(apple,timeout(100)).getColor();

补充例子:mockito测试final类/static方法/自己new的对象


先准备几个类,方便后面讲解:

public final class FinalSampleUtils {

    public static String foo() {
        return "aaa";
    }

    public static String bar(String a) {
        return "bar:" + a;
    }
}

这是一个final类,里面有2个static方法。

public class NewObject {

    public String haha() {
        return "haha";
    }
}

这是一个平淡无奇的类,没啥好说的。它俩的使用方式如下:

import org.springframework.stereotype.Service;

@Service
public class SampleServiceImpl implements SampleService {

    NewObject obj;

    public SampleServiceImpl() {
        obj = new NewObject();
    }

    @Override
    public void helloWorld() {
        String foo = FinalSampleUtils.foo();
        String bar = FinalSampleUtils.bar("test");

        System.out.println("hello1:" + foo);
        System.out.println("hello2:" + bar);
        System.out.println("h:" + obj.haha());
    }
}

这是一个普通的@Service实现类,但有2个注意的地方:

  • 里面用到的NewObject,并不是@Autowired之类由Spring注入的,而是自己new的
  • helloWorld里,使用了final类的静态方法,以及obj的普通方法。

在3.4以下的低版本mockito中,如果想mock helloWorld方法是很困难的,但在高版本中功能有所加强,参考下面的代码:

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.*;
import org.mockito.junit.MockitoJUnitRunner;

import static org.mockito.ArgumentMatchers.any;


@RunWith(MockitoJUnitRunner.class)
public class SampleServiceImplTest {

    @Mock
    NewObject obj;

    @InjectMocks
    SampleServiceImpl sampleService;

    @Test
    public void testHelloWorld() {
        MockedStatic<FinalSampleUtils> mocked = Mockito.mockStatic(FinalSampleUtils.class);

        //mock不带参数的static方法
        mocked.when(FinalSampleUtils::foo).thenReturn("bbb");

        //mock带参数的static方法
        mocked.when(() -> FinalSampleUtils.bar(any())).thenReturn("xxx");

        //mock代码中自己new的实例及“该实例的方法”
        MockedConstruction<NewObject> newObjectMocked = Mockito.mockConstruction(NewObject.class);
        Mockito.when(obj.haha()).thenReturn("who am i ?");

        sampleService.helloWorld();

    }
}

跑出来的效果如下:

hello1:bbb
hello2:xxx
h:who am i ?

从输出上看,不管是带参还是不带参的static方法,都成功mock,返回了mock后的值,而且自己new的对象,也同样mock成功了。

最后总结


项目中,有些函数需要处理某个服务的返回结果,而在对函数单元测试的时候,又不能启动那些服务,这里就可以利用Mockito工具,其中有如下三种注解:

  • @InjectMocks:创建一个实例,简单的说是这个Mock可以调用真实代码的方法,其余用@Mock(或@Spy)注解创建的mock将被注入到用该实例中。
  • @Mock:对函数的调用均执行mock(即虚假函数),不执行真正部分。
  • @Spy:对函数的调用均执行真正部分。

以上就是Mockito的主要玩法。其特征就是就是写出来的代码可读性比较高,易于理解。即使是未接触过Mockito的人,也能很快的理解每行代码的含义。当然,更详细的功能,看官方文档是最好的选择。

在学习Mockito的过程中,对其他方面也有一些思考。

  • 第一点是,以前就听说,Mockito功能上不支持静态方法,final方法等。即使现在去百度,或者谷歌搜索“如何用Mockito mock静态方法”,得到的结果也是让引入PowerMock,但其实这些功能在几年前已经在Mockito自身支持了。也许学习这些内容,官方文档是更好的选择。
  • 第二点是,Mockito只是工具。其实用JMockit也好,EasyMock也好,都只是共同达到一个方便测试的目的。而最终想要的,是想要写出一个真正能“跑完测试脚本之后,就能很放心认为这次的修改没有问题”的代码。如何设计测试案例,这是关键所在。而另一方面,一个类是否易于测试,是否在整体架构设计之时,就已经决定好了呢?