1. Overview

Testing the main class of a Spring Boot application is essential to ensure that the application starts up correctly. While unit tests often focus on individual components, verifying that the application context loads without issues can prevent runtime errors in production.

In this tutorial, we’ll explore different strategies to test the main class of a Spring Boot application effectively.

2. Setup

To begin, we set up a simple Spring Boot application. We can use Spring Initializr to generate the basic project structure.

2.1. Maven Dependencies

To set up our project, we need the following dependencies:

We add the following dependencies to our pom.xml file:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>4.0.0</version>
    <scope>test</scope>
</dependency>

2.2. Main Application Class

The main application class is the heart of any Spring Boot application. It not only serves as the entry point for the application but also acts as the main configuration class, managing components and setting up the environment. Let’s break down how it’s structured and understand why each part matters.

Here’s what a typical Spring Boot main class looks like:

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

The key elements are:

  1. @SpringBootApplication annotation: This annotation is a shorthand for combining three essential annotations:
    • @Configuration: Marks this class as a source of bean definitions.
    • @EnableAutoConfiguration: Tells Spring Boot to start adding beans based on classpath settings, other beans, and property settings.
    • @ComponentScan: Scans the package where this class is located and registers all Spring components (beans, services, etc.).
    This combination ensures the application is configured correctly without needing extensive XML configurations or manual setups.
  2. main() method: The main() method, as in any Java program, serves as the entry point. Here, it calls SpringApplication.run(), which:
    • Bootstraps the application and loads the Spring context, configuring everything based on the @SpringBootApplication annotation.
    • Starts an embedded web server (like Tomcat or Jetty) if it’s a web application, which means the application can run standalone without needing an external server.
    • Accepts command-line arguments, which can be used to configure profiles (like –spring.profiles.active=dev) or other settings at runtime.
  3. SpringApplication.run(): This method does the heavy lifting to start the application:
    • It creates an ApplicationContext that holds the beans and configuration, allowing Spring to manage all dependencies and components.
    • Any runtime configuration from application properties or command-line arguments is applied here, and it starts up any components that depend on these settings.

2.3. Customizing and Testing the Main Application Class

For most applications, application.properties or application.yml is the preferred place to set configurations, as it keeps the main class clean and organizes settings in a central file. However, we can directly customize certain settings within the main class:

3. Testing Strategies

We’ll explore several strategies to test the main class, from basic context loading tests to mocking and command-line arguments.

3.1. Basic Context Loading Test

The simplest way to test if the application context loads is by using @SpringBootTest without any additional parameters:

@SpringBootTest
public class ApplicationContextTest {
    @Test
    void contextLoads() {
    }
}

Here, @SpringBootTest loads the full application context. If any beans are misconfigured, the test fails, helping us catch issues early. In larger applications, we may consider configuring this test only to load specific beans to speed up execution.

3.2. Testing the main() Method Directly

To cover the main() method for tools like SonarQube, we can test it directly:

public class ApplicationMainTest {
    @Test
    public void testMain() {
        Application.main(new String[]{});
    }
}

This straightforward test verifies that the main() method executes without throwing exceptions. It doesn’t load the entire context but ensures the method doesn’t contain runtime issues.

3.3. Mocking SpringApplication.run()

Starting the entire application context is time-consuming, so to optimize this, we can mock SpringApplication.run() using Mockito.

If we’ve added custom logic around SpringApplication.run in our main() method (e.g., logging, handling arguments, or setting custom properties), it might make sense to test that logic without loading the entire application context. In this case, we can mock SpringApplication.run() to validate the additional behaviors surrounding the call.

From version 3.4.0 onward, Mockito supports static method mocking, which allows us to mock SpringApplication.run(). Mocking SpringApplication.run() can be particularly useful if our main() method includes additional logic that we want to verify without loading the full application context.

To facilitate test isolation, we can refactor the main() method so that the actual startup logic is handled in a separate, testable method. This separation allows us to focus on testing initialization logic without starting the entire application:

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        initializeApplication(args);
    }

    static ConfigurableApplicationContext initializeApplication(String[] args) {
        return SpringApplication.run(Application.class, args);
    }
}

Now, the main() method delegates to initializeApplication(), which we can mock in isolation.

With the refactored initializeApplication() method, we can proceed to mock SpringApplication.run() and verify the behavior without fully starting the application context:

