Mockito和Spock单测实战

一、背景

需求倒排,时间紧,任务重。很多测试都是集成测试,且需要启动Spring。

1.1、启动spring服务的集成测试

@RunWith(SpringRunner.class)
@SpringBootTest(classes = ApplicationLoader.class)
@Slf4j
public class BaseTest {
  // ---
  
}
public class MyServiceTest extends BaseTest {
    @Resource
    private MyService myService;
    
    @Test
    public void queryUserInfoTest() {
        Map<Long, Long> map = myService.func(323L));
        Assert.assertNotNull(map);
    }
}
  • 优点:可以一把debug到底2、节省了写单测时间
  • 缺点
    • 测试时,无论是run,还是debug,服务启动很慢
    • PR 流水线慢
    • 执行结果依赖外部环境,单测不稳定
      • 测试方法容易失败依赖服务部署在泳道,泳道机器被回收了,单测会失败
      • 测试方法,起不到验证作用依赖db|rpc数据,有数据时,走到A逻辑分支,没数据走到B分支。其他同学改了分支A的逻辑,但测试时,因为依赖的外部环境没数据,进而走到了分支B。单测正常,实际上这个单测跳过了对改动点的测试,没起到作用。

1.2、单元测试

@RunWith(MockitoJUnitRunner.class)
public class MyServiceTest {

    @InjectMocks
    private MyServiceImpl myService;

    @Mock
    private MyRepository myRepository;

    @Test
    public void queryUserInfoTest() {
        // 1.mock db查询
        when(myRepository.func(any())).thenReturn(Lists.newArrayList());
        // 2.真实调用
        List<UserInfo> result = myService.queryUserInfo(323L);
        // 3.判断
        Assert.assertTrue(CollectionUtils.isEmpty(result));
    }
}

好处:

  • 测试时秒级启动

  • 大大缩短PR时间

  • 不依赖外部环境,每次执行结果都唯一

    其他同学改了分支A的逻辑,单测方法可验证到分支A,若改动点不符合测试断言,测试方法会失败,自然会引起关注,取排查失败原因。

缺点:

需要写不少单元测试代码(测试代码,基本没啥复杂逻辑,代码量虽多,但是写单测耗时不会太多)

3、补充:单测使用单元测试,可以使用集成测试做全流程debug

二、单测规范

2.1 命名

内容JunitSpock
命名类名待测类XxxService测试类为XxxTestXxxSpec
方法名称待测方法名createTasktestCreateTask_{场景描述}1、测试方法,参数异常testCreateTask_paramError2、测试方法,参数正常testCreateTask_paramSuccess3、测试方法正常testCreateTask_successtest createTask 场景描述

2.2单元测试

内容备注
单元测试最小单位是一个方法一个测试类只能对应一个被测类一个测试方法只测试一个方法在指定类中,Shift + command + T快捷键打开此类对应的测试类
避免连接资源需要mock,保证测试方法,每次执行结果都相同数据库rpc调用中间件:Kafka、ExecutorService等

2.3 有效断言

1、常见断言方法:org.junit.Assert

方法名称使用
assertTrueAssert.assertTrue(CollectionUtils.isNotEmpty(skuIdList));
assertEqualsAssert.assertEquals(resp.getCode(), 0);
assertNotNullAssert.assertNotNull(resp);
assertThat(T actual, Matcher<? super T> matcher)其中Matcher见下文org.hamcrest.CoreMatchers

2、常用Matcher(均为org.hamcrest包下类)

  • Matchers:方法更全
  • CoreMatchers: 常用方法
@Test
public void testC() {
  
  //boolean:不为true则抛出对应错误信息
  boolean result = false;
  Assert.assertTrue("测试失败~",result);

    // 一般匹配符
    int s = new C().add(1, 1);
    // allOf:所有条件必须都成立,测试才通过
    assertThat(s, allOf(greaterThan(1), lessThan(3)));
    // anyOf:只要有一个条件成立,测试就通过
    assertThat(s, anyOf(greaterThan(1), lessThan(1)));
     assertThat(s, anyOf(is(1),is(2),is(3));
    // anything:无论什么条件,测试都通过
    assertThat(s, anything());
    // is:变量的值等于指定值时,测试通过
    assertThat(s, is(2));
    // not:和is相反,变量的值不等于指定值时,测试通过
    assertThat(s, not(1));

    // 数值匹配符
    double d = new C().div(10, 3);
    // closeTo:浮点型变量的值在3.0±0.5范围内,测试通过
    assertThat(d, closeTo(3.0, 0.5));
    // greaterThan:变量的值大于指定值时,测试通过
    assertThat(d, greaterThan(3.0));
    // lessThan:变量的值小于指定值时,测试通过
    assertThat(d, lessThan(3.5));
    // greaterThanOrEuqalTo:变量的值大于等于指定值时,测试通过
    assertThat(d, greaterThanOrEqualTo(3.3));
    // lessThanOrEqualTo:变量的值小于等于指定值时,测试通过
    assertThat(d, lessThanOrEqualTo(3.4));

    // 字符串匹配符
    String n = new C().getName("Magci");
    // containsString:字符串变量中包含指定字符串时,测试通过
    assertThat(n, containsString("ci"));
    // startsWith:字符串变量以指定字符串开头时,测试通过
    assertThat(n, startsWith("Ma"));
    // endsWith:字符串变量以指定字符串结尾时,测试通过
    assertThat(n, endsWith("i"));
    // euqalTo:字符串变量等于指定字符串时,测试通过
    assertThat(n, equalTo("Magci"));
    // equalToIgnoringCase:字符串变量在忽略大小写的情况下等于指定字符串时,测试通过
    assertThat(n, equalToIgnoringCase("magci"));
    // equalToIgnoringWhiteSpace:字符串变量在忽略头尾任意空格的情况下等于指定字符串时,测试通过
    assertThat(n, equalToIgnoringWhiteSpace(" Magci   "));

    // 集合匹配符
    List<String> l = new C().getList("Magci");
    // hasItem:Iterable变量中含有指定元素时,测试通过
    assertThat(l, hasItem("Magci"));

    Map<String, String> m = new C().getMap("mgc", "Magci");
    // hasEntry:Map变量中含有指定键值对时,测试通过
    assertThat(m, hasEntry("mgc", "Magci"));
    // hasKey:Map变量中含有指定键时,测试通过
    assertThat(m, hasKey("mgc"));
    // hasValue:Map变量中含有指定值时,测试通过
    assertThat(m, hasValue("Magci"));
}

三、Mockito

3.1pom

mdp集成了mockito的2.15.0版本,无需添加依赖

<!-- https://mvnrepository.com/artifact/org.mockito/mockito-core -->
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>4.3.1</version>
    <scope>test</scope>
</dependency>

3.2常用注解

3.2.1 @RunWith

1、作用

是一个运行器告诉Junit使用什么进行测试

2、使用:【推荐】

@RunWith(MockitoJUnitRunner.class)
public class BaseTest{
    @Test
    public void test(){
        
    }
}

3、作用

用MockitoJUnitRunner进行测试@RunWith(PowerMockRunner.class) 则使用PowerMockRunner进行测试

4、对比:【不推荐使用】

@RunWith(SpringRunner.class)

@RunWith(SpringRunner.class)
@SpringBootTest(classes = ApplicationLoader.class)
public class BaseTest {
    @Test
    public void test(){
        
    }
}

作用:

@RunWith(SpringRunner.class)Test测试类要使用注入的类,比如通过@Resource注入的类,有此注解,这些类才能实例化到spring容器中,自动注入才能生效,否则就是NullPointerExecption

  • 不使用@RunWith(SpringRunner.class) - NPE
@SpringBootTest(classes = ApplicationLoader.class)
public class BaseTest {
    @Resource
    private MyService myService;//null

    @Test
    public void new_test(){
        myService.func(323L);//npe
    }
  
}
  • 使用@RunWith(SpringRunner.class) - 正常注入bean
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ApplicationLoader.class)
public class BaseTest {
  
  	@Resource
    private MyService myService;//SellOutProcessServiceImpl@1952
  
    @Test
    public void test(){
       myService.func(323L);//正常调用
    }
}

@SpringBootTest

针对SpringBoot的测试类,结合@RunWith(SpringRunner.class)注解。加载ApplicationContext,启动spring容器。在测试开始的时候自动创建Spring的上下文

