Springboot单元测试:SpyBeanvsMock

问题是什么?

如下是待测试类,期望测试TestServicetest方法,但是由于某种原因(下例中的doSomething)无法简单的被执行。
所以希望test方法真实执行,而为doSomething方法打桩。

package com.example.demo.service;

import com.example.demo.repositroy.TestRepository;
import org.springframework.stereotype.Component;

@Component
public class TestService {
    private final TestRepository testRepository;

    public TestService(TestRepository testRepository) {
        this.testRepository = testRepository;
    }

    public String doSomething(){
        //假装有复杂的无法执行的业务逻辑
        testRepository.doSomething();
        throw new RuntimeException();
    }

    public String test() {
        doSomething();
        //其他逻辑
        return "id";
    }
}
复制代码

如何解决?

使用MockBean

如果仅使用@MockBean将修饰的对象mock掉,这样TestServicedoSomething()方法就不再执行具体的细节,但是MockBean会将目标对象的所有方法全部mock,所以test不能真实地被执行,也就无法测试了。

when...thenCallRealMethod可达到部分mock的效果,仅test方法真实执行。

@SpringBootTest
@RunWith(SpringRunner.class)
public class TestServiceTest {
    @MockBean
    TestService testService;

    @Test
    public void test(){
        when(testService.test()).thenCallRealMethod();
        assertThat(testService.test(), equalTo("id"));
    }
}
复制代码

使用SpyBean

使用@SpyBean修饰的testService是一个真实对象,仅当doReturn("").when(testService).doSomething()时,doSomething方法被打桩,其他的方法仍被真实调用。

@SpringBootTest
@RunWith(SpringRunner.class)
public class TestServiceTest {
    @SpyBean
    TestService testService;

    @Test
    public void test(){
        doReturn("").when(testService).doSomething();
        assertThat(testService.test(), equalTo("id"));
    }
}
复制代码

小陷阱

与使用@MockBean不同,上节中调用doReturn("").when(testService).doSomething()doSomething方法被打桩。而when(testService.doSomething()).thenReturn("")则达不到此效果。原因是:使用@SpyBean修饰的testService是一个真实对象,所以testService.doSomething()会被真实调用。

Mockito官方文档上这样说:

Sometimes it's impossible or impractical to use when(Object) for stubbing spies. Therefore when using spies please consider doReturn|Answer|Throw() family of methods for stubbing. Example:

 List list = new LinkedList();
   List spy = spy(list);

   //Impossible: real method is called so spy.get(0) throws IndexOutOfBoundsException (the list is yet empty)
   when(spy.get(0)).thenReturn("foo");

   //You have to use doReturn() for stubbing
   doReturn("foo").when(spy).get(0);
复制代码

Mockito does not delegate calls to the passed real instance, instead it actually creates a copy of it. So if you keep the real instance and interact with it, don't expect the spied to be aware of those interaction and their effect on real instance state. The corollary is that when an unstubbed method is called on the spy but not on the real instance, you won't see any effects on the real instance.

Watch out for final methods. Mockito doesn't mock final methods so the bottom line is: when you spy on real objects + you try to stub a final method = trouble. Also you won't be able to verify those method as well.

概括一下:

  • 当使用spy时,考虑使用doReturn|Answer|Throw()
  • spy修饰的变量,Mockito会重新创建一个实例的copy,并不直接作用于真实实例
  • spy对final方法无效

SpyBean vs MockBean

SpyBeanMockBeanspring-boot-test包所提供的两个注解,用于Spy或Mock Spring容器所管理的实例。而Spy与Mock的方式正好相反,spy默认所有方法均真实调用,Mock默认所有方法均调用mock的实现。
使用场景上有什么区别呢?基于上例,虽然两者都能实现,但SpyBean更合适,因为上例在测试TestService,所以testService不应该是一个完全被mock的实例。而如果在TestService的测试用力中想要Mock TestRepository,使用MockBean就比较合适了。

为测试主体类部分打桩考虑使用SpyBean, 为外部依赖打桩,考虑使用MockBean

总结

  • SpyBeanMockBeanspring-boot-test包所提供的两个注解,用于Spy或Mock Spring容器所管理的实例;
  • 使用SpyBean或者Spy时,当需要对某个方法进行打桩时,需要注意一些使用限制;
  • 为测试主体类部分打桩考虑使用SpyBean,为外部依赖打桩,考虑使用MockBean