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() {
}
}
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;
}
- 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());
}
}
}
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());
}
}
}
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.
Top comments (0)