草庐IT

PowerMock测试框架详解

Mr. 阿柴 2023-04-11 原文

摘要

本文介绍了Mock背景,常见的单元测试场景以及对应的测试方法,最后简单介绍了powermockit测试框架。理解本文内容,会带你入门java单元测试,其中第一章、第二章需要重点理解。

一、背景

Mock是什么?

Mock(模拟的)是一种隔离测试类功能的方法。例如:mock测试不需要真实连接数据库,或读取配置文件,或连接服务器。mock对象模拟真实服务,mock对象会返回与传递给它的某个虚拟输入相对应的虚拟数据。

为什么要使用Mock?

单元测试重点在于验证代码逻辑以及结果是否正确,但是在测试过程中经常会遇到如下痛点问题:

  • 接口之间的相互依赖
  • 第三方接口调用,例如连接数据库,连接https等

使用Mock框架可以模拟出外部依赖,只注重测试代码逻辑、验证代码结果,满足测试真实目的。

Mock框架使用流程

  1. 创建 外部依赖 的Mock 对象, 然后将Mock 对象注入到 测试类 中;

  2. 执行 测试代码

  3. 校验 测试代码 是否执行正确。

 本文所需的maven依赖

<scope>test</scope>表示仅作用于测试目录。默认作用于范围compile表示被依赖项目需要参与当前项目的编译,包含测试目录。

mvn -Dmaven.test.skip clean install  方式可以不编译测试用例

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

<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-module-junit4</artifactId>
    <version>2.0.2</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-api-mockito2</artifactId>
    <version>2.0.2</version>
    <scope>test</scope>
</dependency>

二、单元测试待测内容有哪些分类?

2.1 无框架java代码

使用Junit单元测试包进行测试。

1.被测试类中方法无其他依赖

1)普通无依赖静态方法

静态方法无其他依赖类,可以直接调用方法计算结果。只需要构造期望数据,计算实际数据,最后比对数据。

代码示例:

被测试类:

public class MyMath {
	public static int add(int num1,int num2) {
		return num1 + num2;
	}
}

测试代码:

//测试静态方法
	@Test
	public void testStaticAdd() {
		int num1 = 1;
		int num2 = 1;
		int expect = 2;
		int real = MyMath.add(num1, num2);
		Assert.assertEquals(expect, real);
	}

2)非静态无依赖方法

代码示例:

被测试类:

public class MyMath {
	public int multi(int num1,int num2) {
		return num1 * num2;
	}
}

测试代码:

@Test
	public void testMulti() {
		int num1 = 2;
		int num2 = 3;
		int expect = 6;
		int real = new MyMath().multi(num1, num2);
		Assert.assertEquals(expect, real);
	}

2.被测试类中方法存在其他可实现依赖

存在其他可实现依赖时,和2.1测试思路一致。

代码示例:

被测试类:

public class MyRectangle {
	int height;
	int width;
	MyMath myMath = new MyMath();
	
	public void setHeight(int height) {
		this.height = height;
	}

	public void setWidth(int width) {
		this.width = width;
	}

	public int getArea() {
		return myMath.multi(width, height);
	}
	
	public int getPerimeter() {
		return myMath.multi(2, MyMath.add(width, height));
	}
}

测试代码:

	@Test
	public void testRectangleGetArea() {
		int width = 2;
		int height = 3;
		MyRectangle rectangle = new MyRectangle();
		rectangle.setWidth(width);
		rectangle.setHeight(height);
		
		int expectedArea = 6;
		int realArea = rectangle.getArea();
		Assert.assertEquals(expectedArea, realArea);
	}

3..被测试类存在难以实现的其他依赖

使用Mock框架,构造出虚拟的依赖,如有需要,可对虚拟依赖进行打桩以满足测试要求。

打桩:用来代替依赖的代码,或未实现的代码。对构造出的虚拟对象,定制其行为,以满足测试逻辑要求。

测试service层代码举例:

service层代码:需要依赖dao层类从数据库获取数据。

public class RectangleService {
	RectangleDao rectangleDao = new RectangleDao();//rectangleDao未开发完成
	
