使用Mockito或Jmockit模拟私有静态final字段

63

我正在使用类中的 private static final LOGGER 字段,并且希望 LOGGER.isInfoEnabled() 方法返回 false

我该如何使用 mockito 或 jMockit 模拟静态 final 字段?

我的类是:

  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;

  public class Class1 {

  private static final Logger LOGGER = LoggerFactory.getLogger(Class1.class);

    public boolean demoMethod() {
       System.out.println("Demo started");
       if (LOGGER.isInfoEnabled()) {
         System.out.println("@@@@@@@@@@@@@@ ------- info is enabled");
       } else {
         System.out.println("info is disabled");
       }
       return LOGGER.isInfoEnabled();
    }
  }

它的JUnit是:

import mockit.Mocked;
import mockit.NonStrictExpectations;
import org.mockito.InjectMocks;
import org.mockito.MockitoAnnotations;
import org.slf4j.Logger;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import static org.testng.Assert.*;
import com.source.Class1;

public class MyTest {

  @InjectMocks
  Class1 cls1;

  @BeforeMethod
  public void initMocks() {
    MockitoAnnotations.initMocks(this);
  }

  @Test
  public void test(@Mocked final Logger LOGGER) {

    new NonStrictExpectations() {
      {
        LOGGER.isInfoEnabled();
        result = false;
      }
    };
    assertFalse(cls1.demoMethod());
  }
}

当我运行它时,结果如下:
-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.test.MyTest
Configuring TestNG with: TestNG652Configurator
Demo started
@@@@@@@@@@@@@@ ------- info is enabled
Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 1.9 sec <<< FAILURE! - in com.test.MyTest
test(com.test.MyTest)  Time elapsed: 0.168 sec  <<< FAILURE!
java.lang.AssertionError: expected [false] but found [true]
        at com.test.MyTest.test(MyTest.java:35)


Results :

Failed tests:
  MyTest.test:35 expected [false] but found [true]

Tests run: 1, Failures: 1, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 9.899s
[INFO] Finished at: Mon Jun 08 12:35:36 IST 2015
[INFO] Final Memory: 16M/166M
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:2.18.1:test (default-test) on project JMockDemo: The
re are test failures.
[ERROR]
[ERROR] Please refer to D:\perfoce_code\workspace_kepler\JMockDemo\target\surefire-reports for the individual test results.
[ERROR] -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException

我对jmockit很陌生,但我希望我的junit测试用例能够成功运行。我必须使用JMockit或Mockito,不能使用Powermockito。请帮忙。


您可以为这些元素创建一个setter方法,并在测试时设置一个模拟日志记录器。 - Uwe Allner
是的,它是这样工作的,但我必须使记录器不是final,并添加一个setter方法。有没有不改变实际类的方法来做到这一点? - RaT
4
只需要将 @Mocked Logger 改为 @Capturing Logger,就可以正常工作。 - Rogério
谢谢@Rogério,这个也可以用,现在我有另一个可行的解决方案了 :-) - RaT
请参考 https://dev59.com/Q2445IYBdhLWcg3w4-Fe#60988775,了解如何使用普通的Mockito进行操作。 - Kai
4个回答

71

一种方法是使用反射从字段中去掉final修饰符,然后将LOGGER字段替换为Mocked字段。

public class Class1Test {
    @Test
    public void test() throws Exception {
        Logger logger = Mockito.mock(Logger.class);
        Mockito.when(logger.isInfoEnabled()).thenReturn(false);
        setFinalStatic(Class1.class.getDeclaredField("LOGGER"), logger);
        Class1 cls1 = new Class1();
        assertFalse(cls1.demoMethod());
    }

    static void setFinalStatic(Field field, Object newValue) throws Exception {
        field.setAccessible(true);        
        Field modifiersField = Field.class.getDeclaredField("modifiers");
        modifiersField.setAccessible(true);
        modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
        field.set(null, newValue);
    }
}

1
哇,太棒了,这就是我一直在寻找的解决方案,非常感谢你 :-) - RaT
25
这非常危险,因为它会改变整个类加载器的静态实例。任何后续的测试/类都将具有该模拟记录器的实例。在并发运行测试时也不应使用此功能。这可能会导致测试中的严重多线程问题,请谨慎使用。 - Daniel Marcotte
10
愚蠢的模拟框架,如果我想要模拟它,那么它应该自动为我更改修饰符。 - Junchen Liu
1
@DanielMarcotte:我不同意你的观点,因为getDeclaredField包含accessible标志,因此每次调用它都必须返回一个新实例。请看这篇好文章:https://www.artima.com/weblogs/viewpost.jsp?thread=164042。所以重置标志是不必要的。 - Ilker Cat
1
根据原始帖子,“此解决方案将无法在最新的JDK-11或更高版本中使用,因为在纯净版的JDK-11或更高版本中已经不建议修改final变量的值。” https://dev59.com/OWkw5IYBdhLWcg3ws873 - Konstantin Grigorov
显示剩余6条评论

16

接受的解决方案不应该使用JDK 12。原因可以在这里看到。

使用PowerMockito(已测试版本2.0.9)很容易做到。您可以使用 Whitebox.setInternalState 方法为您完成它。

示例:

Whitebox.setInternalState(MyTestClass.class,“myCar”,carMock);

MyTestClass 是包含字段的类。

myCar 是字段的变量名。

carMock 是您想要传递的一些模拟对象。


这对我很有效,而且简单明了。我不需要从记录器中删除最终修饰符。 - Sasha
2
PowerMockito Whitebox 2.0.9 在访问私有的 static final 属性时会抛出异常: Caused by: java.lang.IllegalAccessException: Can not set static final bla-bla field bla-bla.因此,反射是我认为唯一的解决方案。 - Andrew
如果字段仅为final而不是static,这将无法工作。 - zygimantus

0
将参数中的"@Mocked Logger"更改为"@Capturing Logger"即可使其正常工作。
  @Test
  public void test(@Capturing final Logger LOGGER) {

    new NonStrictExpectations() {
      {
        LOGGER.isInfoEnabled();
        result = false;
      }
    };
    assertFalse(cls1.demoMethod());
  }

0

我认为Mockito或jMockit无法在单元测试时模拟静态final类,因为它们尝试覆盖方法。然而,PowerMockito可以使用反射来模拟静态final类/方法。


1
你不能覆盖final方法。只有在使用反射时才能这样做。 - Joao Esperancinha

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接