3.2.2 @InjectMocks

1、作用

@InjectMocks生效的类A下,可通过@Mock类中指定类|接口,将其变成Mock对象,注入到A中

2、使用

@InjectMocks 结合 @Mock

@RunWith(MockitoJUnitRunner.class)
public class MyServiceTest {

    @InjectMocks
    private MyServiceImpl myService;

    @Mock
    private MyRepository myRepository;

    @Test
    public void testQueryInfo_success() {
        when(myRepository.queryInfo(any())).thenReturn(Lists.newArrayList());
        resp = myService.func(323L);
        Assert.assertEquals(resp.getCode(), 0);
    }
}

3、备注

@InjectMocks只能作用在类上,接口不行

3.2.3 @Mock注解

1、作用

通过@InjectMocks注解标注,创建一个实例实例中,通过@Mock注解创建的的mock对象,将被注入到该实例中

2、使用:同上

3、备注

@Mock注解,作用等效mock()方法

3.2.4 @Spy

1、作用

@Spy声明的对象,对函数的调用均执行方法的真实调用

2、使用

场景1:使用MdpBeanCopy对象,走真实方法

  • MyConvertor
@BeanCopy
public interface MyConvertor {
    Target copy(Source source);
}
  • 业务
@Service
@Slf4j
public class MyService {
    @Resource
    private MyConvertor myConvertor;

    public List<Long> func(Source req) {

        // 使用BeanCopy进行数据转换 ,单测时,想走真实的copy方法
        Target target = myConvertor.copy(req);
        
       //----
    }
}
  • @Spy
@RunWith(MockitoJUnitRunner.class)
public class MyServiceTest {

    @InjectMocks
    private MyServiceImpl myService;

    @Spy
    private MyConvertor myConvertor = new MyConvertorImpl();//这里必须new接口的实现类

    @Test
    public void testQueryInfo_success() {
        //调用queryInfo时,myConvertor.copy()方法,走真实的方法逻辑
        myService.func();
    }
}

场景2:走真实线程池

  • 使用myExecutor,并发查询
public List<DTO> concurrentQueryData(
                              String date, Long id, List<Long> skuIdList) {
   return Lists.partition(skuIdList, 100).stream()
            .distinct()
            .map(partSkuList -> CompletableFuture.supplyAsync(() ->
                          queryData(date, id, partSkuList),myExecutor))
            .collect(Collectors.toList())
            .stream()
            .map(CompletableFuture::join)
            .flatMap(List::stream)
            .collect(Collectors.toList());
}
  • @Spy
@RunWith(MockitoJUnitRunner.class)
public class MyServiceTest {

    @InjectMocks
    private MyService myService;

    @Spy
    private ExecutorService myExecutor = Executors.newFixedThreadPool(1);//必须创建一个

    @Test
    public void testConcurrentQueryData() {
        //调用concurrentQueryData时,会使用myExecutor线程池,核心线程数为1
        myService.concurrentQueryData("2021-03-23", 1L ,Lists.newArrayList(1L));
    }
}
3.2.5@Before

1、作用

在启动@Test之前,先执行此方法。

2、使用

可以通过Tracer将operator信息,set到组件中

  • 业务校验operator代码
@Service
public class MyService {

   public MyResp editConfig(MyReq req) {
    MyResp resp = new MyResp();
    //01、权限校验
    String operator = getOperator();
    if (StringUtils.isBlank(operator)) {
        resp.setCode(4000);
        resp.setMessage("operator为空~");
        return resp;
    }
       
    req.setMisId(operator);
  
    // ---
   } 
}
  • @Before
@RunWith(MockitoJUnitRunner.class)
public class MyServiceTest {

    @InjectMocks
    private MyService myService;

    @Before
		public void setUp() throws Exception {
   			Tracer.clearContext("mall.sso.user");
		}

		@Test
		public void editConfig() {
   			Tracer.putContext("mall.sso.user", "{\"login\": \"zhangsan\"}");
   			myService.editConfig(req);
		}
}
3.2.6 @Ignore

1、作用:忽略此测试方法

2、使用

@Ignore
@Test
public void test() {
   //---
}

3、备注

作用在测试类上,则整个类中的测试方法均被忽略

3.3常用方法

3.3.1 参数相关-any ()

1、作用

使用any(),填充mock的方法参数

2、使用

  • 业务
List<Long> func(String date, Integer type);
  • mock方法中,使用any()
when(myService.func(any(), any())).thenReturn(Lists.newArrayList());

3、备注

  • 待mock对象的方法声明中,有几个参数,则when()调方法时,填写几个any()
  • 如果待mock方法的入参为基本类型,比如int,就必须使用anyInt()
public boolean setNx(String key, String value, 
                     int expireTime, String redisCategory) {        
}
//方法第3个参数为int类型,则mock时必须用anyInt
when(redisGateway.setNxNew(any(), any()
      , anyInt(), any())).thenReturn(Boolean.TRUE);
  • mock时,可根据不同入参,返回不同结果
when(myService.str2Int("m")).thenReturn(94);
when(myService.str2Int("w")).thenReturn(93);

Integer m = myService.str2Int("m");
Integer w = myService.str2Int("w");
Integer other = myService.str2Int("x");

Assert.assertThat(m, equalTo(93));
Assert.assertThat(w, equalTo(93));
Assert.assertThat(other, equalTo(0)); 
3.3.2 Mock使用-mock()

1、作用

等效@Mock

2、使用

  • 业务
@Service
public class MyService{
 	 @Resource
    private MyRepository myRepository;
  
    public List<Long> func(Long id) {
      return myRepository.queryInfo(id);
    }
}
  • UT
@RunWith(MockitoJUnitRunner.class)
public class MyServiceTest {

    @InjectMocks
    private MyService myService;

    private MyRepository myRepository = mock(MyRepository.class);
  
    @Test
    public void test() {
        when(myRepository.queryInfo(any())).thenReturn(Lists.newArrayList());
        List<DTO> result = myService.func(1L);
    }
}

3、备注

  • 等效1
@Mock
private MyRepository myRepository;
  • 等效2
private MyRepository myRepository = mock(MyRepository.class, "myService");

将myRepository,mock注入myService

3.3.3 mockStatic()

1、作用

mock 静态方法

2、使用

MockedStatic<MyLion> myLion = mockStatic(MyLion.class);

3、备注

  • 可用于mock静态方法。MyLion本身的方法都是静态方法,所以,也可用于Mock MyLion
  • StringUtil等静态方法
  • 后面有mock-static
3.3.4 when()

1、作用

打桩

2、使用

when(myRepository.queryInfo(any(), any(), any()))
                                  .thenReturn(buildData());

3、备注

1)是否通过thenReturn指定返回值

  • 正常when(myService.queryName(any())).thenReturn(“hello”);

    返回"hello"

  • 不指定thenXxx()时,则方法返回值为其类型的默认。when(myService.queryName(any())).thenReturn(“hello”);

    返回String的默认类型:null

  • 同理when、thenReturn都不写,方法返回值为其类型的默认。when(pcGateway.queryName(any())).thenReturn(“hello”); 返回String的默认类型:null

2)selectByExample方法,默认返回空集合

//不指定thenReturn(结果),返回的就是selectByExample返回其List<T>类型的默认值:空集合
when(myMapper.selectByExample(any()));
List<XxxDO> result = myRepository.func(model);//空集合
  • 方法声明返回Obj时,则返回默认null

    业务

    @Service
    public class MyService {
    		//queryUser方法,返回Object
        public MyUser queryUser() {
            return new MyUser();
        }
    }
    

    指定thenReturn,正常返回

    UT

    when(myService.queryUser(any())).thenReturn(new MyUser());
    
    • 不指定thenReturn,返回null
    when(myService.queryUser(any()));
    
    • 方法声明返回Integer时,则返回默认0
    • 方法声明返回String时,则返回默认null
3.3.5 reset()

1、作用

重置mock的对象

2、使用

  • 业务
public class MyRepository {
  
    @Resource
    private MyMapper myMapper;
  
    public List<XxxDO> func() {
       // ---
       return myMapper.selectByExample(xxxExample);
    }
}
  • 不使用reset
@RunWith(MockitoJUnitRunner.class)
public class MyRepositoryTest {
    @InjectMocks
    private MyRepository myRepository;