	public int getRectangleAreaById(String id) {
		MyRectangle myRectangle = rectangleDao.getRectangleById(id);
		return myRectangle.getArea();
	}
}

Dao层代码:dao层代码未开发或需要连接数据库,不好操作。

public class RectangleDao {
	public MyRectangle getRectangleById(String id) {
		//代码未开发
		return new MyRectangle();
	}
}

测试用例代码:mock dao层对象,并通过打桩方式定制其行为。

方式1:将mock的rectangleDao对象,通过java反射方式设置到rectangleService对象中


@RunWith(PowerMockRunner.class)
public class RectangleServiceTest {
	/**
	 * 此处仅测试RectangleService类代码逻辑,因此该类的依赖需要mock出来,并打桩(自定义对象的行为)
	 */
	@Test
	public void testRectangleService() throws Exception{
		//构造service内部依赖的rectangleDao对象,
		RectangleDao rectangleDao = PowerMockito.mock(RectangleDao.class);
		PowerMockito.when(rectangleDao.getRectangleById("1")).thenReturn(new MyRectangle(2,3));
		
		//通过反射的方式,将mock出来的rectangleDao配置到rectangleService中
		RectangleService rectangleService = new RectangleService();
		Field field = rectangleService.getClass().getDeclaredField("rectangleDao");
		field.setAccessible(true);
		field.set(rectangleService, rectangleDao);
		
		//构造期望数据,计算实际数据,比对两者
		MyRectangle myRectangle = new MyRectangle(2,3);
		int expectedArea = myRectangle.getArea();
		int actualArea = rectangleService.getRectangleAreaById("1");
		Assert.assertEquals(expectedArea, actualArea);
	}
}

方式2:将mock的rectangleDao对象,通过注解的方式设置到rectangleService对象中

Note:

  • @Mock注解:创建一个Mock对象。
  • @InjectMocks注解:创建一个实例对象,将其他用@Mock、@Spy注解创建的对象注入到用该实例中。
  • @Before注解:junit中的注解,表示每一个@Test注解测试用例执行前,都会执行一遍。
 /**
* 此处仅测试RectangleService类代码逻辑,因此该类的依赖需要mock出来,并打桩(自定义对象的行为)
*/
@RunWith(PowerMockRunner.class)
public class RectangleServiceTest {
	@InjectMocks  //将其他用@Mock(或@Spy)注解创建的对象设置到下面对象中
	RectangleService rectangleService;//创建bean(类似new RectangleService)
	@Mock
	RectangleDao rectangleDao;
	
	@Before
	public void setUp() {
		MockitoAnnotations.initMocks(this);//初始化上面@Mock和@InjectMocks标注对象
	}
	
	@Test
	public void testRectangleService() throws Exception{
		//打桩
		PowerMockito.when(rectangleDao.getRectangleById("1")).thenReturn(new MyRectangle(2,3));
		//构造期望数据,计算实际数据,比对两者
		MyRectangle myRectangle = new MyRectangle(2,3);
		int expectedArea = myRectangle.getArea();
		//调用实际数据并对比
		int actualArea = rectangleService.getRectangleAreaById("1");
		Assert.assertEquals(expectedArea, actualArea);
	}
}

显然,第2种方式更加方便,尤其是被测试类中依赖许多其他对象时,注解方式更加高效。

2.2 依赖框架的java代码

  • spring-boot框架中,类的依赖通过@Autowired注入,而非new创建,但两者本质一样。在springboot框架中,同样可以使用2.3中的方式进行单元测试。
  • 此外,在springboot框架中还可以使用spring-boot-test包进行单元测试。

1.被测试类不存在难以实现的其他依赖

可直接使用junit测试包对代码逻辑进行测试,参考2.1章节。

2.被测试类存在难以实现的其他依赖

方式1:类似2.1.3,直接使用powermock框架对代码进行测试

代码示例:

service层:依赖dao层方法。

和无框架代码区别在于,springboot框架内开发的bean都交给spring IOC容器管理,使用时直接注入而非new对象,但两者本质一样。

@Service
public class RectangleService {
	@Autowired
	RectangleDao rectangleDao;//rectangleDao未开发完成
	
