How to test a controller with constructor injection by MockMvc
Asked Answered
E

3

6

I have a controller with constructor injection

@RestController
@RequestMapping("/user")
public class MainController {

    private final UserMapper userMapper; // autowired by constructor below

    public MainController(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    @RequestMapping("/getChannels")
    public String index() {
        LoginUser user = userMapper.getUserByName("admin");
        return "Channels: " + user.getChannels();
    }
}

It's a simple class which is working fine. However when I tried to run a JUnit testing with below class I got an error.

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
public class MainControllerTest {

    private MockMvc mvc;
    private final UserMapper userMapper;

    public MainControllerTest(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    @Before
    public void setUp() throws Exception {
        mvc = MockMvcBuilders.standaloneSetup(new MainController(userMapper)).build();
    }

    ......

The error is:

java.lang.Exception: Test class should have exactly one public zero-argument constructor

I was confused by above error message that how could I inject the userMapper with a zero-argument constructor? I know it's possible to add the @Autowired for userMapper in MainController however the field injection is not recommended. Please could anybody guide me a suitable way for both constructor injection and MockMvc testing. Thanks.

Edgington answered 19/3, 2017 at 8:28 Comment(0)
S
1

Other answers talk about using annotations, but here your problem doesn't have any relation to using annotations. keep in mind as spring 4.3+ you don't need to annotate constructor for dependencies, see more here.

In fact You don't need to try to simulate constructor injection in your Test class (MainControllerTest). All you need is to declaring UserMapper as spring component in your application context and in your test class it will be auto injected in your controller as your running application.

Whats your error mean: All of Junit Test classes as error message says should have exactly one public zero-argument constructor that's because Junit Test suites in cases like your scenario doesn't know how to instantiate Test class.

Salubrious answered 19/3, 2017 at 8:38 Comment(1)
I have another doubt here. Is there a way to get MainController instance in the test class without using 'new' keyword or @Autowired annotation?Truehearted
O
0

Use @Autowire on the field. Field injection is not recommended in production code, because it makes reasoning messy (what was injected where and why). But test contexts (and especially those like this one) are simple, so there the pitfall of hard reasoning is not there.

@Autowired
private final UserMapper userMapper;

public MainControllerTest() { //remove me, implicit
}

This change will fix the problem, as jUnit will have a constructor it understands and will be able to instantiate the test class.

Ovid answered 19/3, 2017 at 8:35 Comment(0)
P
-3

You re missing @Autowired for the MainController constructor and always use the constructor injection as shown below to resolve the issue:

@RestController
@RequestMapping("/user")
public class MainController {

    private final UserMapper userMapper;

    @Autowired
    public MainController(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    //add methods here
}

It is NOT a best practice to use the field injection, look here for more details.

Also, to fix the issue, you need to update your Test class as shown below:

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
public class MainControllerTest {

    private MockMvc mvc;

    @Mock
    private UserMapper userMapper;

    @InjectMocks
    private MainController mainController = new MainController(userMapper);

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
        mvc = MockMvcBuilders.standaloneSetup(mainController)..build();
    }

    ......
}
Pittsburgh answered 19/3, 2017 at 8:37 Comment(3)
I tested your suggested source code. Then I got a NPE in MainController. It was caused by the userMapper generated by @InjectMocks doesn't work. Anyway, I found a solution to replace the annotation of WebAppConfiguration by SpringBootTest. However I would still like to know how to make it workable with out Spring Boot.Edgington
Downvote as the code won't work. NPE is imminent while initializing the mainController field in the test. The field should not be assigned manually as the userMapper field is still null at this location. It is initialized later in setUp() method by initMocks(this). So the general rule is not to assign the field manually if @InjectMocks is present on the field. It is the Mockito responsibility to inject all the mocked dependencies and provide an instantiated object. Not to mention tedvinke.wordpress.com/2014/02/13/…Breastplate
Marcin is right. Especially when you are passing dependencies through constructor the above code does not work. You need to follow the link Marcin has given.. 1. RunWith(MockitoJUnitRunner.class) 2. Remove InjectMocks from the controller. No annotations required for the controller in fact. 3. Initialize the controller in a function annotated with Before.Knavery

© 2022 - 2024 — McMap. All rights reserved.