[转]Powermock学习之基本用法
本文主要内容:
1、@PrepareForTest注释
2、访问私有状态内容
3、抑制(禁止加载),即不运行,不需要的内容
4、测试听众
5、模拟策略
6、模拟系统类
PowerMock是一个Java模拟框架,可用于解决通常认为很难甚至无法测试的测试问题。使用PowerMock,可以模拟静态方法,删除静态初始化程序,允许模拟而不依赖于注入,等等。PowerMock通过在执行测试时在运行时修改字节码来完成这些技巧。PowerMock还包含一些实用程序,可让您更轻松地访问对象的内部状态。
PowerMock由两个扩展API组成。
一个用于EasyMock,一个用于Mockito。要使用PowerMock,您需要依赖这些API中的一个以及测试框架。
目前PowerMock支持JUnit和TestNG。有三种不同的JUnit测试执行程序可供使用,一种适用于JUnit 4.4+,一种适用于JUnit 4.0-4.3,一种适用于JUnit 3(已弃用)。
TestNG有一个测试执行器,它需要版本5.11+,具体取决于您使用的PowerMock版本。
@PrepareForTest这个注释告诉PowerMock准备测试某些类。需要使用此批注定义的类通常是需要进行字节码操作的类。这包括final类,带有final,private,static或本地方法的类,这些方法应该被mock,并且类应该在实例化时返回一个模拟对象。
这个注释可以放在测试类或者单独的测试方法中。
如果放在一个类上,这个测试类中的所有测试方法都将由PowerMock处理(以便测试)。
如果要为单个方法重写此行为,只需在特定测试方法上放置@PrepareForTest注释。例如,如果您想在测试方法A中修改类X,但在测试方法B中希望X完好无损,那么这很有用。在这种情况下,您在方法B上放置@PrepareForTest,并从value()列表中排除类X.
有时你需要准备内部类来进行测试,这可以通过提供应该模拟到fullyQualifiedNames()列表的内部类的完全限定名来完成。
@Target( { ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface PrepareForTest {
Class<?>[] value() default IndicateReloadClass.class;
String[] fullyQualifiedNames() default "";
}
访问内部状态:
使用Whitebox.setInternalState(..)设置一个实例或类的非公共成员。
使用Whitebox.getInternalState(..)得到的实例或类的非公共成员。
使用Whitebox.invokeMethod(..)调用实例或类的非公共方法。
用于使用Whitebox.invokeConstructor(..)私有构造函数创建类的实例。
注意:
所有这些都可以在不使用PowerMock的情况下实现,这只是普通的Java反射。然而,反射需要大量的锅炉代码,并且容易出错,因此PowerMock会为您提供这些实用方法。PowerMock让您可以选择是否重构代码并添加getter / setter方法来检查/更改内部状态,或者是否使用其实用方法在不更改生产代码的情况下完成相同的操作。
抑制不需要的行为:
有时候你想甚至需要抑制某些构造函数,方法或静态初始化器的行为,以便单元测试你自己的代码。一个典型的例子是当你的类需要在某种第三方框架中从另一个类扩展时。当这个第三方类在构造函数中做些什么来阻止你单元测试你自己的代码时,就会出现问题。例如,框架可能会尝试加载一个DLL或出于某种原因访问网络或文件系统。
抑制构造函数:
假设我们要测试ExampleWithEvilParent类的getMessage()方法,看起来好像很简单。但是这个父类试图加载一个dll文件,当你为这个ExampleWithEvilParent类运行一个单元测试时它将不会出现。使用PowerMock,您可以禁止EvilParent的构造函数,以便您可以单元测试ExampleWithEvilParent类。
public class ExampleWithEvilParent extends EvilParent {
private final String message;
public ExampleWithEvilParent(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
public class EvilParent {
public EvilParent() {
System.loadLibrary("evil.dll");
}
}
测试代码如下:
@RunWith(PowerMockRunner.class)
@PrepareForTest(ExampleWithEvilParent.class)
public class ExampleWithEvilParentTest {
@Test
public void testSuppressConstructorOfEvilParent() throws Exception {
//抑制构造函数
suppress(constructor(EvilParent.class));
final String message = "myMessage";
ExampleWithEvilParent tested = new ExampleWithEvilParent(message);
assertEquals(message, tested.getMessage());
}
}
上面的例子在抑制超类构造函数和被测类时起作用。另一种抑制被测试类的构造函数,我们通过Whitebox.newInstance方法实现。例如,如果你自己的代码在它的构造函数中做了一些事情,那么很难进行单元测试。这实例化该类而不调用构造函数。
public class ExampleWithEvilConstructor {
private final String message;
public ExampleWithEvilConstructor(String message) {
System.loadLibrary("evil.dll");
this.message = message;
}
public String getMessage() {
return message;
}
}
通过下面的方式抑制:
ExampleWithEvilConstructor tested = Whitebox.newInstance(ExampleWithEvilConstructor.class);
请注意,您不需要使用@RunWith(..)注释或将类传递给@PrepareForTest注释。这样做并没有伤害,但没有必要。
抑制私有方法:
在某些情况下,你只是想压制一个方法并使其返回一些默认值,在其他情况下,你可能需要压制或模拟一个方法,因为它会阻止你对自己的类进行单元测试。看看下面的组装示例:
public class ExampleWithEvilMethod {
private final String message;
public ExampleWithEvilMethod(String message) {
this.message = message;
}
public String getMessage() {
return message + getEvilMessage();
}
private String getEvilMessage() {
System.loadLibrary("evil.dll");
return "evil!";
}
}
如果System.loadLibrary(“evil.dll”)在测试getMessage()方法时执行语句,则测试将失败。避免这种情况的一个简单方法是简单地抑制该getEvilMessage方法。你可以使用
suppress(method(ExampleWithEvilMethod.class, "getEvilMessage"));
完整的测试如下:
@RunWith(PowerMockRunner.class)
@PrepareForTest(ExampleWithEvilMethod.class)
public class ExampleWithEvilMethodTest {
@Test
public void testSuppressMethod() throws Exception {
//抑制私有方法
suppress(method(ExampleWithEvilMethod.class, "getEvilMessage"));
final String message = "myMessage";
ExampleWithEvilMethod tested = new ExampleWithEvilMethod(message);
assertEquals(message, tested.getMessage());
}
}
禁止静态初始化器:
public class ExampleWithEvilStaticInitializer {
static {
System.loadLibrary("evil.dll");
}
private final String message;
public ExampleWithEvilStaticInitializer(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
这个大家应该比较熟悉,直接贴出完整测试:
@RunWith(PowerMockRunner.class)
@SuppressStaticInitializationFor("org.mycompany.ExampleWithEvilStaticInitializer")
public class ExampleWithEvilStaticInitializerTest {
@Test
public void testSuppressStaticInitializer() throws Exception {
final String message = "myMessage";
ExampleWithEvilStaticInitializer tested = new ExampleWithEvilStaticInitializer(message);
assertEquals(message, tested.getMessage());
}
}
抑制字段:
public class MyClass {
private MyObject myObject = new MyObject();
public MyObject getMyObject() {
return myObject;
}
}
suppress(field(MyClass.class, "myObject"));
注意:
在powermock+mockit中,抑制语法如下:
PowerMockito.suppress(PowerMockito.method(类.class,"方法名"));
测试听众
PowerMock 1.1及以上版本具有测试监听器的概念。测试监听器可用于从测试框架获取事件,例如测试方法开始和结束以及测试执行的结果。这些测试监听器的目的是提供独立于测试框架的方式,通过实现org.powermock.core.spi.PowerMockTestListener并将其传递给PowerMockListener注释来获取和响应这些通知。PowerMock有一些内置的测试监听器供您使用。
1、AnnotationEnabler
考虑下面的一段代码:
@RunWith(PowerMockRunner.class)
@PowerMockListener(AnnotationEnabler.class)
public class PersonServiceTest {
@Mock
private PersonDao personDaoMock;
private PersonService classUnderTest;
@Before
public void setUp() {
classUnderTest = new PersonService(personDaoMock);
}
...
}
使用@Mock注释消除了手动设置和拆卸模拟的需要,这可以最大限度地减少重复测试代码并使测试更具可读性。AnnotationEnabler适用于EasyMock和Mockito API。在EasyMock版本中,如果要创建部分模拟,还可以提供您希望模拟的方法的名称,例如:
@RunWith(PowerMockRunner.class)
@PowerMockListener(AnnotationEnabler.class)
public class PersonServiceTest {
@Mock("getPerson")
private PersonDao personDaoMock;
private PersonService classUnderTest;
@Before
public void setUp() {
classUnderTest = new PersonService(personDaoMock);
}
...
}
这段代码将指示PowerMock创建一个PersonDao只模拟”getPerson”方法的部分模拟。由于EasyMock支持好的和严格的模拟,你可以使用@MockNice和@MockStrict注释来获得这个好处。
在Mockito中,你只是spy(..)用来部分地模拟一个类或实例。
2、FieldDefaulter
此测试监听器实现可用于在每次测试之后为junit测试中的每个成员字段设置默认值。对于许多开发人员来说,使用JUnit时,创建tearDown方法和使所有引用无效的标准过程大致相同(有关此问题的更多信息,请参阅此处)。但是,可以使用FieldDefaulter自动完成此操作。举个例子,假设你有5个合作者想要在你的测试中进行模拟,并且你想确保每次测试之后它们都被设置为null,以允许它们被垃圾收集。所以不要这样做:
@RunWith(PowerMockRunner.class)
public class MyTest {
private Collaborator1 collaborator1Mock;
private Collaborator2 collaborator2Mock;
private Collaborator3 collaborator3Mock;
private Collaborator4 collaborator4Mock;
private Collaborator5 collaborator5Mock;
...
@After
public void tearDown() {
collaborator1Mock = null;
collaborator2Mock = null;
collaborator3Mock = null;
collaborator4Mock = null;
collaborator5Mock = null;
}
...
}
您可以使用FieldDefaulter测试监听器彻底摆脱拆除方法:
@RunWith(PowerMockRunner.class)
@PowerMockListener(FieldDefaulter.class)
public class MyTest {
private Collaborator1 collaborator1Mock;
private Collaborator2 collaborator2Mock;
private Collaborator3 collaborator3Mock;
private Collaborator4 collaborator4Mock;
private Collaborator5 collaborator5Mock;
...
}
模拟策略:
一个模拟策略可以用来更容易地将某些代码与PowerMock单独测试到一个特定的框架中。模拟策略实现可以例如抑制一些方法,抑制静态初始化器或拦截方法调用,并改变它们对于特定框架或一组类或接口的返回值(例如返回模拟对象)。例如,可以实施模拟策略以避免为测试编写重复的设置代码。假设你使用的是框架X,为了让你测试它,需要某些方法总是返回一个模拟实现。也许一些静态初始化器也必须被抑制。而不是在测试之间复制这段代码,写一个可重用的模拟策略是一个好主意。
PowerMock 1.1提供了三种模拟slf4j,java common-logging和log4j的模拟策略。以slf4j为例,假设您有一个如下所示的类:
public class Slf4jUser {
private static final Logger log = LoggerFactory.getLogger(Slf4jUser.class);
public final String getMessage() {
log.debug("getMessage!");
return "log4j user";
}
}
这里我们遇到了一个问题,因为记录器在Slf4jUser类的静态初始化器中被实例化。有时这会导致问题,具体取决于日志配置,因此您想在单元测试中执行的操作是对日志实例进行存根。这是完全可行的,而不使用模拟政策。一种方法是先从我们的测试中禁用Slf4jUser类的静态初始化程序。然后,我们可以创建一个存根或Logger类的一个很好的模拟,并将其注入Slf4jUser实例。但是这还不够,想象一下,我们已经配置了slf4j来使用log4j作为后端日志记录,然后在运行测试时我们会在控制台中显示以下错误:
log4j:ERROR A "org.apache.log4j.RollingFileAppender" object is not assignable to a org.apache.log4j.Appender" variable.
log4j:ERROR The class "org.apache.log4j.Appender" was loaded by
log4j:ERROR [org.powermock.core.classloader.MockClassLoader@aa9835] whereas object of type
log4j:ERROR "org.apache.log4j.RollingFileAppender" was loaded by [sun.misc.Launcher$AppClassLoader@11b86e7].
log4j:ERROR Could not instantiate appender named "R".
为了避免这个错误信息,我们需要准备org.apache.log4j.Appender测试。完整的测试设置将如下所示:
@RunWith(PowerMockRunner.class)
@SuppressStaticInitializationFor("org.myapp.Slf4jUser")
@PrepareForTest( Appender.class)
public class MyTest {
@Before
public void setUp() {
Logger loggerMock = createNiceMock(Logger.class);
Whitebox.setInternalState(Slf4jUser.class, loggerMock);
...
}
...
}这种设置行为将不得不被复制到处理slf4j的所有测试类。相反,您可以使用Slf4j模拟策略来照顾您为此设置。你的测试看起来像这样:
@RunWith(PowerMockRunner.class)
@MockPolicy(Slf4jMockPolicy.class)
public class Slf4jUserTest {
...
}
请注意,我们没有做任何设置来模拟slf4j,这Slf4jMockPolicy需要照顾。
模拟策略也可以像这样链接或嵌套:
@RunWith(PowerMockRunner.class)
@MockPolicy({MockPolicyX.class, MockPolicyY.class})
public class MyTest {
...
}
请注意,链中的后续模拟策略可以覆盖上述策略的行为。在这个例子中,这意味着MockPolicyY可能会覆盖由定义的行为MockPolicyX。如果编写自定义模拟策略,记住这一点很重要。
还可以创建自定义的模拟策略,这里不再叙述。
模拟系统类:
PowerMock 1.2.5及以上版本支持Java系统类中的模拟方法,例如位于java.lang和 http:// java.net 中的模拟方法。这可以在不修改JVM或IDE设置的情况下运行!尽管如此,mock这些类的方式有点不同。通常情况下,你需要准备包含静态方法的类(我们称之为X),但是因为PowerMock不可能为测试准备一个系统类,所以必须采取另一种方法。因此,不是准备X,而是准备在X中调用静态方法的类!我们来看一个简单的例子:
public class SystemClassUser {
public String performEncode() throws UnsupportedEncodingException {
return URLEncoder.encode("string", "enc");
}
}
@RunWith(PowerMockRunner.class)
@PrepareForTest( { SystemClassUser.class })
public class SystemClassUserTest {
@Test
public void assertThatMockingOfNonFinalSystemClassesWorks() throws Exception {
mockStatic(URLEncoder.class);
expect(URLEncoder.encode("string", "enc")).andReturn("something");
replayAll();
assertEquals("something", new SystemClassUser().performEncode());
verifyAll();
}
}