文章目录
1、单元测试
前言
我们在购买电脑的时候,一次就可以开机了,这是因为在出厂的时候厂家就帮我们做好了测试。那如果没有厂家这步,我们会面临显示器无法显示的问题,磁盘无法挂载等情况。那运气好可能一次就能定位,运气不好,还需要排查显示器,内存条,主板,显卡等一系列组件。等我们排查就花费了大量的时间和精力。那如果在组装之前就测试好了每个组件情况,也就能避免这样的事情发生了。如果把电脑的生产,测试和软件的开发测试类比,就会发现。
显卡,内存条就像是软件中的单元,通常是函数或者类,对单个元器件的测试就像是软件测试中的单元测试;
组装完成的功能机箱,显示器就像是软件中的模块,对机箱显示器的测试就像是软件中的集成测试; 电脑全部组装完成就像是软件完成了预发布版本。
电脑全部组装完成后的开机测试就像是软件中的系统测试。
1.1、定义
单元测试是指对软件中的最小可测试单元在与程序其他部分相隔离的情况下进行检查和验证的工作,这里的最小可测试单元通常是指函数或者类。
- 驱动代码是用来调用被测函数的,而桩代码和 Mock 代码是用来代替被测函数调用的真实代码的。
- Stub(桩对象):Stub通常用于替代测试对象的某些部分,以便进行单元测试等测试。例如,当被测代码需要访问外部数据源或者调用其他函数时,我们可以使用Stub来模拟这些依赖项的行为,从而使得测试过程更加独立和可控。
- Mock(模拟对象):Mock通常用于模拟函数或对象的行为,以便更好地进行单元测试或功能测试。例如,当被测代码需要与某个对象进行交互时,我们可以使用Mock来模拟该对象的行为和响应,并判断被测代码的行为是否正确。
1.2、作用
一般测试方法如下:
- 启动整个应用,模拟用户正常操作。设计到大量的改动需要再次模拟场景。
- 代码某个地方写一个临时入口(比如main),模拟调用。临时代码用后需要删除。
当有如下场景的时候就可以考虑采用单元测试
- 测试场景较多,且需要多次场景测试。(比如每次改动一个点需要多次调用模拟场景,这里适合做成自动化。)
- 被测单元依赖的模块尚未开发完成A,而被测单元需要依赖模块的返回值进行后续处理。
- 需要将当前被测单元和其依赖模块独立开来,构造一个独立的测试环境,不关注被测单元的依赖对象,只关注被测单元的功能逻辑。(比如新增了逻辑不需要测试整个流程,只需要测试修改部分逻辑)
- 被测单元依赖的对象较难模拟或者构造比较复杂。(比如db连接池)
1.3、使用
1.3.1、 常用注解
@SpringBootTest
:获取启动类,加载配置,寻找主配置启动类(被 @SpringBootApplication 注解@RunWith(SpringRunner.class)
:让JUnit运行Spring的测试环境,获得Spring环境的上下文的支持@Test
:测试方法,可以测试期望异常(配置expected )和超时时间。@Mock
:是 Mockito.mock() 方法的简写。创建的是全部mock的对象,即在对具体的方法打桩之前,mock对象的所有属性和方法全被置空(0或null)。@Spy
:会调用真实的方法,有返回值的调用真实方法并返回真实值;如果发现修饰的变量是 null,会自动调用类的无参构造函数来初始化。定义了mock方法的则执行mock(即虚假函数);默认生成后所有依赖的对象都会null,且要一个无参构造。@InjectMocks
:创建一个实例,其余用@Mock(或@Spy)注解创建的mock将被注入到用该实例中。如果使用spring的@Autowired注解一起使用,则会直接使用spring容器的对象,并将@Mock(或@Spy)对象注入。@MockBean
: Spring Boot 中的注解。我们可以使用 @MockBean 将 mock 对象添加到 Spring 应用程序上下文中。该 mock 对象将替换应用程序上下文中任何现有的相同类型的 bean。如果应用程序上下文中没有相同类型的 bean,它将使用 mock 的对象作为 bean 添加到上下文中。@SpyBean
:同上。
1.3.2、 注意事项
@InjectMocks
由mock框架管理,所以只能注入@Mock和@Spy的对象。
@MockAService aService;@InjectMocksAController aController;//这里会注aService@AutowiredAController aController;//这里不会注aServiceclassBController{AService aService;}
- @MockBean和@SpyBean由spring管理,会替换上下文相同对象。
@MockBeanAService aService;@AutowiredAController aController;//这里会注入aService
- 如果想一个spring对象注入mock框架的对象,可通过@InjectMocks桥接。
@MockAService aService;@Autowired@InjectMocksAController aController;//这里会注入aService
@SpyBean
存在循环依赖问题,其原因主要是早期暴露和正常暴露会创建不同对象,造成对象不一致。通过如下方式也没办法解决,因为spy的是spring增强的对象,而不是像@SpyBean
注解代理的是原生对象。
AService bean = context.getBean(AService.class);AService spy =Mockito.spy(bean);
- 设置 spy 逻辑时不能再使用
when(某对象.某方法).thenReturn(某对象)
的语法,而是需要使用doReturn(某对象).when(某对象).某方法
或者doNothing(某对象).when(某对象).某方法
。 - 对于 static 、 final 、private修饰的方法和equals()、hashCode()方法, Mockito 无法对其进行when(…).thenReturn(…) 操作。
1.3.4、 注解使用场景
- 非spring环境:@Mock+@Spy+@InjectMocks
- spring环境:@MockBean+@SpyBean+@Autowired,为测试主体类部分打桩考虑使用
SpyBean
, 为外部依赖打桩,考虑使用MockBean
测试代码
@ServicepublicclassAService{publicStringhasReturnAndArgs(String str){return"10";}publicStringhasReturn(){return"10";}publicvoidhasArgs(String str){System.out.println(1000);}publicvoidnoArgs(){System.out.println(1000);}}@RestControllerpublicclassAController{@AutowiredprivateAService aService;publicStringhasReturnAndArgs(String str){return aService.hasReturnAndArgs(str);}publicStringhasReturn(){return aService.hasReturn();}publicvoidhasArgs(String str){
aService.hasArgs(str);}publicvoidnoArgs(){
aService.noArgs();}}
1.3.5、使用mock
@RunWith(SpringJUnit4ClassRunner.class)@SpringBootTest(classes =Application.class)publicclassServiceTest{@Beforepublicvoidbefore(){// 启用 Mockito 注解MockitoAnnotations.initMocks(this);}@MockAService aService;@InjectMocksAController aController;@Testpublicvoidtest(){//1.不调用真实方法,默认返回nullString value = aService.hasReturnAndArgs("10");Assert.assertEquals(value,null);//2.打桩//当传参是10L时,返回responseMockito.when(aService.hasReturnAndArgs("10")).thenReturn("30");//当传参是20L时,真实调用Mockito.when(aService.hasReturnAndArgs("20")).thenCallRealMethod();//当传参是30L时,抛出异常Mockito.when(aService.hasReturnAndArgs("30")).thenThrow(newException("test error"));Assert.assertEquals(aService.hasReturnAndArgs("10"),"30");//入口为真实方法,内部mock对象调用的也是mock方法Assert.assertNotEquals(aService.hasReturnAndArgs("20"),"30");try{Assert.assertNotEquals(aService.hasReturnAndArgs("30"),"30");}catch(Exception e){System.out.println(e.getMessage());}//3.注入对象Assert.assertEquals(aController.hasReturnAndArgs("10"),"30");}}
1.3.6、使用Spy
@RunWith(SpringJUnit4ClassRunner.class)@SpringBootTest(classes =Application.class)publicclassServiceTest{@Beforepublicvoidbefore(){// 启用 Mockito 注解MockitoAnnotations.initMocks(this);}@SpyAService spy;@Testpublicvoidtest(){//AService spyTemp = new AService();//AService spy = Mockito.spy(spyTemp);//1.调用真实方法Assert.assertEquals(spy.hasReturnAndArgs("20"),"10");//2.打桩Mockito.doReturn("30").when(spy).hasReturnAndArgs("20");Assert.assertEquals(spy.hasReturnAndArgs("20"),"30");//验证是否被调用了一次Mockito.verify(spy,times(1)).hasReturnAndArgs("20");//设置任何hasReturnAndArgs调用都返回30Mockito.doReturn("30").when(spy).hasReturnAndArgs(Mockito.anyString());Assert.assertEquals( spy.hasReturnAndArgs("-2"),"30");Mockito.verify(spy,times(2)).hasReturnAndArgs(Mockito.anyString());//不支持这样Mockito.when(spy.hasReturnAndArgs("20")).thenReturn("10");Assert.assertEquals(spy.hasReturnAndArgs("20"),"10");}}
1.3.7、使用spring集成
@RunWith(SpringJUnit4ClassRunner.class)@SpringBootTest(classes =Application.class)publicclassServiceTest{@Beforepublicvoidbefore(){// 启用 Mockito 注解MockitoAnnotations.initMocks(this);}@SpyBeanprivateAService spy;@AutowiredAController aController;@Testpublicvoidtest(){//调用真实方法Assert.assertEquals(spy.hasReturnAndArgs("20"),"10");Mockito.doReturn("30").when(spy).hasReturnAndArgs(Mockito.anyString());Assert.assertEquals(spy.hasReturnAndArgs("20"),"30");Mockito.verify(spy,times(1)).hasReturnAndArgs(Mockito.anyString());Assert.assertEquals(aController.hasReturnAndArgs("20"),"30");}}
1.3.8、行为验证
//是否调用过一次Mockito.verify(spy).hasReturnAndArgs(Mockito.anyString());//是否调用过N次Mockito.verify(spy,times(1)).hasReturnAndArgs(Mockito.anyString());//没有被调用,相当于 times(0)Mockito.verify(spy,never()).hasReturnAndArgs(Mockito.anyString());//atLeast(N) 至少被调用 N 次//atLeastOnce() 相当于 atLeast(1)//atMost(N) 最多被调用 N 次
1.3.9、断言
@TestpublicvoidtestAssert(){// allOf: 所有条件都必须满足,相当于&&assertThat("myname",allOf(startsWith("my"),containsString("name")));// anyOf: 其中一个满足就通过, 相当于||assertThat("myname",anyOf(startsWith("na"),containsString("name")));// both: &&assertThat("myname",both(containsString("my")).and(containsString("me")));// either: 两者之一assertThat("myname",either(containsString("my")).or(containsString("you")));// everyItem: 每个元素都需满足特定条件assertThat(Arrays.asList("my","mine"),everyItem(startsWith("m")));// hasItem: 是否有这个元素assertThat(Arrays.asList("my","mine"),hasItem("my"));// hasItems: 包含多个元素assertThat(Arrays.asList("my","mine","your"),hasItems("your","my"));// is: is(equalTo(x))或is(instanceOf(clazz.class))的简写assertThat("myname",is("myname"));assertThat("mynmae",is(String.class));// anything(): 任何情况下,都匹配正确assertThat("myname",anything());// not: 否为真,相当于!assertThat("myname",is(not("you")));// nullValue(): 值为空String str =null;assertThat(str,is(nullValue()));// notNullValue(): 值不为空String str2 ="123";assertThat(str2,is(notNullValue()));// -------------------------字符串匹配// containsString:包含字符串assertThat("myname",containsString("na"));// stringContainsInOrder: 顺序包含,“my”必须在“me”前面assertThat("myname",stringContainsInOrder(Arrays.asList("my","me")));// endsWith: 后缀assertThat("myname",endsWith("me"));// startsWith: 前缀assertThat("myname",startsWith("my"));// isEmptyString(): 空字符串assertThat("",isEmptyString());// equalTo: 值相等, Object.equals(Object)assertThat("myname",equalTo("myname"));assertThat(newString[]{"a","b"},equalTo(newString[]{"a","b"}));// equalToIgnoringCase: 比较时,忽略大小写assertThat("myname",equalToIgnoringCase("MYNAME"));// equalToIgnoringWhiteSpace: 比较时, 首尾空格忽略, 比较时中间用单个空格assertThat(" my \t name ",equalToIgnoringWhiteSpace(" my name "));// isOneOf: 是否为其中之一assertThat("myname",isOneOf("myname","yourname"));// isIn: 是否为其成员assertThat("myname",isIn(newString[]{"myname","yourname"}));// toString() 返回值校验assertThat(333,hasToString(equalTo("333")));//------------------------ 数值匹配// closeTo: [operand-error, operand+error], Double或BigDecimal类型assertThat(3.14,closeTo(3,0.5));assertThat(newBigDecimal("3.14"),is(BigDecimalCloseTo.closeTo(newBigDecimal("3"),newBigDecimal("0.5"))));// comparesEqualTo: compareTo比较值assertThat(2,comparesEqualTo(2));// greaterThan: 大于assertThat(2,greaterThan(0));// greaterThanOrEqualTo: 大于等于assertThat(2,greaterThanOrEqualTo(2));// lessThan: 小于assertThat(0,lessThan(2));// lessThanOrEqualTo: 小于等于assertThat(0,lessThanOrEqualTo(0));// -----------------------------------------------------集合匹配// array: 数组长度相等且对应元素也相等assertThat(newInteger[]{1,2,3},is(array(equalTo(1),equalTo(2),equalTo(3))));// hasItemInArray: 数组是否包含特定元素assertThat(newString[]{"my","you"},hasItemInArray(startsWith("y")));// arrayContainingInAnyOrder, 顺序无关,长度要一致assertThat(newString[]{"my","you"},arrayContainingInAnyOrder("you","my"));// arrayContaining: 顺序,长度一致assertThat(newString[]{"my","you"},arrayContaining("my","you"));// arrayWithSize: 数组长度assertThat(newString[]{"my","you"},arrayWithSize(2));// emptyArray: 空数组assertThat(newString[0],emptyArray());// hasSize: 集合大小assertThat(Arrays.asList("my","you"),hasSize(equalTo(2)));// empty: 空集合assertThat(newArrayList<String>(),is(empty()));// isIn: 是否为集合成员assertThat("myname",isIn(Arrays.asList("myname","yourname")));// -------------------------------------------------Map匹配Map<String,String> myMap =newHashMap();
myMap.put("name","john");// hasEntry: key && value匹配assertThat(myMap,hasEntry("name","john"));// hasKey: key匹配assertThat(myMap,hasKey(equalTo("name")));// hasValue: value匹配assertThat(myMap,hasValue("john"));}
1.3.10、常用方法
//有参有返回Mockito.doReturn("haha").when(spy).hasReturnAndArgs(Mockito.anyString());Assert.assertEquals(spy.hasReturnAndArgs("str"),"haha");//无参有返回Mockito.doReturn("hasReturn").when(spy).hasReturn();Assert.assertEquals(spy.hasReturn(),"hasReturn");//有参无返回Mockito.doNothing().when(spy).hasArgs(Mockito.anyString());
spy.hasArgs("haha");Mockito.verify(spy).hasArgs(Mockito.anyString());//无参无返回Mockito.doNothing().when(spy).noArgs();
spy.noArgs();Mockito.verify(spy).noArgs();//调用真实方法Mockito.doCallRealMethod().when(spy).hasReturnAndArgs("hha");Assert.assertEquals(spy.hasReturnAndArgs("hha"),"10");//静态方法//抛出异常Mockito.doThrow(newRuntimeException()).when(spy).hasReturnAndArgs(eq("20"));try{
spy.hasReturnAndArgs("20");}catch(Exception e){System.out.println(e.getMessage());}//---------------------------------------------------------其他//参数匹配器//anyInt()、anyString()、anyDouble()、anyList()、anyMap(),eq(1)Mockito.doReturn("haha").when(spy).hasReturnAndArgs(eq("20"));//模拟返回值Mockito.doAnswer(invocation ->{//获取参数值Object[] args = invocation.getArguments();String arg =(String) args[0];if("hallo".equals(arg)){return"helloWorld";}return arg;}).when(spy).hasReturnAndArgs(Mockito.anyString());Assert.assertEquals(spy.hasReturnAndArgs("hallo"),"helloWorld");Assert.assertEquals(spy.hasReturnAndArgs("ha"),"ha");
1.3.11、mock静态方法
导入pom
<dependency><groupId>org.mockito</groupId><artifactId>mockito-inline</artifactId><version>4.0.0</version></dependency><dependency><groupId>org.mockito</groupId><artifactId>mockito-core</artifactId><version>4.0.0</version></dependency>
MockitoAnnotations.initMocks(this);Mockito.mockStatic(XXX.class).when(XXX::getXXX).thenReturn("xxx");//如果用多次需要关闭try(MockedStatic<XXX> xx=Mockito.mockStatic(XXX.class)){
xx.when(()->A.b(params)).thenReturn(null);}
1.4、统计覆盖率
红色为尚未覆盖的行,绿色为覆盖的行。class,method,line分别表示类/方法/行代码测试覆盖率。
版权归原作者 Nuan_Feng 所有, 如有侵权,请联系我们删除。