原创

单元测试中 PowerMock 的使用记录

PowerMock 的使用

在单元测试中,一般不能访问数据库,而某些对象又不容易构造,如 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 异常。

1. PowerMock 简单用法

与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

2. PowerMock 进阶用法

PowerMock 的通用语法为:when(mock.method(args)).thenReturn(Object)

对于无返回值的方法,语法为:PowerMockito.doNothing().when(mock).method(args);

2.1 模拟普通方法

2.1.1 无返回值

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() ,最终也不会真正去调用方法。

2.1.2 有返回值

测试类:

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

2.2 模拟静态方法

2.2.1 无返回值

测试类如下:

@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"); 并不需要写。

2.2.2 有返回值

测试类如下:

@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

2.3 模拟静态变量

一般在对对象的静态变量进行模拟时,需要在测试类上加上 @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

2.4 模拟私有变量

实际上,但凡需要修改private修饰的变量,而没有成员方法可以使用的时候,都可以用反射来修改变量的权限,进而修改变量值。

2.4.1 普通私有变量

例如,定义类:

@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

2.4.2 静态私有变量

对于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

2.5 模拟私有方法

无论是普通私有方法,还是静态私有方法,都可以用 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

2.6 验证调用

2.6.1 验证静态方法

验证静态方法是否被调用, 包含两步 ,语法为:

  1. PowerMockito.verifyStatic(Class.class, Mockito.times(n));
  2. 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.

2.6.2 验证私有方法

验证私有方法是否被调用时,只需要一步就行: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.

2.7 模拟异常

2.7.1 对于静态方法

PowerMockito.when(HttpRequestUtil.method()).thenThrow(new RuntimeException("MSG"));

2.7.2 对于成员方法

Connection con = PowerMockito.mock(Connection.class);
PowerMockito.doThrow(new RuntimeException("MSG")).when(con).method();

3. 其他用法

3.1 PowerMockito.whenNew()

在被模拟类中有创建其他类,并且这些类会访问数据库,在这种情况下,我们可以使用 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类型对象。

4. 写单元测试中遇见的其他问题

测试类中,多个测试方法公用了同一个类,导致类中静态属性交叉感染,解决办法如下:

在工作写测试类时,一个测试类中的多个方法都使用到了某个类的一些静态属性,这导致,单独进行方法测试时测试能通过,但是进行总的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);
    }
}
Java
  • 作者:CoCo(联系作者)
  • 发表时间:2024-03-28 18:59:36
  • 更新时间:2024-03-28 18:59:36
  • 版权声明 © 原创不易,转载请注明出处
  • 留言