如何测试 Spring 启动应用程序的主类

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

对于我的初级班 Sonarqube 告诉我,我只有60% 的覆盖率。所以平均覆盖率并不像预期的那样好。

enter image description here

我的 Test 类只是默认类。

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


@Test
public void contextLoads() {
}
}

因此,如何在应用程序的 starter 类中测试主类?

133738 次浏览

You can do something like this

@Test
public void applicationContextLoaded() {
}


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

I had the same goal (having a test that runs the main() method) and I noticed that simply adding a test method like @fg78nc said will in fact "start" the application twice : once by spring boot test framework, once via the explicit invocation of mainApp.main(new String[] {}), which I don't find elegant.

I ended up writing two test classes : one with @SpringBootTest annotation and the empty test method applicationContextLoaded(), another one without @SpringBootTest (only RunWith(SpringRunner.class)) that calls the main method.

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[] {});
}
}

Overall, the application is still started two times, but because there is now two test classes. Of course, with only these two tests methods, it seems overkill, but usually more tests will be added to the class SpringBootApplicationTest taking advantage of @SpringBootTest setup.

All these answers seem overkill.
You don't add tests to make a metric tool happy.
Loading a Spring context of the application takes time. Don't add it in each developer build just to win about 0.1% of coverage in your application.
Here you don't cover only 1 statement from 1 public method. It represents nothing in terms of coverage in an application where thousands of statements are generally written.

First workaround : make your Spring Boot application class with no bean declared inside. If you have them, move them in a configuration class (for make them still cover by unit test). And then ignore your Spring Boot application class in the test coverage configuration.

Second workaround : if you really need to to cover the main() invocation (for organizational reasons for example), create a test for it but an integration test (executed by an continuous integration tool and not in each developer build) and document clearly the test class purpose :

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[] {});
}
}
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>your.awesome.package.Application</mainClass>
</configuration>
</plugin>

If you aim for 100% coverage, one thing you can do is simply not having a main method at all. You still require a class annotated with @SpringBootApplication but it can be empty.

Be warned though as it has its drawbacks and other tools that rely on main can break.

You can Mock SpringApplication since that is a dependency of the method under test. See how here. I.e.

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"});
}


}

I solved in a different way here. Since this method is there only as a bridge to Spring's run, I annotated the method with @lombok.Generated and now sonar ignores it when calculating the test coverage.

Other @Generated annotations, like javax.annotation.processing.Generated or javax.annotation.Generated might also work but I can't test now because my issue ticket was closed.

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);
}


}

Even though this question has been answered extensively I had a use case that is not covered here that is perhaps interesting to share. I am validating some properties at startup and I wanted to assert that the application would fail to start if these properties were configured wrong. In JUnit4 I could have done something like this:

@ActiveProfiles("incorrect")
@SpringBoot
public class NetworkProbeApplicationTest {


@Test(expected=ConfigurationPropertiesBindException.class)
public void contextShouldNotLoadWhenPropertiesIncorrect() {
}
}

But in JUnit5 you can no longer add the "expected" value to your @Test annotation and you have to do it differently. And since I wanted to start the application with an incorrect set of properties I needed to pass in which profile to use as a main() argument. I could not really find this documented anywhere, but passing in arguments through the main() method requires you to prefix your arguments with a double hyphen and separate the key and value with an equals sign. A complete test would look like this:

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));
}
}

This simple mock test for SpringApplication does not invoke any methods but just tests the starter app. [uses 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"});
}
}

In addition to the answers above, here is a unit test of a SpringBoot application's main method for if you are using JUnit 5 and Mockito 3.4+:

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" }); });


}

It verifies that the static method run() on the SpringApplication class is called with the expected String array when we call ElectronicGiftCardServiceApplication.main().

Same idea as awgtek and Ramji Sridaran, but their solutions are for JUnit 4.

Using junit

import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.springframework.boot.SpringApplication;


import static org.assertj.core.api.Assertions.*;
class WebsiteApplicationTests {
    

@Test
void testApplication() {
MockedStatic<SpringApplication> utilities = Mockito.mockStatic(SpringApplication.class);
utilities.when((MockedStatic.Verification) SpringApplication.run(WebsiteApplication.class, new String[]{})).thenReturn(null);
WebsiteApplication.main(new String[]{});
assertThat(SpringApplication.run(WebsiteApplication.class, new String[]{})).isEqualTo(null);
}
}

Add these dependencies in pom.xml

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.8.0</version>
<scope>test</scope>
</dependency>

If the idea is to exclude the SpringApplication class from sonar scan (which is the recommended way of doing it), you can exclude it with the following configuration in the build.gradle

plugins {
id 'org.sonarqube' version '3.4.0.2513'
}
 

 

sonarqube {
properties {
property "sonar.exclusions", "**/*Application.java"
}
}