在单元测试中,一般不能访问数据库,而某些对象又不容易构造,如
PreparedStatement
,ResultSet
等,此时,可以通过Mock模拟对象解决。
导入依赖:
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito</artifactId>
<version>1.7.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>2.0.7</version>
<exclusions>
<exclusion>
<groupId>org.junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
<exclusion>
<groupId>org.powermock</groupId>
<artifactId>powermock-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.powermock</groupId>
<artifactId>powermock-reflect</artifactId>
</exclusion>
</exclusions>
<scope>test</scope>
</dependency>
需要注意的是:使用PowerMock的时候最好禁用掉Mockito框架的依赖,否则会出现框架版本冲突,产生 java.lang.NoClassDefFoundError: org/mockito/cglib/proxy/MethodInterceptord
异常。
与Mockito框架类似,模拟对象,调用模拟对象的方法时返回模拟的结果。 例如: 定义类:
@Data
public class Animal {
public int animalTypeCount;
public void shout(String arg) {
System.out.println("Animal shout: " + arg);
}
public int getAge(int age) {
System.out.println("Animal's age: " + age);
return age;
}
public String getAnimalTypeCount(int animalTypeCount) {
this.animalTypeCount = animalTypeCount;
return "There are more than " + this.animalTypeCount + " types animal!";
}
}
在测试类中:
public class PowerMockTest {
private Animal animal;
@Before
public void setUp() {
animal = PowerMockito.mock(Animal.class);
}
@Test
public void test1() {
this.animal.shout("aw!");
}
}
注意:此处与Mockito框架用法不同的是,不用使用 @Mock
标注类对象,只需要写明是对哪一个类进行模拟: animal = PowerMockito.mock(Animal.class);
,这句是必须的,否则会报空指针异常。
运行 test1()
控制台并 不会有输出 ,表示mock对象成功。
与Mockito类似,PowerMockito.mock(Class)
是对整个对象的模拟,默认对象中的属性和方法都是空值(默认值),而 PowerMockito.spy(new Object())
是部分模拟,是真实调用方法,并返回方法实际调用的结果。举例如下,新建一个类用于测试:
public class Person {
public static final long HUMAN_POPULATION = 6000000000L;
public static String nation = "CN";
public void say(String arg) {
System.out.println("Person say: " + arg);
}
public static void sing(String song) {
System.out.println("sing: " + song);
}
public static BigDecimal getDreamMoney(int age) {
return new BigDecimal(1.25 * age);
}
public String getPopulation() {
return "This world has more than " + HUMAN_POPULATION + " people!";
}
}
测试类:
public class PowerMockTest {
private Person person;
public void setUp() {
person = PowerMockito.spy(new Person());
}
@Test
public void test2() {
person.say("Hello");
}
}
运行 test2()
后控制台能打印出:
Person say: Hello
PowerMock 的通用语法为:when(mock.method(args)).thenReturn(Object)
。
对于无返回值的方法,语法为:PowerMockito.doNothing().when(mock).method(args);
。
public class PowerMockTest2 {
private Person person;
@Before
public void setUp() {
// person = PowerMockito.mock(Person.class);
person = PowerMockito.spy(new Person());
}
@Test
public void test4() {
person.say("Hello");
PowerMockito.doNothing().when(person).say("Hello");
}
}
控制台输出:
Person say: Hello
可以发现,当进行 PowerMockito.doNothing().when(person).say("Hello");
时,即使是 spy()
,最终也不会真正去调用方法。
测试类:
public class PowerMockTest2 {
private Person person;
@Before
public void setUp() {
person = PowerMockito.spy(new Person());
}
@Test
public void test5() {
System.out.println(person.getPopulation());
PowerMockito.when(person.getPopulation()).thenReturn("Mock Return");
System.out.println(person.getPopulation());
}
}
控制台输出结果:
This world has more than 6000000000 people!
Mock Return
测试类如下:
@RunWith(PowerMockRunner.class)
@PrepareForTest({Person.class}) // 适用于模拟final类或有final, private, static, native方法的类
@PowerMockIgnore("javax.management.*") // 用于解决类加载错误
public class PowerMockTest2 {
private Person person;
@Before
public void setUp() {
PowerMockito.mockStatic(Person.class);
}
@Test
public void test6() throws Exception {
Person.sing("lalala");
PowerMockito.doNothing().when(Person.class, "sing", "lalala");
}
}
控制台输出为空。
当需要mock静态方法(变量)时,需要在测试类上写上 @RunWith(PowerMockRunner.class)
, 和 @PrepareForTest({Person.class})
注解。
并且要使用 PowerMockito.mockStatic(Person.class);
来模拟含有静态方法(变量)的类。
可以发现,当进行 mockStatic()
后,实际上就不去真正调用静态方法了,因此对于无返回值的静态方法,PowerMockito.doNothing().when(Person.class, "sing", "lalala");
并不需要写。
测试类如下:
@RunWith(PowerMockRunner.class)
@PrepareForTest({Person.class})
@PowerMockIgnore("javax.management.*")
public class PowerMockTest2 {
private Person person;
@Before
public void setUp() {
PowerMockito.mockStatic(Person.class);
}
@Test
public void test7() {
System.out.println(Person.getDreamMoney(25));
PowerMockito.when(Person.getDreamMoney(25)).thenReturn(new BigDecimal("1000000"));
System.out.println(Person.getDreamMoney(25));
}
}
控制台输出结果为:
null
1000000
一般在对对象的静态变量进行模拟时,需要在测试类上加上 @SuppressStaticInitializationFor("com.cocohub.powermock.Person")
来抑制类中静态资源的生成。
当JavaBean的变量被static修饰时,测试类中对类静态资源的引用返回值为默认值,例如:
@RunWith(PowerMockRunner.class)
@PrepareForTest({Person.class})
@PowerMockIgnore("javax.management.*")
@SuppressStaticInitializationFor("com.cocohub.powermock.Person")
public class PowerMockTest2 {
private Person person;
@Before
public void setUp() {
PowerMockito.mockStatic(Person.class);
}
@Test
public void test8() {
System.out.println(Person.HUMAN_POPULATION);
System.out.println(Person.nation);
}
}
控制台输出为:
6000000000
null
可以发现,对于静态变量,有final修饰时,变量有值;而当变量只是 static
时,变量为默认值。
注解 @SuppressStaticInitializationFor
的作用在于,抑制类中的静态资源,包括静态属性、静态代码块、静态方法。在工作中,遇见过一些类中会使用静态代码块包裹一些连接数据库的静态方法,
在测试的时候,就需要屏蔽掉这部分静态代码,使用这个注解就能很好的解决,但是这会导致类中其他有用的静态变量也被屏蔽掉。 在不修改被测类的前提下,可以在测试类中对这部分静态资源手动赋值。例如:
@RunWith(PowerMockRunner.class)
@PrepareForTest({Person.class})
@PowerMockIgnore("javax.management.*")
@SuppressStaticInitializationFor("com.cocohub.powermock.Person")
public class PowerMockTest2 {
private Person person;
@Before
public void setUp() {
PowerMockito.mockStatic(Person.class);
// 进行初始化
Person.nation = "JP";
generateLogger();
}
// 初始化被测类中的日志对象
private void generateLogger() throws NoSuchFieldException, IllegalAccessException {
Logger logger = LogManager.getLogger(ValidatorEngine.class);
Class<ValidatorEngine> aClass = ValidatorEngine.class;
Field loggerField = aClass.getDeclaredField("logger");
loggerField.setAccessible(true);
loggerField.set(ValidatorEngine.class, logger);
}
@Test
public void test8() {
System.out.println(Person.HUMAN_POPULATION);
System.out.println(Person.nation);
}
}
控制台输出为:
6000000000
JP
实际上,但凡需要修改private修饰的变量,而没有成员方法可以使用的时候,都可以用反射来修改变量的权限,进而修改变量值。
例如,定义类:
@Data
public class Animal {
private int animalTypeCount;
private String type = "MAMMAL";
public static final int ANIMAL_TYPES = 100;
private static String sexuality = "male";
public void shout(String arg) {
System.out.println("Animal shout: " + arg);
}
public int getAge(int age) {
System.out.println("Animal's age: " + age);
return age;
}
public String getAnimalTypeCount(int animalTypeCount) {
this.animalTypeCount = animalTypeCount;
return "There are more than " + this.animalTypeCount + " types animal!";
}
public String getType() {
System.out.println("This animal' s type is: " + type);
return type;
}
public String getSexuality() {
System.out.println("This animal is " + sexuality);
return sexuality;
}
private void getInfo(String type, int animalTypeCount, String sexuality) {
System.out.println("Animal' s type is: " + type);
System.out.println("Animal' s types count is: " + animalTypeCount);
System.out.println("Animal' s sexuality is: " + sexuality);
}
}
在测试类中修改type的值并获取:
public class PowerMockTest3 {
private Animal animal;
@Before
public void setUp() {
animal = PowerMockito.spy(new Animal());
}
@Test
public void test9() throws NoSuchFieldException, IllegalAccessException {
Class<Animal> aClass = Animal.class;
Field typeField = aClass.getDeclaredField("type");
typeField.setAccessible(true);
typeField.set(animal, "INSECT");
String res = animal.getType();
System.out.println(res);
}
}
控制台输出:
This animal' s type is: INSECT
INSECT
对于static修饰的私有变量,使用反射当然是可以修改该变量的值的,但是也可使用 PowerMock 提供的方法:Whitebox.setInternalState(Class, "fieldName", params...);
。
如:
public class PowerMockTest3 {
private Animal animal;
@Before
public void setUp() {
animal = PowerMockito.spy(new Animal());
}
@Test
public void test10() {
Whitebox.setInternalState(Animal.class, "sexuality", "female");
String res = animal.getSexuality();
System.out.println(res);
}
}
控制台输出为:
This animal is female
female
无论是普通私有方法,还是静态私有方法,都可以用 PowerMockito.doReturn(false).when(mock, "methodName", params...);
来模拟。
public class PowerMockTest3 {
private Animal animal;
@Before
public void setUp() {
animal = PowerMockito.spy(new Animal());
}
@Test
public void test11() throws Exception {
System.out.println(animal.getInfoEntrance());
PowerMockito.doReturn(false).when(animal, "getInfo", "mock-type", 0, "mock-sexuality");
System.out.println(animal.getInfoEntrance());
}
}
控制台输出:
Animal' s type is: MAMMAL
Animal' s types count is: 100
Animal' s sexuality is: male
true
Animal' s type is: mock-type
Animal' s types count is: 0
Animal' s sexuality is: mock-sexuality
false
验证静态方法是否被调用, 包含两步 ,语法为:
PowerMockito.verifyStatic(Class.class, Mockito.times(n));
Class.method();
需要注意的是,在写下需要验证某个类之后,要紧跟该类调用的方法。例如: 定义对象:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
private String name;
private int age;
private List<Course> courses;
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public void setCourse(List<Course> courses) {
this.courses = courses;
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public void rollInGetDefaultLesson() {
Course.initCourse();
}
public void getCourseInfo() {
Course defaultLesson = Course.getDefaultLesson();
this.courses = new ArrayList<>();
this.courses.add(defaultLesson);
for (Course course : this.courses) {
System.out.println("\"courseName: " + course.getCourseName() + ", courseTeacher: " + course.getTeacherName() + ".\"");
}
}
@Override
public String toString() {
return "This student' s info: [name=" + name
+ "][age=" + age + "].";
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Course {
private String courseName;
private String teacherName;
private static String defaultCourse;
private static String defaultTeacher;
public void setCourseName(String name) {
this.courseName = name;
}
public void setTeacherName(String name) {
this.teacherName = name;
}
public static void initCourse() {
defaultCourse = "Security";
defaultTeacher = "Monitor";
}
public static Course getDefaultLesson() {
return new Course(defaultCourse, defaultTeacher);
}
public void talkWithTeacher() {
boolean res = isFallInLove();
if (res) {
System.out.println("This student falls in love");
} else {
System.out.println("This student does not fall in love");
}
}
private boolean isFallInLove() {
return false;
}
@Override
public String toString() {
return "This Course info: {" +
"courseName='" + courseName + '\'' +
", teacherName='" + teacherName + '\'' +
'}';
}
}
测试类:
@RunWith(PowerMockRunner.class)
@PrepareForTest({Student.class, Course.class})
public class PowerMockTest4 {
private Student student;
@Test
public void test14() throws Exception {
PowerMockito.mockStatic(Course.class);
Course defaultLesson = new Course();
defaultLesson.setTeacherName("mock-teacher");
defaultLesson.setCourseName("mock-course");
PowerMockito.doReturn(defaultLesson).when(Course.class, "getDefaultLesson");
Student student = new Student();
student.rollInGetDefaultLesson();
student.getCourseInfo();
PowerMockito.verifyStatic(Course.class, Mockito.times(1));
Course.getDefaultLesson();
}
}
控制台输出:
"courseName: mock-course, courseTeacher: mock-teacher."
当设置 PowerMockito.verifyStatic(Course.class, Mockito.times(100));
而实际上调用没有100次时,会报错:
org.mockito.exceptions.verification.TooLittleActualInvocations:
com.cocohub.powermock.Course.getDefaultLesson();
Wanted 100 times but was 1 time.
验证私有方法是否被调用时,只需要一步就行:PowerMockito.verifyPrivate(mock, Mockito.times(n)).invoke("methodName");
测试类如下:
@RunWith(PowerMockRunner.class)
@PrepareForTest({Student.class, Course.class})
public class PowerMockTest4 {
private Student student;
@Test
public void test15() throws Exception {
Student student = PowerMockito.spy(new Student());
student.talkWithTeacher();
PowerMockito.verifyPrivate(student, Mockito.times(1)).invoke("isFallInLove");
student.talkWithTeacher();
}
}
当 Mockito.times(1)
与实际调用次数不匹配时,会报与上同样的异常:
org.mockito.exceptions.verification.TooLittleActualInvocations:
com.cocohub.powermock.Student.isFallInLove();
Wanted 2 times but was 1 time.
PowerMockito.when(HttpRequestUtil.method()).thenThrow(new RuntimeException("MSG"));
Connection con = PowerMockito.mock(Connection.class);
PowerMockito.doThrow(new RuntimeException("MSG")).when(con).method();
在被模拟类中有创建其他类,并且这些类会访问数据库,在这种情况下,我们可以使用 PowerMockito.whenNew(Class).withNoArguments().thenReturn(object);
来模拟创建对象。
系列方法为: withNoArguments()
, withAnyArguments()
,根据构造方法进行选择。
在使用 whenNew()
时,需要使用注解:@RunWith(PowerMockRunner.class)
例如:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
private String name;
private int age;
private List<Course> courses;
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public void setCourse(List<Course> courses) {
this.courses = courses;
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public void getCourseInfo() {
for (Course course : this.courses) {
System.out.println("\"courseName: " + course.getCourseName() + ", courseTeacher: " + course.getTeacherName() + ".\"");
}
}
@Override
public String toString() {
return "This student' s info: [name=" + name
+ "][age=" + age + "].";
}
}
测试类:
@RunWith(PowerMockRunner.class)
@PrepareForTest({Student.class})
public class PowerMockTest4 {
@Before
public void setUp() throws Exception {
Student studentMock = PowerMockito.spy(new Student());
studentMock.setName("Jerry");
studentMock.setAge(20);
PowerMockito.whenNew(Student.class).withArguments("CoCo", 18).thenReturn(studentMock);
// PowerMockito.whenNew(Student.class).withNoArguments().thenReturn(studentMock);
// PowerMockito.whenNew(Student.class).withAnyArguments().thenReturn(studentMock);
}
@Test
public void test12() {
Student student = new Student("CoCo", 18);
System.out.println(student.toString());
}
}
控制台输出:
This student' s info: [name=Jerry][age=20].
测试类:
@RunWith(PowerMockRunner.class)
public class PowerMockTest4 {
private Student student;
@Before
public void setUp() throws Exception {
student = PowerMockito.spy(new Student());
Course course = PowerMockito.spy(new Course());
course.setCourseName("Java");
course.setTeacherName("Tony");
PowerMockito.whenNew(Course.class).withAnyArguments().thenReturn(course);
}
@Test
public void test12() {
Student student = new Student("CoCo", 18);
System.out.println(student.toString());
}
@Test
public void test13() {
Course course1 = new Course("C++", "Dora");
Course course2 = new Course("Golang", "Eric");
ArrayList<Course> courses = new ArrayList<>();
courses.add(course1);
courses.add(course2);
student.setCourses(courses);
student.getCourseInfo();
}
}
控制台输出:
"courseName: Java, courseTeacher: Tony."
"courseName: Java, courseTeacher: Tony."
通过打断点可以看到,这种方式创建出来的对象类型是通过CGLIB进行代理后的代理对象,而不是原本的Student类型对象。
测试类中,多个测试方法公用了同一个类,导致类中静态属性交叉感染,解决办法如下:
在工作写测试类时,一个测试类中的多个方法都使用到了某个类的一些静态属性,这导致,单独进行方法测试时测试能通过,但是进行总的Run Test时会有个别类报错。 究其原因就是多个测试方法共享了某个类的静态属性资源,导致资源感染。 解决办法很简单,就是在这些使用了静态资源的测试方法首行,对类的静态资源手动初始化。如果这些静态资源还是私有的话,就需要利用反射来修改值:
public class MyTest {
private void initStaticFields() throws NoSuchFieldException, IllegalAccessException {
Class<GetPackageAttachable> aClass = GetPackageAttachable.class;
Field basicPlanCodeToSuppBenTbl = aClass.getDeclaredField("BasicPlanCodeToSuppBenTbl");
Field planCatToSuppBenTbl = aClass.getDeclaredField("PlanCatToSuppBenTbl");
Field riderPlanCodeToSuppBenTbl = aClass.getDeclaredField("RiderPlanCodeToSuppBenTbl");
Field packagePlanCode = aClass.getDeclaredField("PackagePlanCode");
Field packagePlanCodeToSuppBenTbl = aClass.getDeclaredField("PackagePlanCodeToSuppBenTbl");
basicPlanCodeToSuppBenTbl.setAccessible(true);
planCatToSuppBenTbl.setAccessible(true);
riderPlanCodeToSuppBenTbl.setAccessible(true);
packagePlanCode.setAccessible(true);
packagePlanCodeToSuppBenTbl.setAccessible(true);
basicPlanCodeToSuppBenTbl.set(GetPackageAttachable.class, null);
planCatToSuppBenTbl.set(GetPackageAttachable.class, null);
riderPlanCodeToSuppBenTbl.set(GetPackageAttachable.class, null);
packagePlanCode.set(GetPackageAttachable.class, null);
packagePlanCodeToSuppBenTbl.set(GetPackageAttachable.class, null);
}
}