【问题标题】:How do I JUnit test a Spring autowired constructor?我如何 JUnit 测试 Spring 自动装配的构造函数?
【发布时间】:2016-10-10 04:28:14
【问题描述】:

我在网上进行了大量搜索,但找不到使用自动装配构造函数进行单元测试的示例。我正在使用 Spring 将属性文件中的值自动装配到我的应用程序中。我想对 MyApp.java 的 start 方法进行单元测试,但我有一个自动装配的构造函数,所以我不知道如何实例化 MyApp。没有自动装配的属性,我在单元测试中这样做:

@Test
public void testStart() {
  try{
   MyApp myApp = new MyApp();
   myApp.start();
   }
   catch (Exception e){
     fail("Error thrown")
   }
}

我不想模拟自动装配,因为我需要从属性文件中获取值并使事情进一步复杂化,我通过注释来配置所有内容。我没有 spring.xml、application-context.xml 或 web.xml 文件。那么我该如何实例化/测试 MyApp 的 start 方法呢?我尝试添加 @RunWith(SpringJUnit4ClassRunner.class) 并自动装配 MyApp myApp,但它会引发有关无法加载应用程序上下文的错误,这些错误无法通过在测试类上实现 ApplicationContextAware 来修复。

这里是 MyApp.java

@Component
public class MyApp {

    private static ApplicationContext applicationContext;
    private static MyAppProperties myAppProperties;

    //Obtain the values from the app.properties file
    @Autowired
    MyApp(MyAppProperties myAppProps){
        myAppProperties = myAppProps;
    }

    public static void main(String[] args) throws Exception {
     // Instantiate the application context for use by the other classes
     applicationContext = new AnnotationConfigApplicationContext("com.my.company");

     start();
    }

    /**
     * Start the Jetty server and configure the servlets
     * 
     * @throws Exception
     */
    public static void start() throws Exception {
        // Create Embedded Jetty server
        jettyServer = new Server();

        // Configure Jetty so that it stops at JVM shutdown phase
        jettyServer.setStopAtShutdown(true);
        jettyServer.setStopTimeout(7_000);

        // Create a list to hold all of the handlers
        final HandlerList handlerList = new HandlerList();

        // Configure for Http
        HttpConfiguration http_config = new HttpConfiguration();
        http_config.setSecureScheme("https");
        http_config.setSecurePort(myAppProperties.getHTTP_SECURE_PORT());
    ....
    }
}

这是我的 app.properties 文件

# Spring Configuration for My application

#properties for the embedded jetty server
http_server_port=12345

这里是 MyAppProperties.java

@Component
public class MyAppProperties implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    //List of values from the properties files to be autowired
    private int HTTP_SERVER_PORT;
    ...

    @Autowired
    public MyAppProperties( @Value("${http_server_port}") int http_server_port, ...){
        this.HTTP_SERVER_PORT = http_server_port;
    }

    /**
     * @return the applicationContext
     */
    public ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    /**
     * @param applicationContext
     *            the applicationContext to set
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    /**
     * @param name
     *            the name to set
     */
    public void setHTTP_SERVER_PORT(String name) {
        JETTY_SERVER_NAME = name;
    }

    /**
     * @return the httpServerPort
     */
    public int getHTTP_SERVER_PORT() {
        return HTTP_SERVER_PORT;
    }
}

这里是 MyAppTest.java

@RunWith(SpringJUnit4ClassRunner.class)
public class MyAppTest implements ApplicationContextAware{

    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext appContext) {
        applicationContext = appContext;    
    }

    @Autowired
    private MyApp myapp;

    @Test
    public void testStart(){
    try {
        if(myapp != null){
            myapp.start();
        }
        else{
            fail("myapp is null");
        }
    } catch (Exception e) {
        fail("Error thrown");
        e.printStackTrace();
    } 
    }
}

更新:这是我的配置类

@Configuration
@Component
public class ApplicationConfig implements ApplicationContextAware {

    private final Logger LOGGER = LoggerFactory.getLogger(ApplicationConfig.class);
    private ApplicationContext applicationContext;

