99

I have a spring-boot application where my @SpringBootApplication starter class looks like a standard one. So I created many tests for all my functionalities and send the summary to sonarqube to see my coverage.

For my starter class Sonarqube tells me that I just have 60% coverage. So the average coverage is not good as expected.

enter image description here

My Test class is just the default one.

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

    @Test
    public void contextLoads() {
    }
}

So how can I test my main class in the starter class of my application?

G. Ann - SonarSource Team
  • 22,346
  • 4
  • 40
  • 76
Patrick
  • 12,336
  • 15
  • 73
  • 115
  • You can just mock `SpringApplication` class using mockito and verify it has been called with the correct arguments when you are executing the main `method` – Olivier Boissé Oct 09 '17 at 15:51
  • See also jacoco gradle solution: https://stackoverflow.com/a/43196918/907576 – radistao Jun 15 '18 at 12:49

12 Answers12

127

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[] {});
   }
}
davidxxx
  • 125,838
  • 23
  • 214
  • 215
  • 13
    yeah you are right. Just wanted to make the tool happy. Thanks for your useful workarounds. – Patrick Mar 19 '18 at 07:41
  • 2
    You are very welcome :) And thanks for this interesting question :) – davidxxx Mar 19 '18 at 21:38
  • 2
    @davidxxx I used the same approach and working fine as well. But SonarQube says that method annotated with Test should have at least one assert method. What can be used here. Please suggest. – Rohit Jun 06 '18 at 10:45
  • 8
    @ Rohit You could add a silly assertion such as `Assert.assertTrue(true, "silly assertion to be compliant with Sonar")` – davidxxx Jun 06 '18 at 11:01
  • 2
    I use your second workaround, and it will start a real spring-boot application (in my case which costs almost 20 seconds) and try to connect to a real database defined in in yaml file (which might not be connected successfully everytime) – Sam Su Jun 14 '18 at 03:14
  • I keep getting - org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'myApiController': Unsatisfied dependency expressed through field 'myBusinessImpl' | Could not resolve placeholder 'http.client.connection.timeout' in value "${http.client.connection.timeout}". I tried adding @TestPropertySource("classpath:test-manifest.yml") above my test class but still it is not working – Thiagarajan Ramanathan Jun 21 '19 at 20:22
  • I am afraid that your issue needs a specific post because its resolution requires to have more information (the maven layout, which works, which doesn't work, what you tried so far). – davidxxx Jun 21 '19 at 20:33
  • True. I was using `@SpringBootTest` a lot in my unit test until I found it takes time for loading. – Deqing Sep 03 '21 at 00:45
  • I don't think this answer is correct. It's going to start up the entire container, and if that container is a web based one the test may never end. The question is just about testing the main method for coverage, so the best way of doing this is just to exclude it from the coverage analysis, because, as you pointed out the test is pointless. – PaulNUK Nov 29 '21 at 13:26
  • for anyone reading this answer in 2022: just bypass this class via sonar config: sonar.exclusions=path/to/file/ElectronicGiftCardApplicationApp.java – bladefist Sep 13 '22 at 14:18
  • A good standard approach would be `Assertions.assertDoesNotThrow(() -> MyApplication.main(new String[]{}));` It gives a hint as to how to test main if parameters are included somehow as well. – Ryan Deschamps Mar 22 '23 at 19:01
  • `useMainMethod=ALWAYS` is a much better solution as per https://stackoverflow.com/a/75896841/443422 – Marcin Wisnicki Aug 16 '23 at 18:35
22

You can do something like this

@Test
public void applicationContextLoaded() {
}

@Test
public void applicationContextTest() {
    mainApp.main(new String[] {});
}
fg78nc
  • 4,774
  • 3
  • 19
  • 32
17

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

}
Polyana Fontes
  • 3,156
  • 1
  • 27
  • 41
13

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.

tostao
  • 2,803
  • 4
  • 38
  • 61
marcpa00
  • 131
  • 5
10

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.

djangofan
  • 28,471
  • 61
  • 196
  • 289
Valentijn
  • 119
  • 1
  • 3
6

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>
Siddharth Vaish
  • 402
  • 3
  • 4
3

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

}
awgtek
  • 1,483
  • 16
  • 28
3

A bit late, but actually there's a simpler and cleaner way to get the main method of a SpringBoot application covered by tests.

Add useMainMethod set to ALWAYS or WHEN_AVAILABLE to your @SpringBootTest annotation:

@SpringBootTest(useMainMethod = SpringBootTest.UseMainMethod.ALWAYS)
class MyApplicationTest {
    @Test
    void main() {}
}

If not specified, the default value of useMainMethod is NEVER, hence the main method will never be used when creating the SpringApplication under test.

Setting it to WHEN_AVAILABLE, it will make the main method be used, if it actually exists.

This will make the coverage checks happy and avoid the test to be breaking even when no @SpringBootConfiguration-annotated class exists or that class does not have a main method.

For more details, see: https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTest.java#L193

Marcin Wisnicki
  • 4,511
  • 4
  • 35
  • 57
matteo.cajani
  • 2,495
  • 4
  • 21
  • 19
  • This is the best answer if excluding from coverage is not an option. All the other answers have potential unwanted side-effects. – Marcin Wisnicki Aug 16 '23 at 18:35
2
<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.

Stephen Kennedy
  • 20,585
  • 22
  • 95
  • 108
Mariano LEANCE
  • 1,036
  • 13
  • 10
1

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"});
    }
}
Ramji
  • 101
  • 1
  • 4
1

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"
    }
}
jfk
  • 4,335
  • 34
  • 27
0

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));
    }
}
Dharman
  • 30,962
  • 25
  • 85
  • 135
Jonck van der Kogel
  • 2,983
  • 3
  • 23
  • 30