    @Mock
    private MyMapper myMapper;

    @Test
    public void test() {
        //场景1
        when(myRepository.selectByExample(any()))
                          .thenReturn(Lists.newArrayList(null));
        List<SellOutProcessPlanDO> result1 = myRepository.func();

        //场景2: 获取的selectByExample结果 和 场景1一样。同一个mock对象myRepository
        List<XxxDO> result = myRepository.func();
    }
}
  • 使用reset
@RunWith(MockitoJUnitRunner.class)
public class MyRepositoryTest {
    @InjectMocks
    private MyRepository myRepository;

    @Mock
    private MyMapper myMapper;

    @Test
    public void test() {

        //场景1,selectByExample的mock结果为含有XxxDO元素的集合
        when(myMapper.selectByExample(any()))
                           .thenReturn(Lists.newArrayList(XxxDO));
        List<XxxDO> result1 = myRepository.func();

        //场景2,因为reset了。selectByExample的mock结果不再是含有XxxDO元素的集合,而是空集合
        reset(myMapper)
        List<XxxDO> result = myRepository.func();
    }
}
3.3.6 mock方法返回值-doNothing()

1、作用

mock返回值方法即void方法

2、使用

  • 业务
@Service
public class MyService{
    public void func(Long id) {
       // 
    }
}
  • UT
doNothing().when(myService).func(any());
3.3.7 thenReturn()

1、作用

mock返回值方法的结果

2、使用

when(myMapper.countByExample(any())).thenReturn(1);

3备注

每一次调用mock方法,返回不同结果【用于while、for循环调用mock】

//为list.size()方法赋值:第一次调用为1,第二次调用为2、、
when(list.size()).thenReturn(1)
                 .thenReturn(2)
  							 .thenReturn(3)
                 .thenReturn(4);

assertEquals(1,list.size());//第一次调用,值为1
assertEquals(2,list.size());
assertEquals(3,list.size());
assertEquals(4,list.size());//第四次为4
assertEquals(4,list.size());//第五-N次,同样为4

或
指定mock方法的入参,对应mock结果
when(myRepository.countSku(123456789L)).thenReturn(1L);
when(myRepository.countSku(987654321L)).thenReturn(2L);
3.3.8 doReturn()

1、作用

作用同理thenReturn(),只是使用方式不同

2、使用

  • doReturn
doReturn(1)
  .when(myMapper)
  .countByExample(any());
  • thenReturn
when(myMapper.countByExample(any()))
  .thenReturn(1);

3、备注

doReturn 和 thenReturn大部分情况下都是可以相互替换的。除非特殊mock

3.3.9 doThrow()和thenThrow()

1、作用

mock方法,让其返回异常

2、使用

  • void 方法

业务

@Service
public class MyService {
    @Resource
    private MyRepository myRepository;
    public void query() {
        myRepository.queryInfo(1L);
    }
}

@Repository
public class MyRepository {
    public void queryInfo(Long skuId) {
    }
}

UT

@InjectMocks
private MyService myService;
    
@Mock
private MyRepository myRepository;
 
@Test
public void test(){
    doThrow(IllegalArgumentException.class)
  	   .when(myRepository)
  	   .queryInfo(any());

    try {
        myService.func();
     } catch (Exception e) {
       Assert.assertThat(e,instanceOf(IllegalArgumentException.class));
     } 
}
  • 有返回值方法

业务

@Service
public class MyService {
    @Resource
    private MyRepository myRepository;
    public Integer func() {
        return myRepository.queryInfo(1L);
    }
}

@Repository
public class MyRepository {
    public void queryInfo(Long skuId) {
    }
}

UT

@InjectMocks
private MyService myService;
    
@Mock
private MyRepository myRepository;
 
@Test
public void test(){
    when(myRepository.queryInfo(any()))
    .thenThrow(IllegalArgumentException.class);

	  try {
       myService.func();
	  } catch (Exception e) {
       Assert.assertThat(e,instanceOf(IllegalArgumentException.class));
	  }
}

3、备注

异常类型,自己指定

3.3.10 thenCallRealMethod()

1、作用

  • 走方法真实的代码逻辑
  • when()调用方法的时候,不是thenReturn()方法结果,而是走方法的实际代码逻辑

2、使用

  • 业务
@Service
public class MyService {
    @Resource
    private MyRepository myRepository;
  
    public Integer func() {
        return myRepository.queryInfo(1);
    }
}

@Repository
public class MyRepository {
    public Integer queryInfo(Integer sum) {
        return sum + 1;
    }
}
  • UT
@InjectMocks
private MyService myService;
    
@Mock
private MyRepository myRepository;
 
@Test
public void test(){
    //这里在调用queryInfo方法时,会走实际的代码逻辑,方法返回值为:sum + 1即 1+1=2
		when(myRepository.queryInfo(any())).thenCallRealMethod();
		Integer res = myService.func();
		Assert.assertThat(res, is(2));
}

3、备注

  • 作用同理@Spy注解

业务

@Service
public class MyService {
    @Resource
    private MyRepository myRepository;
  
    public String func(Integer id) {
        return myRepository.queryInfo(id);
    }
}

@Repository
public class MyRepository {
    public String queryInfo(Integer id){
        if (id < 0) {
            throw new IllegalArgumentException("参数不合法");
        }
        return "" + id;
    }
}

UT

@Spy
private MyRepository myRepository;

@Test
public void t() {
   try {
     //当func方法调用queryInfo时,
     String result = myService.func(-1);
     //会走queryInfo的真实方法,方法入参为-1,小于0,会抛异常
    } catch (Exception e) {
        Assert.assertThat(e, instanceOf(IllegalArgumentException.class));
    }
}
3.3.11 verify()

1、作用

验证,验证是否执行了mock方法

2、使用

@RunWith(MockitoJUnitRunner.class)
public class MyServiceTest {
    @InjectMocks
    private MyService myService;

    @Mock
    private MyRepository myRepository;

    @Test
    public void t() {
      when(myRepository.queryInfo(any())).thenReturn("hello");
      myService.func(-1);
      //验证myRepository的queryInfo方法,是否被mock执行了1次
      verify(myRepository, times(1)).queryInfo(any());
    }
}

3、备注

必须调用了mock方法,触发了其执行,才能使用verift判断。否则会报错:Wanted but not invoked:

when(myRepository.queryInfo(any())).thenReturn("hello");
//myService.func(-1); 将此方法注释掉后,导致:
//不会调用myRepository的queryInfo方法,会报错Wanted but not invoked
verify(myRepository, times(1)).queryInfo(any());

四、Spock

4.1环境配置

  • 插件:IDEA安装插件:spock framework Enhancements
  • pom
<dependency>
  <groupId>org.spockframework</groupId>
  <artifactId>spock-core</artifactId>
  <version>1.3-groovy-2.4</version>
  <scope>test</scope>
</dependency>
   
<dependency>
  <groupId>org.spockframework</groupId>
  <artifactId>spock-spring</artifactId>
  <version>1.3-groovy-2.4</version>
  <scope>test</scope>
</dependency>

4.2创建spock测试类

在指定类下,shift + command + T, 选择Spock创建,选择浅绿色的Spock图标,创建

4.3 类

1、使用

写法1【推荐】

class MyServiceSpec extends Specification {
    MyService myService = new MyService()
    MyRepository myRepository = Mock()

    def setup() {
        myService.myRepository = myRepository
    }
}

写法2:不用setup

  • 业务
@Service
public class MyService {
    @Resource
    private MyRepository myRepository;
    @Resource
    private MyGateway myGateway;

    public void func1(String date, Long skuId) {
        myRepository.querySku(date, skuId);
        myGateway.queryRpc(1);
    }
}
  • UT
class MyServiceSpec extends Specification {
    MyRepository myRepository = Mock()
    MyGateway myGateway = Mock()
    MyService myService = new MyService(
               myRepository:myRepository, 
               myGateway: myGateway)

    def "test"() {
        given:
        myRepository.querySku(_, _) >> {1L}
        myGateway.queryRpc(_) >> {1L}

        when:
        myService.func1("03-26", 123L)

        then:
        noExceptionThrown()
    }
}

2、作用

1MyService myService = new MyService()
类似Mockito@InjectMock
private MyService myService;**//标注测试待测试类**

    
2MyRepository myRepository = Mock()
类似Mockito@Mock
private MyRepository myRepository;**//标注需要mock的对象**

