How do I unit test Spring Controller using annotations?

I am new to the concept of unit testing with Spring controllers. I'm following some examples I found online and trying to implement their testing strategy. This is my basic controller:

@Controller
public class GreetingController {

    @RequestMapping("/greeting")
    public String greeting(@RequestParam(value = "name2", required = false, defaultValue = "World2") String name2,
                           Model model) {

        model.addAttribute("name", name2);
        return "greeting";
    }

}

This is my unit test:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WebAppConfiguration
public class ControllerGreetingTest {

    private MockMvc mockMvc;

    @Autowired
    GreetingController greetingController;
    @Test
    public void shouldReturnSomething() throws Exception {
        mockMvc.perform(get("/greeting"))
                .andExpect(status().isOk())
                .andExpect(view().name("greeting"));
    }

}

Seems pretty straight forward but I get the following error:

java.lang.IllegalStateException: Neither GenericXmlWebContextLoader nor AnnotationConfigWebContextLoader was able to detect defaults, and no ApplicationContextInitializers were declared for context configuration [ContextConfigurationAttributes@1698539 declaringClass = 'com.practice.demo.ControllerGreetingTest', locations = '{}', classes = '{}', inheritLocations = true, initializers = '{}', inheritInitializers = true, name = [null], contextLoaderClass = 'org.springframework.test.context.ContextLoader']

I'm assuming I have to add a parameter to the @ContextConfiguration annotation but not sure what to include in there.

EDIT = This is what I have so far:

public class ControllerGreetingTest {

    private MockMvc mockMvc;

   @Before
    public void setup(){
        this.mockMvc = standaloneSetup(new GreetingController()).build();
    }

    @Test
    public void shouldReturnDefaultString() throws Exception {
        mockMvc.perform(get("/greeting"))
                .andExpect(status().isOk())
                .andExpect(view().name("greetings"))
                .andExpect(model().attribute("name","World2"));
    }

}

It does the job but it doesn't use any of the Spring annotations like I tried to do before.. this approach is not good so trying to figure out why I keep gettings errors whenever I include the annotations in my test file.

My POM:

<dependencies>
    <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <version>1.5.7.RELEASE</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-mongodb</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.hamcrest</groupId>
        <artifactId>hamcrest-all</artifactId>
        <version>1.3</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <artifactId>hamcrest-core</artifactId>
                <groupId>org.hamcrest</groupId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-core</artifactId>
        <version>1.9.5</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>3.2.3.RELEASE</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <scope>test</scope>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.mongodb/mongo-java-driver -->
        <dependency>
            <groupId>org.mongodb</groupId>
            <artifactId>mongo-java-driver</artifactId>
            <version>3.4.2</version>
        </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>1.5.7.RELEASE</version>
        <scope>test</scope>
    </dependency>

</dependencies>

Upvotes: 3

Views: 5170

Answers (3)

gclaussn
gclaussn

Reputation: 1796

Just add the @ContextConfiguration annotation and refer one or more XML configuration file locations or one or more configuration classes. Otherwise Spring cannot autowire your controller, which should be tested.

Example: You want to test a controller, which uses MyService via @Autowired:

MyControllerTest: Injects the controller, which should be tested using the MyTestConfig configuration class.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {MyTestConfig.class})
@WebAppConfiguration
public class MyControllerTest {

  private MockMvc mockMvc;

  @Autowired
  private MyController controller;

  @Before
  public void setUp() {
    mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
  }

  // Tests
}

MyTestConfig: Returns all beans, which are required for the test. Use Mockito to mock the depedencies of your controller, because we want to test only the controller and not the service layer.

@Configuration
public class MyTestConfig {

  @Bean
  public MyService myService() {
    return Mockito.mock(MyService.class);
  }

  @Bean
  public MyController myController() {
    return new MyController();
  }
}

Upvotes: 1

barbakini
barbakini

Reputation: 3154

You should use spring-boot-starter-test dependency. It has almost everything for testing.

And for testing controller part of a spring application, you should use @WebMvcTest annotation for your test class. With this annotation spring will load context just for controller part. Plus you don't need to setup method if you use this annotation. You can simply autowire mockMvc. Your test class should be like this:

@RunWith(SpringRunner.class)
@WebMvcTest(GreetingController.class)
public class ControllerGreetingTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private SomeServiceClass someServiceClass;

    @Test
    public void shouldReturnDefaultString() throws Exception {
        mockMvc.perform(get("/greeting"))
            .andExpect(status().isOk())
            .andExpect(view().name("greetings"))
            .andExpect(model().attribute("name","World2"));
    }

}

Note: Your controller does not have any autowired fields. In cases that controller has some autowired objects like service or repository objects. you can simply mock them with annotation @MockBean as you can see above code.

See this link for other test slice annotations spring provided

Upvotes: 3

varren
varren

Reputation: 14731

For projects with org.springframework.boot:spring-boot-starter-test can use

@RunWith(SpringRunner.class)
@SpringBootTest(classes = App.class)
public class ControllerGreetingTest {
    ...
}

Where App.class is you main application class annotated with @SpringBootApplication. But you better read the documentation. And if you don't want to include (classes = App.class) part you also can change folder structure

For simple controllers it is possible to perform simple standalone tests

@RunWith(SpringRunner.class)
@WebMvcTest(controllers = ControllerGreeting.class)
public class ControllerGreetingTest {
    @Autowired
    private MockMvc mockMvc;

    ...

}

Upvotes: 2

Related Questions