	public int getRectangleAreaById(String id) {
		MyRectangle myRectangle = rectangleDao.getRectangleById(id);
		return myRectangle.getArea();
	}
}

dao层:和数据库交互。

@Component
public class RectangleDao {
	public MyRectangle getRectangleById(String id) {
		//代码未开发
		return new MyRectangle();
	}
}

测试用例:


@RunWith(PowerMockRunner.class)
public class RectangleServiceTest {
	@InjectMocks  //将其他用@Mock(或@Spy)注解创建的对象设置到下面对象中
	RectangleService rectangleService;//创建bean(类似new RectangleService)
	@Mock
	RectangleDao rectangleDao;
	
	@Before
	public void setUp() {
		MockitoAnnotations.initMocks(this);//初始化上面@Mock和@InjectMocks标注对象
	}
	
	@Test
	public void testRectangleService() throws Exception{
		//打桩
		PowerMockito.when(rectangleDao.getRectangleById("1")).thenReturn(new MyRectangle(2,3));
		//构造期望数据,计算实际数据,比对两者
		MyRectangle myRectangle = new MyRectangle(2,3);
		int expectedArea = myRectangle.getArea();
		//调用实际数据并对比
		int actualArea = rectangleService.getRectangleAreaById("1");
		Assert.assertEquals(expectedArea, actualArea);
	}
	
}

方式2:使用spring-boot-test框架对代码进行测试

代码示例:

使用springboot-test的注解。

@RunWith(SpringRunner.class)
@SpringBootTest
public class RectangleServiceTest2 {
	@Autowired  //注入spring IOC容器管理的bean
	RectangleService rectangleService;//创建bean(类似new RectangleService)
	@MockBean  //mock对象
	RectangleDao rectangleDao;
	
	@Test
	public void testRectangleService() throws Exception{
		//打桩
		Mockito.when(rectangleDao.getRectangleById("1")).thenReturn(new MyRectangle(2,3));
		//构造期望数据,计算实际数据,比对两者
		MyRectangle myRectangle = new MyRectangle(2,3);
		int expectedArea = myRectangle.getArea();
		//调用实际数据并对比
		int actualArea = rectangleService.getRectangleAreaById("1");
		Assert.assertEquals(expectedArea, actualArea);
	}
	
}

三、PowerMock框架核心方法

 3.1 PowerMock创建mock对象

T PowerMock.mock(Class<T> type);//创建模拟对象,支持final和native方法

public static <T> void spy(Class<T> type);//创建真实对象

代码示例:

//方式1:注解
@Mock
RectangleDao rectangleDao;
//方式2:创建
RectangleDao rectangleDao = PowerMockito.mock(RectangleDao.class);

//方式1:注解
@Spy
RectangleDao rectangleDao;
//方式2:创建
RectangleDao rectangleDao = PowerMockito.spy(new RectangleDao);

3.2 对mock对象方法进行打桩

  • 当使用PowerMockito.whenNew方法时,必须加注解@PrepareForTest和@RunWith。注解@PrepareForTest里写的类是需要mock的new对象代码所在的类。
  • 当需要mock final方法的时候,必须加注解@PrepareForTest和@RunWith。注解@PrepareForTest里写的类是final方法所在的类。 
  • 当需要mock静态方法的时候,必须加注解@PrepareForTest和@RunWith。注解@PrepareForTest里写的类是静态方法所在的类。
  • 当需要mock私有方法的时候, 只是需要加注解@PrepareForTest,注解里写的类是私有方法所在的类
  • 当需要mock系统类的静态方法的时候,必须加注解@PrepareForTest和@RunWith。注解里写的类是需要调用系统方法所在的类

(1)mock非final类(接口、普通类、虚基类)的非final方法:

先mock对象,然后对mock的对象进行打桩顶起方法行为,最后比对结果。

public class RectangleTest {
    @Test
    public void testObjectNormalMethod(){
        MyRectangle myRectangle = PowerMockito.mock(MyRectangle.class);
        PowerMockito.when(myRectangle.getArea()).thenReturn(6);

        int expectArea = 6;
        int actualArea = myRectangle.getArea();
        Assert.assertEquals(expectArea,actualArea);
    }
}

