This article is a part 2
on a series of 2 articles, where in the first one I showed you how to upload and download an image into an SQL database.
You can find that article at the link below ππ½
In this article you will learn how to write unit, integration and end-to-end tests for image management with SQL (Postgres) Database and Spring Boot.
Prerequisite
While the blog post may provide some effective explanations, having all the points listed below in check beforehand of this will enhance the reading experience and enable readers to grasp the concepts and ideas more effectively.
- Mockito Framework
- Testcontainers dependencies
- Docker installed on your computer
Unit Testing
To perform unit testing of our previous tutorial, we should test the business logic of our service layer. To do so, we will use Mockito Framework in our test class.
@ExtendWith(MockitoExtension.class)
public class ImageServiceTest {
@InjectMocks
private ImageService imageService;
@Mock
private ImageRepository imageRepository;
//...
@ExtendWith
is a repeatable annotation that is used to register extensions for the annotated test class, test interface, test method, parameter, or field.@Mock
used for creating mock instances without having to call Mockito.mock manually.@InjectMocks
creates an instance of the class and injects the mocks that are created with the @Mock annotation.Once we have declared our dependencies, we can write our first unit test, to assert that we can actually upload an image.
@Test
@SneakyThrows
public void should_upload_image() {
// Arrange
Image imageUploaded = new Image();
MultipartFile imageFile = new MockMultipartFile("file", "test-image.png", IMAGE_PNG_VALUE, new byte[BITE_SIZE]);
when(imageRepository.save(any(Image.class))).thenReturn(imageUploaded);
try (MockedStatic<ImageUtils> mockedImageUtils = mockStatic(ImageUtils.class)) {
mockedImageUtils.when(() -> ImageUtils.compressImage(isA(byte[].class))).thenReturn(isA(byte[].class));
// Act
String expected = "file uploaded successfully : test-image.png";
String actual = imageService.uploadImage(imageFile);
// Assert
assertThat(expected).isEqualTo(actual);
mockedImageUtils.verify(() -> ImageUtils.compressImage(isA(byte[].class)), times(1));
mockedImageUtils.verifyNoMoreInteractions();
}
verify(imageRepository, times(1)).save(any(Image.class));
verifyNoMoreInteractions(imageRepository);
}
You might be wondering why I am using the try-with-resources block and MockedStatic
and the annotations @SneakyThrows
, let me provide additional information about that below.
MockedStatic
β Since Mockito 3.4.0, we can use the Mockito.mockStatic(Class<T> classToMock) methodto mock invocations to static methods calls
. This method returns a MockedStatic object for our type, which is a scoped mock object.
Therefore, in our test above, the utilities variable represents a mock with a thread-local explicit scope. Itβs important to note that scoped mocks must be closed by the entity that activates the mock.
This is why we define our mock within a try-with-resources construct so that the mock is closed automatically when we finish with our scoped block.@SneakyThrows
β can be used to sneakily throw checked exceptions without actually declaring this in your method'sthrows
clause.
This somewhat contentious ability should be used carefully, of course. The code generated by Lombok will not ignore, wrap, replace, or otherwise modify the thrown checked exception; it simply fakes out the compiler.
On the JVM (class file) level, all exceptions, checked or not, can be thrown regardless of thethrows
clause of your methods, which is why this works.MockMultipartFile
β Itβs a Mock implementation of the MultipartFile interface. Itβs useful for testing application controllers that access multipart uploads.
Now we can write a second unit test to assert that we can download an image from a database through the service layer. The whole test file will look like this with imports
region.
import com.example.entity.Image;
import com.example.repository.ImageRepository;
import com.example.service.ImageService;
import com.example.util.ImageUtils;
import lombok.SneakyThrows;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;
import java.util.Optional;
import static com.example.util.ImageUtils.BITE_SIZE;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.*;
import static org.springframework.http.MediaType.IMAGE_PNG_VALUE;
@ExtendWith(MockitoExtension.class)
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class ImageServiceTest {
@InjectMocks
private ImageService imageService;
@Mock
private ImageRepository imageRepository;
@Test
@SneakyThrows
public void should_upload_image() {
// Arrange
Image imageUploaded = new Image();
MultipartFile imageFile = new MockMultipartFile("file", "test-image.png", IMAGE_PNG_VALUE, new byte[BITE_SIZE]);
when(imageRepository.save(any(Image.class))).thenReturn(imageUploaded);
try (MockedStatic<ImageUtils> mockedImageUtils = mockStatic(ImageUtils.class)) {
mockedImageUtils.when(() -> ImageUtils.compressImage(isA(byte[].class))).thenReturn(isA(byte[].class));
// Act
String expected = "file uploaded successfully : test-image.png";
String actual = imageService.uploadImage(imageFile);
// Assert
assertThat(expected).isEqualTo(actual);
mockedImageUtils.verify(() -> ImageUtils.compressImage(isA(byte[].class)), times(1));
mockedImageUtils.verifyNoMoreInteractions();
}
verify(imageRepository, times(1)).save(any(Image.class));
verifyNoMoreInteractions(imageRepository);
}
@Test
@SneakyThrows
public void should_download_image() {
// Arrange
var imageData = new byte[BITE_SIZE];
String imageName = "random-img.png";
Image imageEntity = Image.builder().imageData(imageData).name(imageName).id(2L).build();
Optional<Image> imageInDatabase = Optional.of(imageEntity);
try (MockedStatic<ImageUtils> mockedImageUtils = mockStatic(ImageUtils.class)) {
when(imageRepository.findByName(anyString())).thenReturn(imageInDatabase);
mockedImageUtils.when(() -> ImageUtils.decompressImage(isA(byte[].class))).thenReturn(imageData);
// Act
byte[] actual = imageService.downloadImage(imageName);
// Assert
assertArrayEquals(imageData, actual);
mockedImageUtils.verify(() -> ImageUtils.decompressImage(isA(byte[].class)), times(1));
mockedImageUtils.verifyNoMoreInteractions();
}
verify(imageRepository, times(1)).findByName(anyString());
verifyNoMoreInteractions(imageRepository);
}
}
Integration Testing
We write the integration test to confirm that the query scenarios can be played on our database (identical to the production one).
But, also because you may have several SQL queries that are not played by these scenarios selected by the End 2 End tests, but covered by the integration tests.
So, the first part is to create an abstract class TestContainers
which will be used as a parent class for our integration tests class.
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
@Testcontainers(disabledWithoutDocker = true)
public abstract class TestContainers {
@Container
public static PostgreSQLContainer<?> postgreSQLContainer = new PostgreSQLContainer<>(
DockerImageName.parse("postgres:15.1-alpine"))
.withUsername("1kevinson")
.withPassword("admingres");
static {
postgreSQLContainer.start();
}
@DynamicPropertySource
static void databaseProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgreSQLContainer::getJdbcUrl);
registry.add("spring.datasource.username", postgreSQLContainer::getUsername);
registry.add("spring.datasource.password", postgreSQLContainer::getPassword);
}
}
@Testcontainers
allows us to use the Testcontainers extension.@Container
to declare a static field as a container, this allows the Testcontainers extension to manage its lifecycle.@DynamicPropertySource
is a method-level annotation for integration tests that need to add properties with dynamic values to the Environment's set of PropertySources.If you are keen to know more about Testcontainers and these annotations above, take a look at this article below.
Now we have to declare another class, which will serve as a placeholder for Tests annotations.
import com.example.demo.TestContainers;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.test.context.TestPropertySource;
@DataJpaTest
@EntityScan("com.example.entity")
@EnableJpaRepositories("com.example.repository")
@TestPropertySource(locations = "classpath:application-test.yml")
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class BaseIntegrationTest extends TestContainers {
}
@DataJpaTest
β Annotation for a JPA test that focuses only on JPA components. Using this annotation will disable full auto-configuration and instead apply only configuration relevant to JPA tests.@EntityScan("com.example.entity")
β identifies which class should be used y a specific persistence context, in our case itβs JPA.@EnableJpaRepositories("com.example.repository")
β annotation to enable JPA repositories. Will scan the package of the annotated configuration class for Spring Data repositories by default.@TestPropertySource(locations = "classpath:application-test.yml")
β class-level annotation that is used to configure the locations() of properties files and inline properties() to be added to the Environment's set of PropertySources for an ApplicationContext for integration tests.@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
β Annotation that can be applied to a test class to configure a test database to use instead of the application-defined or autoconfigured DataSource.Our application-test.yml
will look like this.
spring:
datasource:
driver-class-name: org.postgresql.Driver
jpa:
show-sql: true
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
hibernate:
ddl-auto: update
use-new-id-generator-mappings: false
logging:
level:
org.hibernate:
SQL: DEBUG
type.descriptor.sql.BasicBinder: TRACE
The integration test class, will test the scenarios that are necessary to upload and download images in our container database.
import com.example.entity.Image;
import com.example.repository.ImageRepository;
import com.example.util.ImageUtils;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class JPAImageRepositoryTest extends BaseIntegrationTest {
@Autowired
private ImageRepository imageRepository;
private static Image imageFixture;
@BeforeEach
void setUp() {
imageFixture = Image.builder()
.id(1L)
.name("integration-test-image.png")
.type("image/png")
.imageData(new byte[ImageUtils.BITE_SIZE])
.build();
imageRepository.save(imageFixture);
}
@Test
@Order(1)
void can_upload_an_image_in_database() {
// Arrange & Act
List<Image> imageList = imageRepository.findAll();
// Assert
assertThat(imageList).hasSize(1);
assertThat(imageList.get(0))
.usingRecursiveComparison()
.isEqualTo(imageFixture);
}
@Test
@Order(2)
void can_download_an_image_from_database() {
// Arrange
String nameOfImageToDownload = imageFixture.getName();
// Act & Assert
Optional<Image> imageDownloaded = imageRepository.findByName(nameOfImageToDownload);
assertThat(imageDownloaded).isPresent()
.map(Image::getId)
.get().isEqualTo(2L);
assertThat(imageDownloaded)
.isPresent()
.get()
.usingRecursiveComparison()
.ignoringFields("id")
.isEqualTo(imageFixture);
}
}
End 2 End Testing
Now that we have done with the integration test part, we can write the base class for our integration test, which will extend the Testcontainers class.
package com.example.demo.e2e;
import com.example.demo.TestContainers;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.TestPropertySource;
@SpringBootTest
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
public class BaseE2E extends TestContainers {
}
@SpringBootTest
is a powerful annotation in Spring framework which is used to load entire application context for Spring Boot application and create integration test. It allows you to test your application in the same way as it runs on production.@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
Test annotation, which indicates that the ApplicationContext
associated with a test is dirty and should therefore be closed and removed from the context cache.This is how our whole E2E test class will look like:
import com.example.entity.Image;
import com.example.repository.ImageRepository;
import com.example.util.ImageUtils;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import java.util.List;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@AutoConfigureMockMvc
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class ImageControllerTest extends BaseE2E {
@Autowired
private MockMvc mockMvc;
@Autowired
private ImageRepository imageRepository;
@BeforeEach
void setUp() {
imageRepository.deleteAll();
}
@Test
@Order(1)
void should_upload_an_image_in_database() throws Exception {
// Given
MockMultipartFile file = new MockMultipartFile(
"image",
"mock-image.png",
MediaType.IMAGE_PNG_VALUE,
new byte[ImageUtils.BITE_SIZE]
);
// When
var requestBuilder = MockMvcRequestBuilders.multipart("/image").file(file);
// Then
mockMvc.perform(requestBuilder).andExpect(status().isCreated()).andDo(print());
imageRepository.findById(1L).ifPresent(user -> {
assertThat(user.getName()).isEqualTo("mock-image.png");
assertThat(user.getType()).isEqualTo("image/png");
assertThat(user.getImageData()).isInstanceOf(byte[].class);
});
}
@Test
@Order(2)
void should_download_an_image_from_database() throws Exception {
// Given
imageRepository.saveAll(imagesInDatabase());
// When
var requestBuilder = MockMvcRequestBuilders.get("/image/e2e-test-image.png");
// Then
mockMvc.perform(requestBuilder.accept(MediaType.IMAGE_JPEG_VALUE))
.andDo(print())
.andExpect(status().isOk());
}
private List<Image> imagesInDatabase() {
return Stream.of(
Image.builder().id(1L).name("e2e-test-image1.png").type("image/png").imageData(new byte[ImageUtils.BITE_SIZE]).build(),
Image.builder().id(2L).name("e2e-test-image2.jpg").type("image/jpg").imageData(new byte[ImageUtils.BITE_SIZE]).build()
).toList();
}
}
Now we can run our test and see what happens π§ͺ
$ mvn test
I hope you enjoyed reading this, and I'm curious to hear if this tutorial helped you. Please let me know your thoughts below in the comments. Don't forget to subscribe to my newsletter to avoid missing my upcoming blog posts.
You can also find me here LinkedIn β’ Twitter β’ GitHub or Medium
Wrap up
In this tutorial, we have covered how you can write unit, integration and end-to-end tests for uploading and downloading image into an SQL database.
Being able to write tests for development is a must for any professional developer. Hope this tutorial helps, you can get the code source on my GitHub here.