How do I unit-test the controller of a Spring Boot application with a DataSource dependency in a @ComponentScan @Configuration

Viewed 1214

Consider the following basic Spring Boot application:

@SpringBootApplication
@ComponentScan(basePackages = "webmvctestproblem.foo")
public class Application {
  public static void main(String[] args) {
    run(Application.class, args);
  }
}

It contains only two other beans. One controller:

@RestController
class Greeter {
  @GetMapping("/")
  String greet() {
    return "Hello, world!";
  }
}

And one configuration in webmvctestproblem.foo containing a DataSource dependency:

@Configuration
class Bar {
  @Autowired
  private DataSource dataSource;
}

Running the application normally (through gradlew bootrun, e.g.) succeeds. Thus, confirming that the app is configured correctly under normal conditions.

However, running the following test causes a runtime error because Spring still attempts to resolve the data source bean dependency on the configuration class:

@RunWith(SpringRunner.class)
@WebMvcTest
public class GreeterTest {
  @Test
  public void test() throws Exception {
  }
}

Of course, there isn't one to resolve because the test is a @WebMvcTest that is designed to create only MVC-related beans.

How do I get rid of the error? I have already tried excluding the configuration class using the excludeFilters attribute of the existing @WebMvcTest annotation and a new @ComponentScan annotation on the test itself. I don't want to resort to turning it into an integration test with @SpringBootTest.

(The project is also available on GitHub for convenience.)

5 Answers

If the DataSource is not mandatory for the test run, simply mock the DataSource with @MockBean in the test class.

@RunWith(SpringRunner.class)
@WebMvcTest
public class GreeterTest {
    @Autowired
    private MockMvc mockMvc;
    @MockBean
    private DataSource dataSource;

    @Test
    public void shouldGreet() throws Exception {
        mockMvc
                .perform(get("/"))
                .andExpect(content().string("Hello, world!"));
    }
}

Spring will automatically create a Mock for DataSource and inject it into the running test application context.


Based on your source code it works.

(Btw: Your source code has a minor issue. The Greeter controller class is in the base package but the component scan only scans on the "foo" package. So there will be no Greeter controller on the test run if this isn't fixed.)

@WebMvcTest creates a "slice" of all the beans relevant to WebMvc Testing (Controllers, Json conversion related stuff and so forth).

You can examine the defaults in org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTypeExcludeFilter

In order to find which beans are actually supposed to be Run Spring must resolve them somehow, right?

So spring test tries to understand what should be loaded and what not by passing through these filters.

Now, if you mark anything with @Configuration spring "knows" that this is the place where the place should be found. So it will load the configuration and then will check which beans defined in this configuration must actually be loaded. However the object of configuration itself must be loaded anyway.

Now the process of loading the configuration object includes injecting stuff into these configurations - this is lifecycle of object creation of spring. And this is a source of mistake here:

@Configuration
class Bar {
  @Autowired
  private DataSource dataSource;
}

Spring loads Bar and tries as a part of loading this object to autowire the data source. This fails since the DataSource Bean itself is excluded by filters.

Now in terms of solution:

First of all, why do you need this DataSource to be autowired in the Configuration object? Probably you have the bean that uses it, lets call it "MyDao", otherwise I don't see a point of such a construction, since @Configuration-s are basically a place to define bean and you shouldn't put business logic there (if you do - ask a separate question and me/our colleagues will try to help and suggest better implementation).

So I assume you have something like this:

public class MyDao {
   private final DataSource dataSource;

   public MyDao(DataSource dataSource) {
      this.dataSource = dataSource;
   }
}

@Configuration
class Bar {
  @Autowired
  private DataSource dataSource;

  @Bean
  public MyDao myDao() {
     return new MyDao(dataSource);
  }
}

In this case however you can rewrite the configuration in a different way:

@Configuration
class Bar {

  // Note, that now there is no autowired datasource and I inject the parameter in the bean instead - so that the DataSource will be required only if Spring will have to create that MyDao bean (which it won't obviously)
  @Bean
  public MyDao myDao(DataSource dataSource) {
     return new MyDao(dataSource);
  }
}

Now the Bar object will still be created - as I've explained above, but it beans including MyDao of course won't be created, problem solved!

The solution with @Autowired(required=false) provided by @Anish B. should also work - spring will attempt to autowire but won't fail because the data source is unavailable, however you should think whether its an appropriate way to deal with this issue, your decision...

Before you can @Autowire the DataSource bean you need to define the DataSource in some config class or in the properties file. Something like this

spring.datasource.url = jdbc:mysql://localhost/abc
spring.datasource.name=testme
spring.datasource.username=xxxx
spring.datasource.password=xxxx
spring.datasource.driver-class-name= com.mysql.jdbc.Driver 
spring.jpa.database=mysql
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect

Or

@Configuration
public class JpaConfig {   
@Bean
public DataSource getDataSource() 
{
    DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create();
    dataSourceBuilder.driverClassName("org.h2.Driver");
    dataSourceBuilder.url("jdbc:h2:file:C:/temp/test");
    dataSourceBuilder.username("sa");
    dataSourceBuilder.password("");
    return dataSourceBuilder.build();
}

You should use

@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@RunWith(SpringRunner.class)

on your test class, then you can inject

   @Autowired
   private MockMvc mockMvc;

and use it in test

 mockMvc.perform(get("/"))
                .andExpect(status().isOk())
                .andReturn();

i suggest you read the documentation about testing. You can test a spring boot application in 100s of different ways.

WebMvcTest

as suggested by the documentation try, defining what controller class that you want to test in the @WebMvcTest annotation.

@RunWith(SpringRunner.class)
@WebMvcTest(Greeter.class)
public class GreeterTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void shouldGreet() throws Exception {
        mockMvc
            .perform(get("/"))
            .andExpect(content().string("Hello, world!"));
    }
}
Related