2

I'm attempting to run some unit tests and encountering an issue that I am sure stems from a misunderstanding about autowiring. I have a unit test class in which I am trying to use @Autowired on a MockMvc and a REST controller -- both of which end up being null.

I have seen some sources try to explain why this can happen (including this More of Less post and a helpful StackOverflow post that has given me some insight but hasn't completely helped me solve my problem).

Below is relevant source code from a sample project I've made to recreate this problem.

ManagerControllerTest.java

@RunWith(SpringRunner.class)
@WebMvcTest(ManagerController.class)
public class ManagerControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private Manager manager;

    @Autowired
    private ManagerController controller;

    @Test
    public void controllerNotNull() throws Exception {
        assertThat(controller).isNotNull();
    }

    @Test
    public void testStoreSomething() throws Exception {
        String path = "/manager/store-something/";

        MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get(path)
                    .characterEncoding("UTF-8")
                    .contentType(MediaType.APPLICATION_JSON);

        MvcResult result = mockMvc.perform(builder).andReturn();
        assertEquals(HttpStatus.CREATED, result.getResponse().getStatus());
    }
}

The controllerNotNull() test results in

java.lang.AssertionError: Expecting actual not to be null

Although, curiously, when I created a new Gradle project in Java and imported my sample code from this post into it, controllerNotNull() passes.

And testStoreSomething() results in

java.lang.NullPointerException at com.example.sandbox.rest.ManagerControllerTest.testStoreSomething(ManagerControllerTest.java:46)

And here in lies the question: What am I misunderstanding? What am I doing wrong? I can remove the @Autowired from the controller and just instantiate it with new ManagerController() but I am left with MockMvc issue.

ManagerController.java

@Controller
@RequestMapping(value = "/manager/")
public class ManagerController {
    Manager manager = new Manager(new StringStorage());

    @PostMapping(value = "store-something")
    private ResponseEntity<?> storeSomething(String str) {
        manager.storeSomething(str);
        return new ResponseEntity<>(CREATED);
    }
}

Manager.java

public class Manager {
    private final Storage storage;

    public Manager(Storage storage) {
        this.storage = storage;
    }

    public void storeSomething(String str) {
        storage.store(str);
    }
}

Storage.java

public interface Storage {
    void store(String str);
}

StringStorage.java

public class StringStorage implements Storage {
    Map<String, String> stringMap;

    @Override
    public void store(String str) {
        stringMap.put(str, str);
    }
 }

Application.java

@SpringBootApplication
public class Application {

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

}

build.gradle Which has been edited from the original post (to use JUnit4) but the problem remains.

repositories {
    jcenter()
}

apply plugin: 'java'
apply plugin: 'eclipse'

dependencies {
 compile group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '2.0.0.RELEASE'
    compile group: 'org.springframework.boot', name: 'spring-boot-starter-tomcat', version: '2.0.0.RELEASE'

    testCompile group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: '2.0.0.RELEASE'
    testImplementation 'org.junit.jupiter:junit-jupiter-api:4.0.0'
    testImplementation 'org.junit.jupiter:junit-jupiter-params:4.0.0'
    testCompile group: 'org.mockito', name: 'mockito-core', version: '2.17.0'
    testCompile group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: "4.0.0"
    testCompile group: 'org.junit.platform', name: 'junit-platform-launcher', version: "1.3.1"
}
buratino
  • 1,408
  • 2
  • 17
  • 40
  • Not sure about the exact answer but check https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTest.html and read the doc comments it might help you. And in my test I use @ContextConfiguration(classes = {Repository.class, TestConfiguration.class}) so that I can get Repository instance autowired to my test – Ketan Dec 21 '18 at 02:28
  • It would be good if you change Manager.class and StorageService.class to spring component ! else check her to understand more about spring test with mockmvc : https://spring.io/guides/gs/testing-web/ – Abder KRIMA Dec 21 '18 at 03:26
  • 1
    For starters remove the construction of the `Manager` from your controller and use `@Autowired` on the field (or rather use constructor injection which is better). Your manager class should be annotated with `@Component` just as our `StringStorage` bean. Although the latter isn't really necessary. Next you are using JUnit4 in your test but all your dependencies are for Junit5. So either change your test or use JUnit4 as dependencies. Finally you also need the `spring-boot` plugin in your Gradle build (which is lacking some part imho). – M. Deinum Dec 21 '18 at 07:56

2 Answers2

1
@RestController
@RequestMapping(value = "/manager")
public class ManagerController {

    @Autowired
    Manager manager;

    @PostMapping(value = "/store-something")
    private ResponseEntity<?> storeSomething(String str) {
        manager.storeSomething(str);
        return new ResponseEntity<>(CREATED);
    }
}


@Component
public class Manager {

    @Autowired
    private Storage storage;

    public void storeSomething(String str) {
        storage.store(str);
    }
}


public interface Storage {
    void store(String str);
}

@Service
public class StringStorage implements Storage {
    Map<String, String> stringMap = new HashMap<>();

    @Override
    public void store(String str) {
        stringMap.put(str, str);
    }
 }




@SpringBootApplication
public class Application {

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

}

The test class :

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class ApplicationTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void shouldReturnDefaultMessage() throws Exception {
        this.mockMvc.perform(post("/manager/store-something")).andDo(print()).andExpect(status().isOk())
                .andExpect(content().string(containsString("Hello World")));
    }
}
Abder KRIMA
  • 3,418
  • 5
  • 31
  • 54
0

add annotation AutoConfigureMockMvc to your test class

@RunWith(SpringRunner.class)
@AutoConfigureMockMvc
public class ManagerControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private Manager manager;

    @Autowired
    private ManagerController controller;

    @Test
    public void controllerNotNull() throws Exception {
        assertThat(controller).isNotNull();
    }

    @Test
    public void testStoreSomething() throws Exception {
        this.mvc.perform(get("/manager/store-something/"))
        .contentType(MediaType.APPLICATION_JSON)
        .andExpect(status().isOk())

    }

Alternatively you can also use TestRestTemplate with @SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.RANDOM_PORT)

mkjh
  • 1,634
  • 13
  • 25