DEV Community

Cover image for Unit Testing the Image Uploader API with JUnit5 and Mockito
Matheus Bernardes Spilari
Matheus Bernardes Spilari

Posted on

Unit Testing the Image Uploader API with JUnit5 and Mockito

In the first article of this series, we walked through building a robust image uploader using Spring Boot, Cloudinary, Docker, and PostgreSQL. We covered everything from setting up the project to making requests to the endpoint that saves the image and info. If you haven't read that article yet, I highly recommend starting there to get a solid foundation of the application we'll be working with.

Now, it's time to ensure our application is reliable and maintains its integrity over time. This brings us to a crucial aspect of software development: testing. In this article, we will focus on writing unit tests for our image uploader API. We'll explore how to mock dependencies, and write tests that cover different parts of our service.

Unit testing not only helps catch bugs early but also ensures that our code is maintainable and scalable. By the end of this article, you'll have a comprehensive suite of tests for your image uploader API, giving you confidence that your application works as expected.

Let's dive into the world of unit testing and make our image uploader API bulletproof!

Setting up

I'm using VSCode with the Extension Pack for Java. Now we are ready to write our tests.

If you are using another IDE, see the support for all of them here in the JUnit5 documentation.

Tests

1. Book Service Tests

Right-click on the BookService class, click on Go to Test, and select the methods you want to generate tests for from the menu.

A similar file, like the one below, will be generated:

import org.junit.jupiter.api.Test;

public class BookServiceTest {
    @Test
    void testAddBook() {

    }
}
Enter fullscreen mode Exit fullscreen mode

Remember, for this article, we are going to use the AAA pattern of testing (Arrange - Act - Assert).

1.1. Mocking properties

@ExtendWith(MockitoExtension.class)
public class BookServiceTest {

    @Mock
    private BookRepository bookRepository;

    @Mock
    private Cloudinary cloudinary;

    @Mock
    private MultipartFile multipartFile;

    @Mock
    private Uploader uploader;

    @Captor
    private ArgumentCaptor<Book> bookArgumentCaptor;

    @InjectMocks
    private BookService bookService;

}
Enter fullscreen mode Exit fullscreen mode
  • The @Mock annotations mock/simulate the behavior of properties or dependencies that are going to be used by the class.
  • The @InjectMocks annotation creates and injects the mocks into the corresponding fields.

1.2. Writing tests

  • Testing a success case (shouldCreateANewBook).
  • Testing a call to the repository (shouldCallRepositorySave).
  • Testing if the upload fail (shouldFailTheUpload).
@ExtendWith(MockitoExtension.class)
public class BookServiceTest {

    @Mock
    private BookRepository bookRepository;

    @Mock
    private Cloudinary cloudinary;

    @Mock
    private MultipartFile multipartFile;

    @Mock
    private Uploader uploader;

    @Captor
    private ArgumentCaptor<Book> bookArgumentCaptor;

    @InjectMocks
    private BookService bookService;

    @Nested
    class AddBook {
        @Test
        void shouldCreateANewBook() throws Exception {
            // Arrange
            Map<String, Object> uploadResult = Map.of("url", "http://example.com/image.jpg");

            when(cloudinary.uploader()).thenReturn(uploader);

            when(uploader.upload(any(File.class), anyMap())).thenReturn(uploadResult);

            Book book = new Book();

            book.setName("Test Book");
            book.setImgUrl(uploadResult.get("url").toString());

            when(bookRepository.save(any(Book.class))).thenReturn(book);

            when(multipartFile.getOriginalFilename()).thenReturn("test.jpg");
            when(multipartFile.getBytes()).thenReturn("test content".getBytes());

            // Act

            Book result = bookService.addBook("Test Book", multipartFile);

            // Assert

            assertNotNull(result);
            assertEquals("Test Book", result.getName());
            assertEquals("http://example.com/image.jpg", result.getImgUrl());
        }

        @Test
        void shouldCallRepositorySave() throws Exception {
            // Arrange
            Map<String, Object> uploadResult = Map.of("url", "http://example.com/image.jpg");

            when(cloudinary.uploader()).thenReturn(uploader);

            when(uploader.upload(any(File.class), anyMap())).thenReturn(uploadResult);

            Book book = new Book();

            book.setName("Test Book");
            book.setImgUrl(uploadResult.get("url").toString());

            when(bookRepository.save(any(Book.class))).thenReturn(book);

            when(multipartFile.getOriginalFilename()).thenReturn("test.jpg");
            when(multipartFile.getBytes()).thenReturn("test content".getBytes());

            // Act
            bookService.addBook("Test Book", multipartFile);

            // Assert
            verify(bookRepository, times(1)).save(bookArgumentCaptor.capture());
            Book capturedBook = bookArgumentCaptor.getValue();
            assertEquals("Test Book", capturedBook.getName());
            assertEquals("http://example.com/image.jpg", capturedBook.getImgUrl());
        }