:其中when(...).thenReturn(...)表示,当对象 rectangle 调用 getArea(0)方法,并且参数为 0 时,返回结果为0,这相当于定制了mock 对象的行为结果.

(2)模拟final类或final方法:

final方法所在的类,需要通过@PrepareForTest注解设置,然后mock对象,再然后打桩(指定方法行为),最后对比。

    @Test
    @PrepareForTest(MyRectangle.class)
    public void testObjectFinalMethod(){
        MyRectangle myRectangle = PowerMockito.mock(MyRectangle.class);
        PowerMockito.when(myRectangle.getFinalArea()).thenReturn(6);

        int expectArea = 6;
        int actualArea = myRectangle.getFinalArea();
        Assert.assertEquals(expectArea,actualArea);
    }

(3)mockStatic方法

static方法所在的类,需要通过@PrepareForTest注解设置,mock静态方法所在的类,再然后打桩(指定方法行为),最后对比。

public static void mockStatic(Class<?> classToMock);//模拟类的静态方法

    @Test
    @PrepareForTest(MyRectangle.class)
    public void testObjectStaticMethod(){
        PowerMockito.mockStatic(AreaUtils.class);
        PowerMockito.when(AreaUtils.getStaticArea(new MyRectangle(2,3))).thenReturn(6);

        int expectArea = 6;
        int actualArea = AreaUtils.getStaticArea(new MyRectangle(2,3));
        Assert.assertEquals(expectArea,actualArea);
    }

(4)mock方法的参数

mock不好实际调用的类,然后打桩mock的参数对象的方法行为,最后对比结果。

AreaUtils类:

public class AreaUtils {
    public static int getStaticArea(MyRectangle rectangle){
        return rectangle.height * rectangle.width;
    }
    public boolean callArgumentInstance(File file) {
        return file.exists();
    }
}

 测试代码:

    @Test
    public void testMockObject() {
        File file = PowerMockito.mock(File.class);//假设file对象不好实现,这里构造一个file对象
        AreaUtils areaUtils = new AreaUtils();

        PowerMockito.when(file.exists()).thenReturn(true);//定制file对象的方法行为:file.exists方法时,设置其返回值为true
        Assert.assertTrue(areaUtils.callArgumentInstance(file));//传入构造好的file对象。由于造的file对象的file.exists()方法返回值为true,因此调用demo.call方法返回的就是true
    }

打桩方法总结:

when().thenReturn()

when().thenThrow()

when().thenCallRealMethod()

when(file.exists()).thenThrow(Exception.class);

whenNew(File.class).withArguments("bbb").thenReturn(file);