public class ApplicationMockTest {
    @Test
    public void testMainWithMock() {
        try (MockedStatic<SpringApplication> springApplicationMock = mockStatic(SpringApplication.class)) {
            ConfigurableApplicationContext mockContext = mock(ConfigurableApplicationContext.class);
            springApplicationMock.when(() -> SpringApplication.run(Application.class, new String[] {}))
              .thenReturn(mockContext);

            Application.main(new String[] {});

            springApplicationMock.verify(() -> SpringApplication.run(Application.class, new String[] {}));
        }
    }
}

In this test, we’re mocking SpringApplication.run() to prevent the actual application from starting, which saves time and isolates the test. By returning a mock ConfigurableApplicationContext, we handle any interactions within initializeApplication() safely, avoiding real context initialization. Additionally, we use verify() to confirm that SpringApplication.run() is called with the correct arguments, which allows us to validate the startup sequence without requiring a full application context.

This approach is especially helpful when the main() method contains custom startup logic, as it lets us test and verify that logic independently, keeping test execution fast and isolated.

3.4. Using @SpringBootTest With useMainMethod

Starting from Spring Boot 2.2, we can instruct @SpringBootTest to use the main() method when starting the application context:

@SpringBootTest(useMainMethod = SpringBootTest.UseMainMethod.ALWAYS)
public class ApplicationUseMainTest {
    @Test
    public void contextLoads() {
    }
}

Setting useMainMethod to ALWAYS ensures that the main() method runs during the test. This approach can be beneficial in scenarios where:

  • The main() method contains additional setup logic: If our main() method includes any setup or configuration that’s important for the application’s correct startup (such as setting custom properties or additional logging), this test verifies that logic as part of the context initialization.
  • Increased code coverage: This strategy allows us to cover the main() method as part of the test, ensuring that the full startup sequence, including the main() method, is verified in a single test. This can be especially useful when aiming for complete startup verification without writing a separate test to call main() directly.

3.5. Excluding the Main Class From Coverage

If the main class doesn’t contain critical logic, we might choose to exclude it from code coverage reports to focus on more meaningful areas.

To exclude the main() method from code coverage, we can annotate it with @Generated, which is available from the javax.annotation (or jakarta.annotation if using Jakarta EE) package. This approach signals to code coverage tools, such as JaCoCo or SonarQube, that the method should be ignored in coverage metrics:

@SpringBootApplication
public class Application {
    @Generated(value = "Spring Boot")
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

The value attribute in @Generated is required, and it typically indicates the source of the generated code. Here, specifying “Spring Boot” clarifies that this code is part of the Spring Boot startup sequence.

If we prefer a simpler approach and our coverage tool supports it, we can alternatively use @SuppressWarnings(“unused”) on the main() method to exclude it from coverage:

@SpringBootApplication
public class Application {
    @SuppressWarnings("unused")
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

In general, using @Generated is a more reliable approach for code coverage exclusion, as most tools recognize this annotation as an instruction to ignore the annotated code in coverage metrics.

Another option for excluding the main class from coverage is to configure our code coverage tools directly.

For JaCoCo in pom.xml:

<build>
    <plugins>
        <plugin>
            <groupId>org.jacoco</groupId>
            <artifactId>jacoco-maven-plugin</artifactId>
            <version>0.8.7</version>
            <configuration>
                <excludes>
                    <exclude>com/baeldung/mainclasstest/Application*</exclude>
                </excludes>
            </configuration>
        </plugin>
    </plugins>
</build>

For SonarQube (in sonar-project.properties):

sonar.exclusions=src/main/java/com/baeldung/mainclasstest/Application.java

Excluding trivial code from coverage can be more practical than writing tests solely to satisfy coverage metrics.

3.6. Handling Application Arguments

If our application uses command-line arguments, we might want to test the main() method with specific inputs:

public class ApplicationArgumentsTest {
    @Test
    public void testMainWithArguments() {
        String[] args = { "--spring.profiles.active=test" };
        Application.main(args);
    }
}

This test checks whether the application starts correctly with specific arguments. It’s useful when certain profiles or configurations need to be validated.

4. Conclusion

Testing the main class of a Spring Boot application ensures that the application starts correctly and increases code coverage. We explored various strategies, from basic context loading tests to mocking SpringApplication.run(). Depending on the project’s needs, we can choose the approach that best balances test execution time and coverage requirements. Excluding the main class from coverage is also a viable option when the method contains trivial code.

By implementing these tests, we enhance the reliability of our application startup process and catch potential issues early in the development cycle.

The complete source code for this tutorial is available over on GitHub.