1

I have this quite simple controller class and a simple (jpa) repository. What I want to do is to test it's api but mock it's repository and let it return an object or not depending on the test case.

My problem now is that I don't know how to do that.

I know how to mock a repository and inject it to a controller/service class with @Mock / @InjectMocks / when().return()

But I fail when I want to do the same after doing a request with MockMvc. Any help is highly appreciated

The controller


import java.util.Optional;

@RestController
@Slf4j
public class ReceiptController implements ReceiptsApi {

    @Autowired
    private ReceiptRepository receiptRepository;
    @Autowired
    private ErrorResponseExceptionFactory exceptionFactory;
    @Autowired
    private ApiErrorResponseFactory errorResponseFactory;

    @Override
    public Receipt getReceipt(Long id) {
        Optional<ReceiptEntity> result = receiptRepository.findById(id);
        if (result.isEmpty()) {
            throw invalid("id");
        }
        ReceiptEntity receipt = result.get();
        return Receipt.builder().id(receipt.getId()).purchaseId(receipt.getPurchaseId()).payload(receipt.getHtml()).build();
    }

    private ErrorResponseException invalid(String paramName) {
        return exceptionFactory.errorResponse(
                errorResponseFactory.create(HttpStatus.NOT_FOUND.value(), "NOT_VALID", String.format("receipt with id %s not found.", paramName))
        );
    }
}

And it's test class

@WebMvcTest(ReceiptController.class)
@RestClientTest
public class ReceiptControllerTest {

    @InjectMocks
    private ReceiptController receiptController;
    @Mock
    private ReceiptRepository receiptRepository;
    @Mock
    private ErrorResponseExceptionFactory exceptionFactory;
    @Mock
    private ApiErrorResponseFactory errorResponseFactory;

    private MockMvc mvc;


    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        mvc = MockMvcBuilders.standaloneSetup(
                new ReceiptController())
                      .build();
    }

    @Test
    public void getReceiptNotFoundByRequest() throws Exception {
        mvc.perform(MockMvcRequestBuilders
                            .get("/receipt/1")
                            .header("host", "localhost:1348")
                            .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isNotFound());
    }

    //TODO: Finish this test
    @Test
    public void getReceiptFoundByRequest() throws Exception {
        ReceiptEntity receipt1 = ReceiptEntity.builder().id(99999L).purchaseId(432L).html("<html>").build();
        when(receiptRepository.findById(1L)).thenReturn(Optional.of(ReceiptEntity.builder().id(1L).purchaseId(42L).html("<html></html>").build()));

        ResultActions result = mvc.perform(get("/receipt/1")
                                                   .header("host", "localhost:1348")
                                                   .accept(MediaType.APPLICATION_JSON))
                                       .andExpect(status().isOk());
    }
Martin
  • 113
  • 5
  • 16

2 Answers2

0

Within your setUp() method, you're using Mockito to mock the beans annotated with @Mock and inject them in a new instance of ReceiptController, which is then put into the field annotated with @InjectMocks.

On the next line, you're setting up MockMvc with a new instance of ReceiptController (you use new ReceiptController()), rather than using the instance that Mockito created. That means that all fields within that instance will be null.

This pretty much boils down exactly to Why is my Spring @Autowired field null.


To solve this, you could pass receiptController to MockMvc. In that case, you could also remove the @WebMvcTest and @RestClientTest as you're not using them.

Alternatively, you could setup your test with @RunWith(SpringRunner.class), and properly set up a Spring test by using @Autowired in stead of @InjectMocks and @MockBean in stead of @Mock. In that case, you don't need a setUp() method at all, as MockMvc could be autowired by Spring.

g00glen00b
  • 41,995
  • 13
  • 95
  • 133
0

@WebMvcTest and MockMvc allows you to do integration testing, not unit testing.

They allow you to boot an actual Spring application (using a random port) and test the actual controller class with its dependencies. This means that the variables you declared at the top of your test are not actually used in your current setup.

If you wish to unit-test your controller, you can remove @WebMvcTest, create mock dependencies (like you did) and call your methods directly instead of using MockMvc.

If you really wish to write an integration test, you need to mock your external dependencies (the database for example). You can configure spring to use an embedded database (H2 for example) in the test environment, so that you do not affect your real database.

See an example here : https://www.baeldung.com/spring-testing-separate-data-source

Sherif Behna
  • 613
  • 5
  • 4