Spring Boot对静态方法进行打桩

Unit Test in Spring Boot

Posted by Haven Tong on 2019-12-10

Spring Boot对静态方法进行打桩

问题

在对Spring Boot项目进行测试的时候,会对业务逻辑service层进行测试。而service层的代码可能会使用util中的工具类,而工具方法通常来说都是static类型的方法。问题在于,当对service层的代码进行测试时,我们往往需要对静态方法打桩,返回我们需要的结果。然而,主流的测试框架Mockito并不支持对静态方法的打桩。对于此问题,我们需要寻求其他框架的解决方案

解决

1.

首先,通过mockito官方文档的描述,可以发现:

What are the limitations of Mockito

  • Cannot mock final classes
  • Cannot mock static methods
  • Cannot mock final methods - their real behavior is executed without any exception.
  • Mockito cannot warn you about mocking final methods so be vigilant.

可以看到,Mockito并不支持mock静态方法,同时也有以下的描述:

Can I mock static methods?

No. Mockito prefers object orientation and dependency injection over static, procedural code that is hard to understand & change. If you deal with scary legacy code you can use JMockit or Powermock to mock static methods.

通过这样的描述,我们发现其他框架提供了解决方案:

  • JMockit
  • Powermock

之后,由于网上的回答中,Powermock更加主流,且与Mockito的语法相近,于是考虑通过Powermock框架解决问题。

再次查阅了Powermock在github上的项目主页:,得到以下的说明:

urrently PowerMock supports JUnit and TestNG. There are three different JUnit test executors available, one for JUnit 4.4-4.12, one for JUnit 4.0-4.3. The test executor for JUnit 3 is not avaliable since PowerMock 2.0.

可以看出,Powermock当前最新版本仅支持到JUnit4.12,而无法对JUnit5提供支持。然而,spring-boot-starter-test中,整合的已是JUnit5. 在网上找寻了众多的回答,所有的例子都是通过JUnit4编写的测试脚本。

且通过Powermock测试的脚本结构如下:

1
2
3
4
5
@RunWith(PowerMockRunner.class)
@PrepareForTest( { YourClassWithEgStaticMethod.class })
public class YourTestCase {
...
}

该结构为JUnit4的写法,并不能在JUnit5中使用。我也尝试强行在Spring Boot项目中单独添加JUnit4依赖,然后单独通过JUnit4来运行通过Powermock编写的测试脚本,多次尝试之后依然无法运行。于是转向另一个框架–JMockit。

2.

尝试通过JMockit来测试静态方法。首先查看JMockit是否支持JUnit5,在官网寻找到了答案:

To run tests that use any of the JMockit APIs, use your Java IDE, Maven/Gradle build script, etc. the way you normally would. In principle, any JDK of version 1.7 or newer, on Windows, Mac OS X, or Linux, can be used. JMockit supports (and requires) the use of JUnit (version 4 or 5) or TestNG

既然JMockit支持JUnit5,且可以对静态方法进行打桩,这就是一种可行的解决方案。所以接下来我就去寻找对静态方法进行打桩的实现方案。

在一篇博客中,我发现了解决方案:

  • 首先,需要添加JMockit的依赖,通过mvnrepository添加最新版的JMockit依赖:

    1
    2
    3
    4
    5
    6
    7
    <!-- https://mvnrepository.com/artifact/org.jmockit/jmockit -->
    <dependency>
    <groupId>org.jmockit</groupId>
    <artifactId>jmockit</artifactId>
    <version>1.48</version>
    <scope>test</scope>
    </dependency>
  • 查看博客中给出的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    // 需要测试的类
    public class AppManager {

    public boolean managerResponse(String question) {
    return AppManager.isResponsePositive(question);
    }

    public static boolean isResponsePositive(String value) {
    if (value == null) {
    return false;
    }
    int length = value.length();
    int randomNumber = randomNumber();
    return length == randomNumber ? true : false;
    }

    private static int randomNumber() {
    return new Random().nextInt(7);
    }
    }

    // 对静态方法进行打桩
    @Test
    public void givenAppManager_whenStaticMethodCalled_thenValidateExpectedResponse() {
    new MockUp<AppManager>() {
    @Mock
    public boolean isResponsePositive(String value) {
    return false;
    }
    };

    assertFalse(appManager.managerResponse("Some string..."));
    }
  • 按照类似的写法,完成了我的测试脚本的编写

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    @Test
    @DisplayName("发送邮件内容正确")
    void shouldSendCorrectEmailWhenUserIsNew(){
    // 使用JMockit对静态方法进行打桩
    new MockUp<CheckCodeUtil>(){
    @mockit.Mock
    public String generateCheckCode(){
    return "123456";
    }
    };
    when(redisTemplate.opsForValue()).thenReturn(new ValueOperationsFake());
    ArgumentCaptor<String> emailCaptor = ArgumentCaptor.forClass(String.class);
    ArgumentCaptor<String> subjectCaptor = ArgumentCaptor.forClass(String.class);
    ArgumentCaptor<String> contentCaptor = ArgumentCaptor.forClass(String.class);
    customerService.sendCheckCode("10175101152@stu.ecnu.edu.cn");
    verify(mailService, times(1))
    .sendHtmlMail(emailCaptor.capture(), subjectCaptor.capture(), contentCaptor.capture());
    assertAll(
    () -> assertEquals("10175101152@stu.ecnu.edu.cn", emailCaptor.getValue()),
    () -> assertEquals("Registration from MeetHere", subjectCaptor.getValue()),
    () -> assertEquals("<h1>Welcome to MeetHere!</h1><p>Your check code is <u>" +
    "123456" + "</u></p>", contentCaptor.getValue())
    );
    }
  • 编写完毕后,尝试是否可以运行,但是依然报错:

    1
    java.lang.IllegalStateException: JMockit didn't get initialized; please check the -javaagent JVM initialization parameter was used

    针对报错信息去Google进行查找,大多数的解决方案是:

    1
    @RunWith(JMockit.class)

    这同样是JUnit4的写法。并不能解决我所遇到的问题

  • 再次回到JMockit的官方文档,查看官方文档中给出的运行方法,发现了潜在的解决方案:

    JMockit also requires the -javaagent JVM initialization parameter to be used; when using the Maven Surefire plugin for test execution, it’s specified as follows:

    所以看来想要运行JMockito编写的测试脚本,需要指定-javaagent的JVM参数才可以。

    官网给出了方案:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <plugins>
    <plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.22.2</version> <!-- or some other version -->
    <configuration>
    <argLine>
    -javaagent:${settings.localRepository}/org/jmockit/jmockit/${jmockit.version}/jmockit-${jmockit.version}.jar
    </argLine>
    </configuration>
    </plugin>
    </plugins>

    将这段xml复制到pom文件中,并将${jmockit.version}替换为项目中使用的1.48

  • 再次运行测试脚本:

    屏幕快照 2019-12-10 下午6.12.39

    BINGO!