    /**
     * @return the applicationContext
     */
    public ApplicationContext getApplicationContext() {
        LOGGER.debug("Getting Application Context", applicationContext);
        return applicationContext;
    }

    /**
     * @param applicationContext
     *            the applicationContext to set
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    // Needed for @Value
    /**
     * Property sources placeholder configurer.
     *
     * @return the property sources placeholder configurer
     */
    @Bean
    public PropertyPlaceholderConfigurer getPropertyPlaceholderConfigurer() {
        PropertyPlaceholderConfigurer propertyPlaceholderConfigurer = new PropertyPlaceholderConfigurer();
        propertyPlaceholderConfigurer.setLocation(new ClassPathResource("app.properties"));
        return propertyPlaceholderConfigurer;
    }
    ...
}

【问题讨论】:

  • 只添加@RunWith 并且不告诉加载哪个配置会失败。你必须在你的测试类中添加一个public static class,把@Configuration@ComponentScan("your.package")放在上面。在另一个层面上,我建议使用 Spring Boot 来引导您的应用程序并注入/使用属性,因为您似乎正在做 Spring Boot 提供的开箱即用(包括对其的测试支持)。
  • @M.Deinum 不幸的是,由于与其他工具的冲突,我无法选择 Spring Boot。只是为了澄清你是说我应该将我的测试类更改为公共静态类 MyAppTest 还是我应该制作另一个类?当我尝试将 MyAppTest 类设为静态时,我收到一条错误消息,指出它是非法修饰符。
  • 不,你需要添加一个内部配置类。此外,我看不出 Spring Boot 对内部服务器的操作与您自己的操作有什么不同,实际上应该没有任何区别。
  • @M.Deinum 我已经有配置类,抱歉我忘了包含它。我已经编辑了问题以添加它。你能解释一下“笑内部服务器”是什么意思吗?我以前从未听过这句话。
  • 这是自动更正的。我的意思当然是发射而不是

标签: spring unit-testing junit


【解决方案1】:

我们可以使用 jmockito 框架来模拟对象。

通过 Mockito 使用 @InjectMocks 进行依赖注入 您还有 @InjectMocks 注释,它尝试根据类型进行构造函数、方法或字段依赖注入。以下代码是来自 Javadoc 的稍作修改的示例。

// Mockito can construct this class via constructor
public class ArticleManager {
         ArticleManager(ArticleCalculator calculator, ArticleDatabase database) {
        }
}

// Mockito can also perform  method injection
public class ArticleManager {
        ArticleManager() {  }
        void setDatabase(ArticleDatabase database) { }
        void setCalculator(ArticleCalculator calculator) { }
}

// Mockito can also perform  field injection
public class ArticleManager {

    private ArticleDatabase database;
    private ArticleCalculator calculator;
}

以下将是单元测试类。

@RunWith(MockitoJUnitRunner.class)
public class ArticleManagerTest  {
   @Mock private ArticleCalculator calculator;
   @Mock private ArticleDatabase database;
   @Spy private UserProvider userProvider = new ConsumerUserProvider();

   // creates instance of ArticleManager
   // and performs constructor injection on it
   @InjectMocks private ArticleManager manager;

   @Test public void shouldDoSomething() {
           // assume that ArticleManager has a method called initialize which calls a method
           // addListener with an instance of ArticleListener
           manager.initialize();

       // validate that addListener was called
           verify(database).addListener(any(ArticleListener.class));
   }

}

确保您使用的是 @RunWith(MockitoJUnitRunner.class) 如需更多信息,请参阅http://docs.mockito.googlecode.com/hg/1.9.5/org/mockito/InjectMocks.html

【讨论】:

  • 感谢您的回答!如果我模拟 MyApp 构造函数,我还会从属性文件中获取值吗?我尝试测试的方法启动嵌入式码头服务器,它需要从属性文件中获取值来执行此操作。
  • 使用@InjectMocks 被认为是一种不好的做法。例如:lkrnac.net/blog/2014/02/promoting-constructor-field-injection
  • 关于@JonyD 评论:我已经阅读了一些评论,其中大部分与文章相矛盾。你找到最终答案了吗?
猜你喜欢
  • 2011-04-09
  • 1970-01-01
  • 2013-09-28
  • 2013-06-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多