3)def setup() { 
	myService.myRepository = myRepository    
}

类似 Mockito中的@Before,都是做测试功能的前置设置。

这里是将依赖的mock对象myRepository的引用,set到测试类myService中。没有这步,myRepository会报npe。 

3、备注

1)setup 方法 和 given的区别

  • myService.myRepository = myRepository,放在setup方法中mock的对象类似全局变量,不同测试方法都可以使用
class MyServiceSpec extends Specification {
    MyService myService = new MyService()
    MyRepository myRepository = Mock()
    def setup() {
        myService.myRepository = myRepository
    }

    def "test1"() {
        given:
        // ---
        myRepository.countSku(_) >> 1L

        when:
        // --
        then:"结果验证"
        // --
    }
  
    def "test2"() {
        given:
        // ---
        myRepository.selectSKuDOs(_) >> 2L

        when:
        // --
        then:"结果验证"
        // --
    }
}
  • 放在某个测试方法的given标签后,类似局部变量,只能此测试方法单独使用

4.4 方法 -标签

4.4.1 given

1、作用

输入条件提前定义要准备的方法参数mock模拟依赖方法的返回值

2、使用

  • 业务
@Service
public class MyService {
    @Resource
    private MyRepository myRepository;
  
    public Long func(MyUser myUser) {
        return myRepository.queryRepository(myUser);
    }
}

@Repository
public class MyRepository {
    public Long queryRepository(MyUser myUser) {
        // ---
    }
}
  • UT
class MyServiceSpec extends Specification {
    MyService myService = new MyService()
    MyRepository myRepository = Mock()

    def setup() {
        myService.myRepository = myRepository
    }

    def "test"() {
        given:"前提条件"
        MyUser user = new MyUser()
        user.setId(323L)
        
        and:
        myRepository.queryRepository(_) >> {777L}

        when:"执行"
        Long result = myService.func(user)

        then:"结果验证"
        Assert.assertEquals(result, 777L)
    }
}

3、备注

1)given必须作为第一个标签出现

2)添加标签描述(非强制)

如when:“获取信息”,说明这块代码的作用。

3)and:衔接上个标签,补充的作用。

given 可以结合 and一起使用,结构更清晰given: “参数定义”

and: “mock 需要调用的方法返回”

def "test "() {
       given:"参数定义"
       def user = new MyUser(rdcId: 323L)
        
       and:"mock方法"
        myRepository.queryRepository1(_) >> {777L}
  			myRepository.queryRepository2(_,_) >> {"zhangsan"}
 				pcGateway.queryRpc(_,_,_) >> {Lists.newArrayList()}

        when:"执行"
        Long result = myService.func(user)

        then:"结果验证"
        Assert.assertEquals(result, 777L)
}

4)参数命名,以下都可以

  • def user = new MyUser()

  • MyUser user = new MyUser()

  • def MyUser user = new MyUser()

  • 创建对象,建议使用第一种。因为类名较长,创建很多对象时,代码看起来多、长

    when中触发执行方法返回结果,建议使用第二种,这样在then中对方法结果判断时,能够一眼知道返回值的类型

5)属性赋值

  • MyUser
@Data
public class MyUser{
    private String name;
    private Long age;
    private List<Integer> other;
}
  • UT属性赋值
def user = new MyUser(name:"zhangsan", age:2L,other:[1,2])
或
def user = new MyUser()
user.setName("zhangsan")
user.setAge(2L)
4.4.2 when

1、作用

触发动作,指定做什么,是真实调用

2、使用

class MyServiceSpec extends Specification {
    MyService myService = new MyService()
    MyRepository myRepository = Mock()

    def setup() {
        myService.myRepository = myRepository
    }

    def "test "() {
        given:"前提条件"
        def user = new MyUser(id: 323L)
        
        and:"mock"
        myRepository.queryRepository(_) >> {777L}

        when:"执行"
        Long result = myService.func(user)

        then:"结果验证"
        Assert.assertEquals(result, 777L)
    }
}
4.4.3 then

1、作用

验证结果输出

2、使用

class MyServiceSpec extends Specification {
    MyService myService = new MyService()
    MyRepository myRepository = Mock()

    def setup() {
        myService.myRepository = myRepository
    }

    def "test "() {
        given:"前提条件"
        def user = new MyUser(id:323L)
        
        and:"mock"
        myRepository.queryRepository(_) >> {777L}

        when:"执行"
        Long result = myService.func(user)

        then:"结果验证"
        noExceptionThrown()
        Assert.assertEquals(result, 3L)
        with(result) {
            Assert.assertEquals(result, 777L)
        }
    }
}

3、备注

1)一个测试方法,可以包含多个 when-then

2)then: “内容可以如下”

  • 判断是否有异常抛出

无异常:noExceptionThrown()

有异常:thrown(IllegalArgumentException.class)

抛出具体异常 和 异常信息

@Service
public class MyService {
    public Integer func(Integer sum) {
        return sum / 0;
    }
}

//UT
class MyServiceSpec extends Specification {
    MyService myService = new MyService()
      
    def setup() {
        
    }

    def "test "() {
        given:
        Integer sum = 323
        when:
        Integer result = myService.func(sum)
        then:
        def ex = thrown(ArithmeticException)
        ex.message == "/ by zero"
    }
}

3)断言

Assert.assertEquals(result, 323L)
result == 323

4)with

验证返回结果res对象的多个属性,是否符合预期
with(resp) {
    resp.getId == 323L
}

5)判断调用次数(类似Mockito的verify)

then:
1 * myRepository.queryRepository(_)// 验证该方法是否被调用了

6)then和expect的区别调用和预期判断并不复杂,那么可以用expect将两者合在一起

调用和预期判断并不复杂,那么可以用expect将两者合在一起:when + then => expect

when:
def x = Math.max(1, 2)
then:
x == 2
//等价
expect:
Math.max(1, 2) == 2
4.4.4 where

1、作用

用于编写数据驱动的用例

2、使用

  • 业务
@Service
public class MyService {
    public String func(Long id) {
        if (Objects.equals(id, 3L)) {
            return "小明";
        } else {
            return "小红";
        }
    }
}
  • UT
class MyServiceSpec extends Specification {
    MyService myService = new MyService()
      
    def setup() {
    }

    def "test qeury id name"() {
        given:
        String s = ""

        when:
        String result = myService.func(id)
          
        then:
        result == name
          
        where:
        id          | 3L
        3L          | "小明"
        7L          | "小红"
    }
}

3、备注

  • 在单测方法的最后
  • 数据表格至少有两列组成,如果是单例的话,可按照如下形式编写

where:

poiId |_

323 | _

777 | _

null | _

  • 真实方法的执行参数,作为where内容时,等价多个测试方法,测试了不同场景
class MyConsumerSpec extends Specification {
    MyConsumer myConsumer = new MyConsumer()

    def setup() {
    }

    @Unroll
    def "test validateParams,#caseDesc"() {
        given:
        Msg msg = ownDefineMsg
          
        when:
        myConsumer.validateParams(msg)
          
        then:
        def error = thrown(IllegalArgumentException)
        error.getMessage() == errMsg
          
        where:
        caseDesc           | ownDefineMsg     | errMsg
        "消息不能为null"     | null             | "消息不能为null"
        "ID不能为null或0"    | new Msg(id:0L)   | "ID不能为null或0"
    }
}

//等价两个测试方法
// 方法1:myConsumer.validateParams(null)
//方法2:myConsumer.validateParams(new Msg(id:0L))
  • where中字段,可使用${}表达式
def "test qeury #desc"() {
    given:
    String str = ""

    when:
    String result = myService.func2(id)

    then:
    result == name

    where:
    desc    |id    | name
    "小明"   | 3L | "${desc},id${id}"
    "小红"   | 7L | "${desc},id${id}"
}

4.5 常用注解(Ignore、Unroll)

4.5.1 Ignore

1、作用

此case不执行

2、使用

@Ignore
def "test"() {
  // ---
}

3、备注

import spock.lang.Ignore是Spcok的

4.5.2 @Unroll

1、作用

自动把测试方法代码拆分成 n个独立的单测测试展示,运行结果更清晰。

2、使用

class MyConsumerSpec extends Specification {
    MyConsumer myConsumer = new MyConsumer()

    def setup() {
    }

