【问题标题】:How to test main class of Spring-boot application如何测试 Spring-boot 应用程序的主类
【发布时间】:2018-03-20 21:19:35
【问题描述】:

我有一个spring-boot 应用程序,其中我的@SpringBootApplication 入门课程看起来像一个标准课程。所以我为我的所有功能创建了许多测试并将摘要发送到sonarqube 以查看我的覆盖范围。

对于我的初学者课程,Sonarqube 告诉我,我只有 60% 的覆盖率。所以平均覆盖率不如预期。

我的测试类只是默认类。

@RunWith(SpringRunner.class)
@SpringBootTest(classes = ElectronicGiftcardServiceApplication.class)
public class ElectronicGiftcardServiceApplicationTests {

    @Test
    public void contextLoads() {
    }
}

那么如何在我的应用程序的起始类中测试我的主类?

【问题讨论】:

  • 您可以使用 mockito 模拟 SpringApplication 类,并在执行主 method 时验证它是否已使用正确的参数调用
  • 另见 jacoco gradle 解决方案:stackoverflow.com/a/43196918/907576

标签: java spring-boot junit


【解决方案1】:

所有这些答案似乎都是多余的。
您不会为了让度量工具满意而添加测试。
加载应用程序的 Spring 上下文需要时间。不要为了在您的应用程序中赢得大约 0.1% 的覆盖率而在每个开发人员构建中添加它。
在这里,您不仅仅涵盖 1 个公共方法中的 1 个语句。在通常会编写数千条语句的应用程序中,它不代表任何覆盖范围。

第一个解决方法:让你的 Spring Boot 应用程序类没有在里面声明 bean。如果您有它们,请将它们移动到配置类中(以使它们仍然被单元测试覆盖)。然后在test coverage configuration.中忽略你的Spring Boot应用类

第二种解决方法:如果您确实需要覆盖 main() 调用(例如出于组织原因),请为其创建一个测试,但要创建一个集成测试(由持续集成工具执行,而不是在每个开发人员构建中执行)和清楚地记录测试类的目的:

import org.junit.Test;

// Test class added ONLY to cover main() invocation not covered by application tests.
public class MyApplicationIT {
   @Test
   public void main() {
      MyApplication.main(new String[] {});
   }
}

【讨论】:

  • 是的,你是对的。只是想让工具开心。感谢您提供有用的解决方法。
  • 不客气 :) 感谢这个有趣的问题 :)
  • @davidxxx 我使用了相同的方法并且工作正常。但是 SonarQube 说用 Test 注释的方法应该至少有一个 assert 方法。这里可以用什么。请提出建议。
  • @Rohit 你可以添加一个愚蠢的断言,例如Assert.assertTrue(true, "silly assertion to be compliant with Sonar")
  • 我使用你的第二个解决方法,它将启动一个真正的 spring-boot 应用程序(在我的情况下,它花费了将近 20 秒)并尝试连接到在 yaml 文件中定义的真实数据库(这可能不是每次都连接成功)
【解决方案2】:

你可以这样做

@Test
public void applicationContextLoaded() {
}

@Test
public void applicationContextTest() {
    mainApp.main(new String[] {});
}

