Search code examples
unit-testingmockitospring-boot-test

UnnecessaryStubbingException on Mockito stubbing in Spring Boot mockmvc and MvcResult result doesn't contain the mocked value


I am writing a unit tests to test REST API endpoints. I am using MockMvc to handle API testing and @InjectMocks to load endpoint and @Mock to mock the service layer. Below are snippets.

BookController

@RestController
@RequestMapping( path="api/v1/book")
public class BookController {

    private final BookService bookService;

    @Autowired
    public BookController (BookService bookService){
        this.bookService = bookService;
    }

    @GetMapping
    public List<Book> getBooks() {
        return bookService.getAllBook();
    }
}

BookService

@Service
public class BookService {
    private final BookRepository bookRepository;    

    @Autowired
    public BookService(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }

    public List<Book> getAllBook() {
        return bookRepository.findAll();
    }
}

BookRepository

@Repository
public interface BookRepository extends JpaRepository<Book, Long> {
    List<Book> findAll();
}

TestClass

@ExtendWith(MockitoExtension.class)
public class BookControllerTest {
    private MockMvc mockMvc;
    @InjectMocks
    private BookController bookController;

    @Mock
    private BookService bookService;

    @BeforeEach
    public void setup() {
        MockitoAnnotations.initMocks(this);
            mockMvc = MockMvcBuilders.standaloneSetup(bookController).build();
    }

    @Test
    public void getAllRecords_success() throws Exception {
        List<Book> records = Arrays.asList(record1, record2, record3);
        Mockito.when(bookService.getAllBook()).thenReturn(records);
        MvcResult result = mockMvc.perform(MockMvcRequestBuilders
                        .get("/api/v1/book")
                        .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andDo(print())
                .andReturn();
    }
}

When I run the test I got UnnecessaryStubbingException. The MvcResult result variable has empty massive rather than a massive of three books (records). So, I believe the cause is that I probably do not understand very well how to use Mockito. Could you please point me in the right direction?

SpringBoot version is 3.2.1.

Thanks a lot!

=============================

Update#1

I have found that if I change Controller like this:

@RestController
@RequestMapping( path="api/v1/book")
public class BookController {

//    private final BookService bookService;
    BookService bookService;

//    @Autowired
//    public BookController (BookService bookService){
//        this.bookService = bookService;
//    }

    @GetMapping
    public List<Book> getBooks(){
        return bookService.getAllBooks();
    }
}

then mocking works and MvcResult result variable has a massive of three books (records). Could any one explain how to work it around since with the updated controller the spring app returns 500 code on sending via postman a get request on '/api/v1/book' endpoint. There is in console:

NullPointerException: Cannot invoke "mypackage.BookService.getAllBooks()" because "this.bookService" is null


Solution

  • You are initializing your mocks twice:

    • once via MockitoExtension (which internally calls initMocks / openMocks)
    • once via explicit call to initMocks

    Double initialization fails if you inject a mock to a final field

    Check this example:

    interface Foo {
    }
    
    class Bar {
        final Foo foo;
    
        Bar(Foo foo) {
            this.foo = foo;
        }
    }
    
    @ExtendWith(MockitoExtension.class)
    public class InitMocksTest {
        @InjectMocks Bar bar;
        @Mock Foo foo;
    
    
        @BeforeEach
        void setup() {
            System.out.println("bar: " + defaultToString(bar));
            System.out.println("foo: " + defaultToString(foo));
            System.out.println("bar.foo: " + defaultToString(bar.foo));
            System.out.println();
    
            MockitoAnnotations.openMocks(this);
            System.out.println("bar: " + defaultToString(bar));
            System.out.println("foo: " + defaultToString(foo));
            System.out.println("bar.foo: " + defaultToString(bar.foo));
        }
    
        public static String defaultToString(Object o) {
            return o.getClass().getName() + "@" + Integer.toHexString(o.hashCode());
        }
    
        @Test
        void dummy() {
        }
    
    }
    

    Output:

    bar: com.so.examples.mockito.Bar@5d332969
    foo: com.so.examples.mockito.Foo$MockitoMock$v4JhQAjO@6ca320ab
    bar.foo: com.so.examples.mockito.Foo$MockitoMock$v4JhQAjO@6ca320ab
    
    bar: com.so.examples.mockito.Bar@5d332969
    foo: com.so.examples.mockito.Foo$MockitoMock$v4JhQAjO@1e34c607
    bar.foo: com.so.examples.mockito.Foo$MockitoMock$v4JhQAjO@6ca320ab
    

    Observe that after second initialization:

    • foo gets a new value
    • bar foo keeps old value
    • foo and bar.foo are not pointing to the same object