    @Unroll
    def "test validateParams,#caseDesc"() {
        given:
        Msg msg = ownDefineMsg
          
        when:
        myConsumer.validateParams(msg)
          
        then:
        def error = thrown(IllegalArgumentException)
        error.getMessage() == errMsg
          
        where:
        caseDesc           | ownDefineMsg     | errMsg
        "消息不能为null"     | null             | "消息不能为null"
        "ID不能为null或0"    | new Msg(id:0L)   | "ID不能为null或0"
    }
}

3、备注

  • 一般结合where一同使用
  • 不加这个注解,单测失败的时候,不知道具体是where中哪个条件测试失败了
4.5.3 mock 静态、私有方法、final相关注解

具体使用,参考mock static

@RunWith(PowerMockRunner.class)使用powermock的PowerMockRunner运行测试继承自Junit
@PowerMockRunnerDelegate(Sputnik.class)使用powermock的@PowerMockRunnerDelegate()注解可以指定Sputnik类(Spock类的父类)去代理运行power mock。这样就可以在Spock里使用powermock去模拟静态方法、final方法、私有方法
@PrepareForTest([Xxx.class, Xxx.class])模拟final类或有static,final, private,方法的类Xxx
@SuppressStaticInitializationFor([“com.sankuai.groceryscp.returnplan.common.utils.LionUtil”, “com.sankuai.groceryscp.returnplan.common.utils.GrayUtils”])static{}静态代码块或static变量的初始化, 在class被容器load的时候要被执行如果执行错误就会导致单元测试错误。此注解阻止静态初始化
@PowerMockIgnore(“javax.management.*”)解决使用powermock后,提示classloader错误

4.6 字符含义(_ 、 * 、 # 、 >>)

4.6.1 _下划线

given:

_ * myRepository.queryRepository(_) >> {1L}

1、场景

前者_ 作为mock方法的调用次数

  • 作用

表示此mock方法queryRepository匹配次数无限制

  • 使用
匹配调用次数的值内容备注
__ * myRepository.queryRepository(_) >> {1L}没有特殊要求,三者都可以
11 * myRepository.queryRepository(_) >> {1L}
不写myRepository.queryRepository(_) >> {1L}【建议不写】
22 * myRepository.queryRepository(_) >> {1L}对于循环调用mock方法,如5.4代码中for循环调用了2次,为了验证是否循环了2次,才会指定匹配调用次数2 *

后者_ 作为方法参数

  • 同Mockito.any(),表示任何类型的参数
4.6.2

1、作用

一般和 @Unroll注解 + where: 一起使用

where中真实调用方法参数,有多种值,对应多个场景的测试,使用#描述每个测试场景

2、使用

class MyConsumerSpec extends Specification {
    MyConsumer myConsumer = new MyConsumer()

    def setup() {
    }

    @Unroll
    def "test validateParams,#caseDesc"() {
        given:
        Msg msg = ownDefineMsg
          
        when:
        myConsumer.validateParams(msg)
          
        then:
        def error = thrown(IllegalArgumentException)
        error.getMessage() == errMsg
          
        where:
        caseDesc           | ownDefineMsg     | errMsg
        "消息不能为null"     | null             | "消息不能为null"
        "ID不能为null或0"    | new Msg(id:0L)   | "ID不能为null或0"
    }
}

3、备注

4.6.3 *

1、作用

返回值类型通配符,表示任意类型都可以匹配

2、使用

_ * myRepository.queryRepository(_) >> {1L}  
或
myRepository.queryRepository(_) >> {1L}  

4.6.4 >>

1、作用

作用同Mockito.thenReturn()

2、使用

1)返回值类型

  • 返回值为对象
>> new XxxDTO(total: 10, skuIdList: new ArrayList<>())
  • 返回值为Map
>> [1L: true]
>> new XxxDTO(skuIdAndDOMap: [1L: new XxxDO()], sellDate: LocalDate.now())
  • List
>> Lists.newArrayList(new XxxDO(skuId: 1L, skuName: "苹果", 
                      new XxxDO(skuId: 2L, skuName: "香蕉")
  • void
>> {}
  • 异常
>> { throw new XxxException("mock 异常") }
  • Set
>> [1, 2]

2)根据参数不同,返回不同值

  • UT
myRepository.countSku(_) >> {Long skuId -> Objects.equals(skuId, 123456789L) ? 1L : 2L}
  • 业务
@Service
public class MyService {
    @Resource
    private MyRepository myRepository;

    public Long func(String date, Long id) {
        return myRepository.query(date, id);
    }
}

@Repository
public class MyRepository {
    public Long query(String date, Long id) {
        // 业务
    }
}
  • UT
class MyServiceSpec extends Specification {
    MyService myService = new MyService()
    MyRepository myRepository = Mock()

    def setup() {
        myService.myRepository = myRepository
    }

    def "test "() {
        given:
        def date = "2023-03-26"

        and:"这里根据query()方法2个入参中,第一个入参date的值,匹配得到方法的结果"
        myRepository.query(_, _) >> {arg1, arg2 -> respDecideByArg(arg1)}

        when:
        Long result = myService.func1(date, 123456789L)

        then:
        result == 1
    }
    
    // 如果这里根据query方法的第一个入参date为“2023-03-26”,则query返回1
    Long respDecideByArg(String date) {
        if (Objects.equals(date, "2023-03-26")) {
            return 1L
        } else if (Objects.equals(date, "2023-03-25")) {
            return 2L
        } else {
            return 3L
        }
    }
}

五、实战

5.1 mock-网络资源(DB、Rpc、线程池、Redis等)

场景MockitoSpock
MyLion
DB
RPC
Mcc
Redis
线程池参考: Mockito 常用注解@Spy

5.1.1MyLion

5.1.1.1 使用Mockito

MyLion底层的方法都是static方法,需要引入mock static相关的依赖

  • pom
// mock静态方法依赖此pom
<dependency>
   <groupId>org.mockito</groupId>
   <artifactId>mockito-inline</artifactId>
   <version>4.11.0</version>
   <scope>test</scope>
</dependency>

//不能使用MDP默认的2.15版本,太低不支持mockStatic
<dependency>
   <groupId>org.mockito</groupId>
   <artifactId>mockito-core</artifactId>
   <version>4.3.1</version>
   <scope>test</scope>
</dependency>

// bytebuddy基于ASM框架的字节码增强类库和工具
//若无此依赖,会报错:
//Could not initialize plugin: interface org.mockito.plugins.MockMaker (alternate: null)
<dependency>
   <groupId>net.bytebuddy</groupId>
   <artifactId>byte-buddy</artifactId>
   <version>1.9.13</version>
   <scope>test</scope>
</dependency>
  • 业务
@Service
public class MyService{
    //场景1:需要查询MyLion.getLong
    public Long func1() {
        return LionUtil.checkTime();
    }

    //场景2:需要查询MyLion.getList
    public Boolean func2(Long id) {
        return LionUtil.getIdList(id);
    }
}
  • 业务Lion
@UtilityClass
public class LionUtil {
  public static long checkTime() {
     return MyLion.getLong();
  }
  
  public boolean getIdList(Long id) {
     List<Long> grayList = MyLion.getList);
     if (grayList.contains(-1L)) {
        return true;
     }
     return grayList.contains(id);
    }
}
  • UT
@RunWith(MockitoJUnitRunner.class)
public class MyServiceTest {
    @InjectMocks
    private MyService myService;

    @Test
    public void tsetMockLion() {
        MockedStatic<MyLion> myLion = mockStatic(MyLion.class);
      
      	//场景1:mock MyLion.getLong
        myLion.when(() -> MyLion.getLong(any(),any(),any()))
                                       .thenReturn(10L);
        Long res = myService.func1();
        Assert.assertThat(res, is(10L));

      	//场景2:mock MyLion.getList
        myLion.when(() -> MyLion.getList(any(),any(),any(),any()))
                        .thenReturn(Lists.newArrayList(323L));
        Boolean res1 = myService.func2(323L);
        Assert.assertTrue(res1);
    }
}
5.1.1.2 使用Spock
  • pom
<!--spock 整合powermock mock 静态方法-->
<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>
  • 业务-LionUtil
@Service
public class MyService{
   // 通过LionUtil获取MyLion配置
   public Integer func1() {
      return  LionUtil.getLimit();
   }
}
  • 业务-LionUtil调用Lion.getXxx方法