【讨论】:

    【解决方案3】:

    我有同样的目标(有一个运行 main() 方法的测试),我注意到简单地添加一个像@fg78nc 这样的测试方法实际上会“启动”应用程序两次:一次是通过 spring boot 测试框架,一次通过mainApp.main(new String[] {}) 的显式调用,我觉得这并不优雅。

    我最终编写了两个测试类:一个带有@SpringBootTest 注释和空测试方法applicationContextLoaded(),另一个没有@SpringBootTest(仅RunWith(SpringRunner.class))调用main 方法.

    SpringBootApplicationTest

    package example;
    
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.test.context.junit4.SpringRunner;
    import org.springframework.boot.test.context.SpringBootTest;
    
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class SpringBootApplicationTest {
    
      @Test
      public void contextLoads() {
      }
    }
    

    ApplicationStartTest

    package example;
    
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.test.context.junit4.SpringRunner;
    
    @RunWith(SpringRunner.class)
    public class ApplicationStartTest {
      @Test
      public void applicationStarts() {
        ExampleApplication.main(new String[] {});
      }
    }
    

    总的来说,应用程序还是启动了两次,但是因为现在有两个测试类。当然,仅使用这两种测试方法似乎有点过头了,但通常会在 SpringBootApplicationTest 类中添加更多测试,以利用 @SpringBootTest 设置。

    【讨论】:

    • 这个解决方案没有给我任何关于 Jacoco 的报道。
    【解决方案4】:

    我在这里以不同的方式解决了问题。由于这个方法只是作为 Spring 运行的桥梁,我用@lombok.Generated 注释了这个方法,现在声纳在计算测试覆盖率时会忽略它。

    其他 @Generated 注释,如 javax.annotation.processing.Generatedjavax.annotation.Generated 也可能有效,但我现在无法测试,因为我的问题单已关闭。

    package com.stackoverflow;
    
    import lombok.Generated;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    public class Application {
    
        @Generated
        public static void main(String... args) {
            SpringApplication.run(Application.class, args);
        }
    
    }
    

    【讨论】:

    • 有趣的想法。我试试看。
    【解决方案5】:

    您可以模拟SpringApplication,因为这是被测方法的依赖项。看看如何here。 即

    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.powermock.core.classloader.annotations.PrepareForTest;
    import org.powermock.modules.junit4.PowerMockRunner;
    import org.springframework.boot.SpringApplication;
    
    import static org.powermock.api.mockito.PowerMockito.mockStatic;
    import static org.powermock.api.mockito.PowerMockito.verifyStatic;
    
    @RunWith(PowerMockRunner.class)
    public class ElectronicGiftcardServiceApplicationTest {
    
        @Test
        @PrepareForTest(SpringApplication.class)
        public void main() {
            mockStatic(SpringApplication.class);
            ElectronicGiftcardServiceApplication.main(new String[]{"Hello", "World"});
            verifyStatic(SpringApplication.class);
            SpringApplication.run(ElectronicGiftcardServiceApplication.class, new String[]{"Hello", "World"});
        }
    
    }
    

    【讨论】:

    • 只是简单的verifyStatic(),没有传递任何参数
    【解决方案6】:

    除了上面的答案,如果您使用的是 JUnit 5 和 Mockito 3.4+,这里是 SpringBoot 应用程序的主要方法的单元测试:

    try (MockedStatic<SpringApplication> mocked = mockStatic(SpringApplication.class)) {
                
       mocked.when(() -> { SpringApplication.run(ElectronicGiftCardServiceApplication.class, 
          new String[] { "foo", "bar" }); })
             .thenReturn(Mockito.mock(ConfigurableApplicationContext.class));
                
       ElectronicGiftCardServiceApplication.main(new String[] { "foo", "bar" });
                
       mocked.verify(() -> { SpringApplication.run(ElectronicGiftCardServiceApplication.class, 
          new String[] { "foo", "bar" }); });
    
    }   
    

    当我们调用 ElectronicGiftCardServiceApplication.main() 时,它会验证 SpringApplication 类上的静态方法 run() 是否使用预期的 String 数组调用。

    与 awgtek 和 Ramji Sridaran 的想法相同,但他们的解决方案适用于 JUnit 4。

    【讨论】:

      【解决方案7】:
      <plugin>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-maven-plugin</artifactId>
          <configuration>
              <mainClass>your.awesome.package.Application</mainClass> 
          </configuration>
      </plugin>
      

      如果您的目标是 100% 的覆盖率,您可以做的一件事就是根本没有 main 方法。您仍然需要使用 @SpringBootApplication 注释的类,但它可以为空。

      请注意,因为它有其缺点,其他依赖 main 的工具可能会崩溃。

      【讨论】:

        【解决方案8】:

        这个 SpringApplication 的简单模拟测试不调用任何方法,而只是测试启动应用程序。 [使用 PowerMockRunner.class]

        import org.junit.Test;
        import org.junit.runner.RunWith;
        import org.powermock.api.mockito.PowerMockito;
        import org.powermock.core.classloader.annotations.PowerMockIgnore;
        import org.powermock.core.classloader.annotations.PrepareForTest;
        import org.powermock.modules.junit4.PowerMockRunner;
        import org.springframework.boot.SpringApplication;
        
        @RunWith(PowerMockRunner.class)
        @PowerMockIgnore({"com.sun.org.apache.xerces.*", "javax.xml.*", "org.xml.*", "javax.management.*"})
        public class JobsAppStarterTest {
        
            @Test
            @PrepareForTest(SpringApplication.class)
            public void testSpringStartUp() {
                PowerMockito.mockStatic(SpringApplication.class);
                SpringApplication.run(JobsAppStarter.class, new String[] {"args"});
                JobsAppStarter.main(new String[] {"args"});
            }
        }
        

        【讨论】:

        • Powermockito 不再需要。我认为 Mockito 3.4+ 可以做到这一点。
        【解决方案9】:

        尽管这个问题已经得到了广泛的回答,但我有一个未在此处介绍的用例,可能值得分享。我在启动时验证了一些属性,我想断言如果这些属性配置错误,应用程序将无法启动。在 JUnit4 中,我可以这样做:

        @ActiveProfiles("incorrect")
        @SpringBoot
        public class NetworkProbeApplicationTest {
        
            @Test(expected=ConfigurationPropertiesBindException.class)
            public void contextShouldNotLoadWhenPropertiesIncorrect() {
            }
        }
        

        但在 JUnit5 中,您不能再将“预期”值添加到 @Test 注释中,您必须以不同的方式进行操作。因为我想用一组不正确的属性启动应用程序,所以我需要传入哪个配置文件作为 main() 参数。我在任何地方都找不到这个文档,但是通过 main() 方法传入参数需要您在参数前面加上双连字符,并用等号分隔键和值。完整的测试如下所示:

        import org.junit.jupiter.api.Test;
        import org.springframework.boot.SpringApplication;
        import org.springframework.boot.context.properties.ConfigurationPropertiesBindException;
        
        import static org.junit.jupiter.api.Assertions.assertThrows;
        import static org.junit.jupiter.api.Assertions.assertTrue;
        
        public class NetworkProbeApplicationTest {
        
            @Test
            public void contextShouldNotLoadWhenPropertiesIncorrect() {
                Exception exception = assertThrows(ConfigurationPropertiesBindException.class, () -> {
                    SpringApplication.run(NetworkProbeApplication.class, "--spring.profiles.active=incorrect");
                });
        
                String expectedMessage = "Error creating bean with name 'dnsConfiguration': Could not bind properties to 'DnsConfiguration' : prefix=dns";
        
                assertTrue(exception.getMessage().contains(expectedMessage));
            }
        }
        

        【讨论】:

          猜你喜欢
          • 2019-01-21
          • 1970-01-01
          • 1970-01-01
          • 2021-05-27
          • 2019-05-16
          • 2017-01-30
          • 2016-07-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多