        @Test
        void shouldFailTheUpload() throws Exception {
            // Arrange
            when(multipartFile.getOriginalFilename()).thenReturn("test.jpg");
            when(multipartFile.getBytes()).thenReturn("test content".getBytes());

            when(cloudinary.uploader()).thenReturn(uploader);
            when(uploader.upload(any(File.class),
                    anyMap())).thenThrow(IOException.class);

            // Act & Assert
            ResponseStatusException exception = assertThrows(ResponseStatusException.class, () -> {
                bookService.addBook("Test Book", multipartFile);
            });

            assertEquals(HttpStatus.BAD_GATEWAY, exception.getStatusCode());
            assertEquals("Failed to upload the file.", exception.getReason());
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Book Controller Tests

  • Testing a success case (shouldReturnSuccess)
  • Testing a fail case (shouldFailToUploadImage)
  • Testing with a missing name parameter (shouldFailWithMissingNameParameter)
  • Testing with a missing imgUrl parameter (shouldFailWithMissingImageParameter)
package cloudinary.upload.imageUpload.controllers;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.server.ResponseStatusException;

import cloudinary.upload.imageUpload.configs.GlobalExceptionHandler;
import cloudinary.upload.imageUpload.entities.Book;
import cloudinary.upload.imageUpload.services.BookService;

@ExtendWith(MockitoExtension.class)
public class BookControllerTest {

    @Mock
    private BookService bookService;

    @InjectMocks
    private BookController bookController;

    private MockMvc mockMvc;

    @Nested
    class AddBook {
        @Test
        void shouldReturnSuccess() throws Exception {
            // Arrange
            MockMultipartFile image = new MockMultipartFile("imgUrl", "test.jpg", MediaType.IMAGE_JPEG_VALUE,
                    "test content".getBytes());

            Book book = new Book();
            book.setName("Test Book");
            book.setImgUrl("http://example.com/image.jpg");

            when(bookService.addBook(any(String.class), any(MockMultipartFile.class))).thenReturn(book);

            mockMvc = MockMvcBuilders.standaloneSetup(bookController).build();

            // Act & Assert
            mockMvc.perform(multipart("/addBook")
                    .file(image)
                    .param("name", "Test Book"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.name").value("Test Book"))
                    .andExpect(jsonPath("$.imgUrl").value("http://example.com/image.jpg"));
        }

        @Test
        void shouldFailToUploadImage() throws Exception {
            // Arrange
            MockMultipartFile image = new MockMultipartFile("imgUrl", "test.jpg", MediaType.IMAGE_JPEG_VALUE,
                    "test content".getBytes());

            when(bookService.addBook(any(String.class), any(MockMultipartFile.class)))
                    .thenThrow(new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR,
                            "Failed to upload the file."));

            mockMvc = MockMvcBuilders.standaloneSetup(bookController).setControllerAdvice(new GlobalExceptionHandler())
                    .build();

            // Act & Assert
            mockMvc.perform(multipart("/addBook")
                    .file(image)
                    .param("name", "Test Book"))
                    .andExpect(status().isInternalServerError())
                    .andExpect(result -> result.getResponse().equals("Failed to upload the file."));
        }

        @Test
        void shouldFailWithMissingNameParameter() throws Exception {
            // Arrange
            MockMultipartFile image = new MockMultipartFile("imgUrl", "test.jpg", MediaType.IMAGE_JPEG_VALUE,
                    "test content".getBytes());

            mockMvc = MockMvcBuilders.standaloneSetup(bookController).build();

            // Act & Assert
            mockMvc.perform(multipart("/addBook")
                    .file(image))
                    .andExpect(status().isBadRequest());
        }

        @Test
        void shouldFailWithMissingImageParameter() throws Exception {
            // Arrange
            mockMvc = MockMvcBuilders.standaloneSetup(bookController).build();

            // Act & Assert
            mockMvc.perform(multipart("/addBook")
                    .param("name", "Test Book"))
                    .andExpect(status().isBadRequest());
        }
    }

}
Enter fullscreen mode Exit fullscreen mode

Conclusion

These are some simple test cases for you to begin testing your app. Remember, we can refactor these tests by adding some factories to avoid repetition.

Thank you for reading.

Reference

JUnit5 - Docs
Mockito - Docs

Top comments (0)