@UtilityClass
public class LionUtil {
    public static int getLimit() {
        return MyLion.getInt();
    }
}
  • UT
@RunWith(PowerMockRunner.class)
// 所有需要测试的类列在此处,适用于模拟final类或有static,final, private, native方法的类
@PrepareForTest([LionUtil.class])
// static{}静态代码块或static变量的初始化, 在class被容器load的时候就要被执行
//如果执行错误就会导致junit单元测试错误。故使用下方注解阻止静态初始化
@SuppressStaticInitializationFor(["com.sankuai.groceryscp.returnplan.common.utils.LionUtil"])
//指定Sputnik类去代理运行power mock,而Sputnik类是Spock类的父类。
//故可以在Spock里使用powermock去模拟静态static方法、final方法、私有private方法等
@PowerMockRunnerDelegate(Sputnik.class)
//为了解决使用powermock后,提示classloader错误
@PowerMockIgnore("javax.management.*")
class MyServiceSpec extends Specification {
     MyService myService = new MyService()
   
	   def setup() {
        PowerMockito.mockStatic(LionUtil.class)
     }

     def "test lionUtil"() {
        given: 
        // 这里mock了LionUtil的static方法getLimit
        PowerMockito.when(LionUtil.getLimit()).thenReturn(5)

        when:
        myService.func1()

        then:
        noExceptionThrown()
    } 
}

5.1.2 Mockito - DB、RPC

  • 业务 网关查询 + DB查询
@Service
public class MyService{
    @Resource
    private RedisGateway redisGateway;
    @Resource
    private MyRepository myRepository;
  
    public Integer func(Long id) {
        boolean lockResult = redisGateway.setNxNew("key", "val", 1 * 60 * 1000, "xxx");
        if (lockResult) {
            return myRepository.queryRepository(id).size();
        }
        return -1;
    }
}
  • UT
@RunWith(MockitoJUnitRunner.class)
public class MyServiceTest {
    @InjectMocks
    private MyService myService;
  
    @Mock
    private RedisGateway redisGateway;
  
    @Mock
    private MyRepository myRepository;
  
    @Test
    public void test() {
        // mock: rpc
        when(redisGateway.setNxNew(any(), any(), anyInt(), any()))
                                           .thenReturn(Boolean.TRUE);

        // mock: db
        when(myRepository.queryRepository(any()))
                                   .thenReturn(Lists.newArrayList());

        Integer result = sellOutProcessService.func(3L);
        Assert.assertThat(result, is(0));
    }
}

5.1.3 Mockito MyMcc

  • 业务
@Service
public class MyService{
    @Resource
    private MyMccConfig myMccConfig;
    public String func() {
        return myMccConfig.time;
    }
}
  • MyMccConfig
@Configuration
public class MyMccConfig {
    @MYConfig(value = "time:23:00")
    public volatile String time;
}
  • UT
@RunWith(MockitoJUnitRunner.class)
public class MyServiceTest {

    @InjectMocks
    private MyService myService;
  
    @Mock
    private MyMccConfig myMccConfig;
  
    @Test
    public void test() {
        mccConfig.time = "12";
        String result = myService.func();
        Assert.assertEquals("12", result);
    }
}
5.1.4 Mockito - Redis
  • 业务
@Component
public class RedisGateway {
    @Resource(name = "redisStoreClient")
    private RedisStoreClient redisClient;

        public boolean setNx(String key, String value, int expireTime, String redisCategory) {

        try {
            return redisClient.setnx("", value, expireTime);
        } catch (Exception e) {
            log.error("RedisGateway setNx occurs exception", e);
            throw new GatewayException(GATEWAY_EXCEPTION_CODE, e.getMessage());
        }
    }
}
  • UT
@RunWith(MockitoJUnitRunner.class)
public class SquirrelGatewayTest {
    @InjectMocks
    private RedisGateway redisGateway;

    @Mock
    private RedisStoreClient redisStoreClient;
  
    @Test
    public void test() {
        when(redisStoreClient.setnx(any(), any(), anyInt()))
                                  .thenReturn(Boolean.TRUE);

        boolean b = redisGateway.setNx("", "", 1, "");
        Assert.assertTrue(b);
    }
}
5.1.5 Mockito - 线程池

参考: Mockito 常用注解@Spy

5.1.6 Spock - DB、Rpc、MyMcc、Redis和线程池
  • 业务
@Service
public class MyServiceImpl implements MyService {
    @Resource
    private MyRepository myRepository;
    @Resource
    private MyConvertor myConvertor;
    @Resource
    private MyGateway myGateway;
    @Resource
    private ExecutorService myExecutor;
    @Resource
    private RedisGateway redisGateway;
    @Resource
    private MyMccConfig myMccConfig;

    @Override
    public void func() {

        // 01.myMcc
        String key = myMccConfig.time;

        // 02.redis
        boolean lockResult = redisGateway.setNx(key, "", 1, "");

        if (lockResult) {
            // 03.Rpc[线程池并发调用]
            List<DTO> rpcDTOList = Lists.partition(Lists.newArrayList(1L, 2L), 100).stream()
                    .distinct()
                    .map(partSkuList -> CompletableFuture.supplyAsync(() -> 
                          myGateway.queryData("2023-03-23", 323L, partSkuList),myExecutor))
                    .collect(Collectors.toList())
                    .stream()
                    .map(CompletableFuture::join)
                    .flatMap(List::stream)
                    .collect(Collectors.toList());

            // 04.DB
            List<DO> result = myRepository.queryDOList(model);
        }
    }
}
  • UT
class MyServiceImplSpec extends Specification {
    MyService myService = new MyServiceImpl()

    rtedisGateway redisGateway = Mock()
    MyMccConfig myMccConfig =Mock()
    MyGateway myGateway = Mock()
    ExecutorService myExecutor = Executors.newFixedThreadPool(1)
    MyConvertor myConvertor = new MyConvertorImpl()//或MyConvertor myConvertor = Spy(MyConvertorImpl)
    MyRepository myRepository = Mock()

    def setup() {
        myService.myRepository = myRepository
        myService.myConvertor = myConvertor
        myService.myGateway = myGateway
        myService.myExecutor = myExecutor
        myService.redisGateway = redisGateway
        myService.myMccConfig = myMccConfig
    }

    def "test"() {
        given:
        myMccConfig.time = "19"

        and: "mock redis"
        redisGateway.setNx(_, _, _, _) >> {Boolean.TRUE}

        and: "mock rpc"
        myGateway.queryData(_, _, _) >> { Lists.newArrayList()}

        and: "mock DB"
        myRepository.queryDOList(_) >> {Lists.newArrayList()}

        when:
        myService.func()

        then:
        noExceptionThrown()
    }

5.2 mock-static静态方法

5.2.1 Mockito
  • pom
<dependency>
   <groupId>org.mockito</groupId>
   <artifactId>mockito-inline</artifactId>
   <version>4.11.0</version>
   <scope>test</scope>
</dependency>

//不引入此pom会报错:
org.mockito.exceptions.base.MockitoException: 
The used MockMaker SubclassByteBuddyMockMaker does not support the creation of static mocks
Mockito's inline mock maker supports static mocks based on the Instrumentation API.

<dependency>
   <groupId>org.mockito</groupId>
   <artifactId>mockito-core</artifactId>
   <version>4.3.1</version>
   <scope>test</scope>
</dependency>
//mdp自带集成的mockito-core版本是2.15,这个版本过低,
//无法使用用于测试静态方法的mockStatic方法。
  • 业务
@Service
public class MyService {
    public boolean isBlank(String str) {
        return StringUtils.isBlank(str);
    }
}
  • UT
@RunWith(MockitoJUnitRunner.class)
public class MyServiceTest {
    @InjectMocks
    privare MyService myService;
  
    @Test
    public void t1() {
        boolean b = myService.isBlank("");
        System.out.println(b);//true
    }
  
    @Test
    public void t2() {
        MockedStatic<StringUtils> mockStatic = 
                           mockStatic(StringUtils.class);
      
        when(StringUtils.isBlank(any())).thenReturn(false);

        boolean b = myService.isBlank("");
        System.out.println(b);//false
    }
}
5.2.2 Spock

Spock对静态static方法、私有private方法、final类的Mock,需要基于PowerMock来完成