有关PowerMock测试框架详解的更多相关文章

  1. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  2. ruby - 使用 C 扩展开发 ruby​​gem 时,如何使用 Rspec 在本地进行测试? - 2

    我正在编写一个包含C扩展的gem。通常当我写一个gem时,我会遵循TDD的过程,我会写一个失败的规范,然后处理代码直到它通过,等等......在“ext/mygem/mygem.c”中我的C扩展和在gemspec的“扩展”中配置的有效extconf.rb,如何运行我的规范并仍然加载我的C扩展?当我更改C代码时,我需要采取哪些步骤来重新编译代码?这可能是个愚蠢的问题,但是从我的gem的开发源代码树中输入“bundleinstall”不会构建任何native扩展。当我手动运行rubyext/mygem/extconf.rb时,我确实得到了一个Makefile(在整个项目的根目录中),然后当

  3. ruby - Ruby 的 Hash 在比较键时使用哪种相等性测试? - 2

    我有一个围绕一些对象的包装类,我想将这些对象用作散列中的键。包装对象和解包装对象应映射到相同的键。一个简单的例子是这样的:classAattr_reader:xdefinitialize(inner)@inner=innerenddefx;@inner.x;enddef==(other)@inner.x==other.xendenda=A.new(o)#oisjustanyobjectthatallowso.xb=A.new(o)h={a=>5}ph[a]#5ph[b]#nil,shouldbe5ph[o]#nil,shouldbe5我试过==、===、eq?并散列所有无济于事。

  4. ruby - RSpec - 使用测试替身作为 block 参数 - 2

    我有一些Ruby代码,如下所示:Something.createdo|x|x.foo=barend我想编写一个测试,它使用double代替block参数x,这样我就可以调用:x_double.should_receive(:foo).with("whatever").这可能吗? 最佳答案 specify'something'dox=doublex.should_receive(:foo=).with("whatever")Something.should_receive(:create).and_yield(x)#callthere

  5. ruby - Sinatra:运行 rspec 测试时记录噪音 - 2

    Sinatra新手;我正在运行一些rspec测试,但在日志中收到了一堆不需要的噪音。如何消除日志中过多的噪音?我仔细检查了环境是否设置为:test,这意味着记录器级别应设置为WARN而不是DEBUG。spec_helper:require"./app"require"sinatra"require"rspec"require"rack/test"require"database_cleaner"require"factory_girl"set:environment,:testFactoryGirl.definition_file_paths=%w{./factories./test/

  6. ruby-on-rails - 迷你测试错误 : "NameError: uninitialized constant" - 2

    我遵循MichaelHartl的“RubyonRails教程:学习Web开发”,并创建了检查用户名和电子邮件长度有效性的测试(名称最多50个字符,电子邮件最多255个字符)。test/helpers/application_helper_test.rb的内容是:require'test_helper'classApplicationHelperTest在运行bundleexecraketest时,所有测试都通过了,但我看到以下消息在最后被标记为错误:ERROR["test_full_title_helper",ApplicationHelperTest,1.820016791]test

  7. ruby - 即使失败也继续进行多主机测试 - 2

    我已经构建了一些serverspec代码来在多个主机上运行一组测试。问题是当任何测试失败时,测试会在当前主机停止。即使测试失败,我也希望它继续在所有主机上运行。Rakefile:namespace:specdotask:all=>hosts.map{|h|'spec:'+h.split('.')[0]}hosts.eachdo|host|begindesc"Runserverspecto#{host}"RSpec::Core::RakeTask.new(host)do|t|ENV['TARGET_HOST']=hostt.pattern="spec/cfengine3/*_spec.r

  8. ruby-on-rails - 如何使辅助方法在 Rails 集成测试中可用? - 2

    我在app/helpers/sessions_helper.rb中有一个帮助程序文件,其中包含一个方法my_preference,它返回当前登录用户的首选项。我想在集成测试中访问该方法。例如,这样我就可以在测试中使用getuser_path(my_preference)。在其他帖子中,我读到这可以通过在测试文件中包含requiresessions_helper来实现,但我仍然收到错误NameError:undefinedlocalvariableormethod'my_preference'.我做错了什么?require'test_helper'require'sessions_hel

  9. ruby-on-rails - Cucumber 是否只是 rspec 的包装器以帮助将测试组织成功能? - 2

    只是想确保我理解了事情。据我目前收集到的信息,Cucumber只是一个“包装器”,或者是一种通过将事物分类为功能和步骤来组织测试的好方法,其中实际的单元测试处于步骤阶段。它允许您根据事物的工作方式组织您的测试。对吗? 最佳答案 有点。它是一种组织测试的方式,但不仅如此。它的行为就像最初的Rails集成测试一样,但更易于使用。这里最大的好处是您的session在整个Scenario中保持透明。关于Cucumber的另一件事是您(应该)从使用您的代码的浏览器或客户端的角度进行测试。如果您愿意,您可以使用步骤来构建对象和设置状态,但通常您

  10. ruby-on-rails - 如何调试 cucumber 测试? - 2

    我有:When/^(?:|I)follow"([^"]*)"(?:within"([^"]*)")?$/do|link,selector|with_scope(selector)doclick_link(link)endend我打电话的地方:Background:GivenIamanexistingadminuserWhenIfollow"CLIENTS"我的HTML是这样的:CLIENTS我一直收到这个错误:.F-.F--U-----U(::)failedsteps(::)nolinkwithtitle,idortext'CLIENTS'found(Capybara::Element

随机推荐