  • pom
<!--spock 整合powermock mock 静态方法、私有、final方法-->
<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>
  • 业务-使用静态方法
@Service
public class MyService{
   // 场景1:通过LionUtil获取MyLion配置
   public Integer func1() {
      return LionUtil.getLimit();
   }
}
  • 业务-LionUtil的static方法
@UtilityClass
public class LionUtil {
    public static int getLimit() {
        return MyLion.getInt();
}
  • UT
@RunWith(PowerMockRunner.class)
// 所有需要测试的类列在此处,适用于模拟final类或有static,final, private, native方法的类
@PrepareForTest([LionUtil.class, GrayUtils.class])
// static{}静态代码块或static变量的初始化, 在class被容器load的时候就要被执行
//如果执行错误就会导致junit单元测试错误。故使用下方注解阻止静态初始化
@SuppressStaticInitializationFor(["com.sankuai.groceryscp.returnplan.common.utils.LionUtil",
        "com.sankuai.groceryscp.returnplan.common.utils.GrayUtils"])
//指定Sputnik类去代理运行power mock,而Sputnik类是Spock类的父类。
//故可以在Spock里使用powermock去模拟静态static方法、final方法、私有private方法等
@PowerMockRunnerDelegate(Sputnik.class)
//为了解决使用powermock后,提示classloader错误
@PowerMockIgnore("javax.management.*")
class MyServiceSpec extends Specification {
     MyService myService = new MyService()
       
	   def setup() {
        PowerMockito.mockStatic(LionUtil.class,GrayUtils.class)
     }

     def "test lionUtil"() {
        given:
        // 这里mock了LionUtil的static方法getLimit
        PowerMockito.when(LionUtil.getLimit()).thenReturn(5)

        when:
        myService.func1()

        then:
        noExceptionThrown()
    } 
}

5.3 mock-private私有方法

5.3.1 Mockito
  • pom
<!--spock 整合powermock mock 静态方法、私有方法-->
  <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>
  • 业务
@Service
public class MyService {
    public Long func(String date) {
        Long count = 0L;
        return queryPrivate(date, count);
    }


    private Long queryPrivate(String date, Long count) {
        if (date.compareTo("2023-01-01") <=0 ){
            throw new IllegalArgumentException("日期早于2023-01-01");
        }
        return count + 1;
    }
}
  • UT
@RunWith(PowerMockRunner.class)
@PrepareForTest(MyService.class)
public class MyServiceTest {
    MyService myService = new MyService();
  
    @Test
    public void test() throws Exception {
        MyService spy = PowerMockito.spy(myService);//不是Mockito的spy()
        //这种格式不可以!!!:PowerMockito.when(userService, "queryPrivate", any()).thenReturn(777L);
        // 必须这种格式
        PowerMockito.doReturn(777L).when(spy, "queryPrivate", any(), any());
      
        Long id = spy.func("2000-01-01");
        Assert.assertEquals(777L, (long) id);
    }
}
5.3.2 Spock
  • pom
<dependency>
    <groupId>org.spockframework</groupId>
    <artifactId>spock-core</artifactId>
    <version>1.3-groovy-2.4</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.spockframework</groupId>
    <artifactId>spock-spring</artifactId>
    <version>1.3-groovy-2.4</version>
    <scope>test</scope>
</dependency>
        
<!--spock 整合powermock mock 静态方法、私有方法-->
 <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>
  • 业务
@Service
public class MyService {
    public Long func(String date) {
        return queryPrivate(date);
    }

    private Long queryPrivate(String date) {
        if (date.compareTo("2023-01-01") <=0 ){
            throw new IllegalArgumentException("日期早于2023-01-01");
        }
        return 1L;
    }
}
  • UT
@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(Sputnik.class)
@PrepareForTest([MyService.class])
class MyServiceSpec extends Specification {
    MyService myService = new MyService()
    def setup(){
    }

    def "test"() {
        given:"mock 私有"
        def spy = PowerMockito.spy(myService)
          
        // 这里queryPrivate的参数必须传 "2000-01-01" 和 0L,原因见5.4中mock-private私有方法 + mock 网络资源的UT
        PowerMockito.doReturn(777L).when(spy, "queryPrivate", "2000-01-01", 0L)

        when:
        Long id = spy.func("2000-01-01")
        println(id)

        then:
        noExceptionThrown()
        id == 777L
    }
}

5.4 mock-private私有方法 + mock 网络资源

5.4.1 Mockito
  • pom
<!--spock 整合powermock mock 静态方法、私有方法-->
  <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>
  • 业务
@Service
public class MyService {
    @Resource
    private MyRepository myRepository;

    public Long func(String date) {
        Long count = myRepository.countSku(date);//mock myRepository方法
        return queryPrivate(date, count);// mock 私有方法
    }

    private Long queryPrivate(String date, Long count) {
        if (date.compareTo("2023-01-01") <=0 ){
            throw new IllegalArgumentException("日期早于2023-01-01");
        }
        return count + 1;
    }
}


@Repository
public class MyRepository {
    public Long countSku(String date) {
        //业务
    }
}
  • UT
@RunWith(PowerMockRunner.class)
@PrepareForTest(MyService.class)
public class MyServiceTest {

    @InjectMocks
    MyService myService = new MyService();

    @Mock
    private MyRepository myRepository;

    @Test
    public void t1() throws Exception {
        // mock myRepository
        when(myRepository.countSku(any())).thenReturn(1L);

        // mock 私有方法
        MyService spy = PowerMockito.spy(myService);
        PowerMockito.doReturn(777L).when(spy, "queryPrivate", any(), any());

        Long id = spy.func("2000-01-01");
        System.out.println(id);
    }
}
5.4.2 Spock
  • pom
<dependency>
    <groupId>org.spockframework</groupId>
    <artifactId>spock-core</artifactId>
    <version>1.3-groovy-2.4</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.spockframework</groupId>
    <artifactId>spock-spring</artifactId>
    <version>1.3-groovy-2.4</version>
    <scope>test</scope>
</dependency>
        
<!--spock 整合powermock mock 静态方法、私有方法-->
 <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>
  • 业务
@Service
public class MyService {
    @Resource
    private MyRepository myRepository;

    public Long func(String date) {
        Long count = myRepository.countSku(date);//mock myRepository方法
        return queryPrivate(date, count);// mock 私有方法
    }

    private Long queryPrivate(String date, Long count) {
        if (saleDdateate.compareTo("2023-01-01") <=0 ){
            throw new IllegalArgumentException("日期早于2023-01-01");
        }
        return count + 1;
    }
}


@Repository
public class MyRepository {
    public Long countSku(String date) {
        //业务
    }
}
  • UT
@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(Sputnik.class)
@PrepareForTest([MyService.class])
class MyServiceSpec extends Specification {
    MyService myService = new MyService()
    MyRepository myRepository = Mock()

    def setup(){
        myService.myRepository = myRepository
    }

    def "test"() {
        given:"mock myRepository"
        myRepository.countSku(_) >> {123L}

        and:"mock 私有方法"
        def spy = PowerMockito.spy(myService)
        PowerMockito.doReturn(777L).when(spy, "queryPrivate", "2000-01-01", 123L)

        when:
        Long id = spy.func("2000-01-01")
        println(id)

        then:
        noExceptionThrown()
        id == 777L
    }
}
  • 注意事项:
public Long func(String date) {
        Long count = myRepository.countSku(date);//mock myRepository方法
        return queryPrivate(date, count);// mock 私有方法
    }

    private Long queryPrivate(String date, Long count) {
        if (date.compareTo("2023-01-01") <=0 ){
            throw new IllegalArgumentException("日期早于2023-01-01");
        }
        return count + 1;
    }
    
私有方法queryPrivate,第一个入参为String date,第二个入参Long count,在被mock时PowerMockito.doReturn(777L).when(spy, "queryPrivate", "2000-01-01", 123L)

传递的值,必须等于queryPrivate非mock情况即正常逻辑下,会传入的值

eg:
myRepository.countSku(_) >> {123L}的mock结果为123L。即Long count = 123L,所以,正常执行queryPrivate方法时,count字段就会传入123L。

同理,spy.func("2000-01-01"),调用func时,传入date = "2000-01-01",所以,正常执行queryPrivate方法时,date字段就会传入"2000-01-01"PowerMockito.doReturn(777L).when(spy, "queryPrivate", "2000-01-01", 123L)//必须传递"2000-01-01"  和 123L

5.5 mock-参数校验

  • Rpc入参
  • MQ内容
5.5.1 Mockito
  • 业务-req
	@NotNull(message = "id不能为空")
    @Positive(message = "id不能为负")
    private Long id;

    @NotBlank(message = "日期不能为空")
    @DateFormatCheck(value = "yyyy-MM-dd", message = "日期必须为yyyy-MM-dd格式")
    private String date;

    @NotNull(message = "发布状态不能为空")
    @Range(min = 0, max = 1, message = "状态输入有误, 0:待确认, 1:已发布")
    private Integer status;

    @CollectionSizeCheck(value = 5,message = "当前用户输入的skuId集合最多5个")
    private List<Long> skuIdList;
  • 业务校验参数
@Service
public class MyService {
  public TResp func(TReq req) {
  try {
      // 参数校验
      ValidateUtil.validateParam(req);
  } catch (IllegalArgumentException e) {
      //   
  } 
  return response;
	}  
}
  • UT
@RunWith(MockitoJUnitRunner.class)
public class MyServiceTest {
    @InjectMocks
    private MyService myService;
  
    @Test
    public void testQueryProcessList_apramError() {

        //场景1:参数为异常 - id为空
        TReq req;
        TResp resp;
        req = new QueryProcessListTReq();
        resp = myService.func(req);
        Assert.assertEquals(resp.getCode(), -1);

        //场景2:参数为异常 - 日期为空
        req.setId(323L);
        resp = myService.func(req);
        Assert.assertEquals(resp.getCode(),-1);

        //场景3:参数为异常 - 日格式异常
        req.setId(3L);
        req.setDate("20220320");
        resp = myService.func(req);
        Assert.assertEquals(resp.getCode(), -1);

        //场景4:参数为异常 - 状态为空
        req.setDate("2022-03-20");
        resp = myService.func(req);
        Assert.assertEquals(resp.getCode(), -1);

        //场景5:参数为异常 - 状态值不在0-1之间
        req.setDate("2022-03-20");
        req.setStatus(2);
        resp = myService.func(req);
        Assert.assertEquals(resp.getCode(),-1);

        //场景6:参数为异常 - sku大小 > 5
        req.setStatus(1);
        req.setSkuIdList(Lists.newArrayList(1L, 2L, 3L, 4L, 5L, 6L));
        resp = myService.func(req);
        Assert.assertEquals(resp.getCode(), -1);
    }
}
5.5.2 Spock
  • 业务 -MQ内容
@Data
public class Msg implements Serializable{
    private Long id;
}
  • 业务 -校验MQ内容
@Service("myConsumer")
public class MyConsumer {
    @KafkaMsgReceive
    public ConsumeStatus receive(String msgBody) {
        try {
            Msg msg = GsonUtils.fromJson(msgBody, Msg.class);
            validateParams(msg);
            // ---
        } catch (IllegalArgumentException e) {
            
        } catch (Exception e) {
            
        }
        return ConsumeStatus.CONSUME_SUCCESS;
    }

    private void validateParams(Msg msg) {
        Preconditions.checkArgument(Objects.nonNull(msg), "合单消息不能为null");
        Long id = msg.getId();
        Preconditions.checkArgument(Objects.nonNull(id) && id > 0, "ID不能为null或0");
    }
}
  • UT
class MyConsumerSpec extends Specification {
    MyConsumer myConsumer = new MyConsumer()

    def setup() {
    }

    @Unroll
    def "test validateParams,#caseDesc"() {
        given:
        Msg msg = ownDefineMsg
          
        when:
        myConsumer.validateParams(msg)
          
        then:
        def error = thrown(IllegalArgumentException)
        error.getMessage() == errMsg
          
        where:
        caseDesc          | ownDefineMsg     | errMsg
        "消息不能为null"    | null             | "消息不能为null"
        "ID不能为null或0"  | new Msg(id:0L)    | "ID不能为null或0"
    }
}

补充:页面请求中,获取前端字符串格式的入参,直接转后端对象,避免了繁琐的属性赋值

String requestStr = "{是个Json字符串}";
TRequest tRequest = GsonUtils.fromJson(requestStr, TRequest.class);

5.6 mock-循环(for、while)

适用于循环中,对方法mock

eg:200、200循环查询db 或 依赖方数据

  • 第一次查询,有200条数据
  • 第二次查询,有10条数据

总数据量 = 200 + 10

5.6.1 Mockito
  • 业务
@Service
public class MyService {
    @Resource
    private MyRepository myRepository;

    public BigDecimal calculateGMV(List<Order> orderList) {
        BigDecimal gmv = BigDecimal.ZERO;

        for (Order order : orderList) {
            //单价
            BigDecimal skuPrice = order.getPrice();
            //数量
            Long skuId = order.getSkuId();
            Long skuCount = myRepository.countSku(skuId);
            //销售额
            gmv = gmv.add(skuPrice.multiply(BigDecimal.valueOf(skuCount)));
        }
        // 总销售额
        return gmv.setScale(2, BigDecimal.ROUND_HALF_DOWN);
    }
}

@Repository
public class MyRepository {
    public Long countSku(Long skuId) {
        return null;
    }
}
  • UT
@RunWith(MockitoJUnitRunner.class)
public class MyServiceTest {
    @InjectMocks
    private MyService myService;
    @Mock
    private MyRepository myRepository;

    @Test
    public void test() {
        Order o1 = Order.builder().skuId(123456789L).price(BigDecimal.valueOf(10.0)).build();
        Order o2 = Order.builder().skuId(987654321L).price(BigDecimal.valueOf(20.0)).build();

        when(myRepository.countSku(123456789L)).thenReturn(1L);
        when(myRepository.countSku(987654321L)).thenReturn(2L);

        BigDecimal gmv = myService.func(Lists.newArrayList(o1, o2));
        assertEquals(50L, gmv.longValue());
    }
}
5.6.2 Spock
  • 业务:同上
  • UT
class MyServiceSpec extends Specification {
    MyService myService = new MyService()
    MyRepository myRepository = Mock()

    def setup() {
        myService.myRepository = myRepository
    }

    def "test "() {
        given:
        def o1 = new Order(skuId:123456789L, price: 10)
        def o2 = new Order(skuId:987654321L, price: 20)

        and:"for循环中,2次调用myRepository.countSku定义得到不同结果"
        2 * myRepository.countSku(_) >> 1L >> 2L
        //等效 >> {Long skuId -> Objects.equals(skuId, 123456789L) ? 1L : 2L}

        when:
        BigDecimal gmv = myService.calculateGMV(Lists.newArrayList(o1, o2))

        then:"结果验证"
        gmv.longValue() == 50L // 50 = 1*10 + 2*20
    }
}
5.7 打印日志
场景MockitoSpock
打印日志@Slf4j + log.info(“result:[{}]”, GsonUtils.toJsonStr(resp));println(GsonUtils.toJsonStr(resp))printf
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Mockito 是一个 Java 单元测试框架,它可以模拟对象并进行单元测试。切面编程是一种编程范式,它可以在程序运行时动态地将代码织入到现有的代码中。在单元测试中,我们可以使用 Mockito 和切面编程来模拟对象并测试代码的行为。 在使用 Mockito 进行切面单元测试时,我们可以使用 Mockito 的 `@Mock` 注解来创建模拟对象,并使用 `@InjectMocks` 注解将模拟对象注入到被测试对象中。然后,我们可以使用 Mockito 的 `when()` 方法来设置模拟对象的行为,并使用 `verify()` 方法来验证被测试对象的行为。 下面是一个使用 Mockito 进行切面单元测试的示例代码: ```java import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class MyAspectTest { @Mock private MyService myService; @InjectMocks private MyAspect myAspect; @Test public void testMyAspect() { when(myService.doSomething()).thenReturn("mocked result"); myAspect.doSomethingWithAspect(); verify(myService).doSomething(); } } ``` 在这个示例中,我们创建了一个 `MyService` 的模拟对象,并将其注入到 `MyAspect` 中。然后,我们使用 `when()` 方法设置模拟对象的行为,并调用 `MyAspect` 的 `doSomethingWithAspect()` 方法。最后,我们使用 `verify()` 方法验证 `MyService` 的 `doSomething()` 方法是否被调用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值