13.1.3 Testing R2DBC Repository
Spring Data R2DBC includes support for writing integration tests for R2DBC repositories. Specifically, the @DataR2dbcTest annotation, when placed on a test class, causes Spring to create an application context with the generated Spring Data R2DBC repositories as beans that can be injected into the test class. Along with StepVerifier, which we’ve used in previous chapters, this enables us to write automated tests against all of the repositories we’ve created.
For the sake of brevity, we’ll focus solely on a single test class: IngredientRepositoryTest. This will test IngredientRepository, verifying that it can save Ingredient objects, fetch a single Ingredient, and fetch all saved Ingredient objects. The next code sample shows this test class.
Listing 13.7 Testing a Spring Data R2DBC repository
package tacos.data;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.ArrayList;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.r2dbc.DataR2dbcTest;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;
import tacos.Ingredient;
import tacos.Ingredient.Type;
@DataR2dbcTest
public class IngredientRepositoryTest {
@Autowired
IngredientRepository ingredientRepo;
@BeforeEach
public void setup() {
Flux<Ingredient> deleteAndInsert = ingredientRepo.deleteAll()
.thenMany(ingredientRepo.saveAll(
Flux.just(
new Ingredient("FLTO", "Flour Tortilla", Type.WRAP),
new Ingredient("GRBF", "Ground Beef", Type.PROTEIN),
new Ingredient("CHED", "Cheddar Cheese", Type.CHEESE)
)));
StepVerifier.create(deleteAndInsert)
.expectNextCount(3)
.verifyComplete();
}
@Test
public void shouldSaveAndFetchIngredients() {
StepVerifier.create(ingredientRepo.findAll())
.recordWith(ArrayList::new)
.thenConsumeWhile(x -> true)
.consumeRecordedWith(ingredients -> {
assertThat(ingredients).hasSize(3);
assertThat(ingredients).contains(
new Ingredient("FLTO", "Flour Tortilla", Type.WRAP));
assertThat(ingredients).contains(
new Ingredient("GRBF", "Ground Beef", Type.PROTEIN));
assertThat(ingredients).contains(
new Ingredient("CHED", "Cheddar Cheese", Type.CHEESE));
})
.verifyComplete();
StepVerifier.create(ingredientRepo.findBySlug("FLTO"))
.assertNext(ingredient -> {
ingredient.equals(new Ingredient("FLTO", "Flour Tortilla", Type.WRAP));
});
}
}The shouldSaveAndFetchIngredients() method starts by creating a Flux of test Ingredient objects. From this Flux, it uses the flatMap() operation to save each Ingredient via the save() method on the injected IngredientRepository. The call to subscribe() opens the flow of data through the Flux, resulting in the Ingredient objects being saved.
Next up, a StepVerifier is created from the Mono<Ingredient> returned by the repository’s findBySlug() method. The Mono<Ingredient> should have a single Ingredient in it, which is what the assertNext() method verifies, matching it up with the values expected for an Ingredient whose slug is "FLTO". Then, it verifies that the Mono is complete.
Finally, another StepVerifier is created from the Flux<Ingredient> returned by the repository’s findAll() method. One at a time, it asserts that each Ingredient that flows from that Flux matches up with the three Ingredient objects originally saved at the start of the test method. And, as with the other StepVerifier, a call to verifyComplete() verifies that the Mono is complete and has no more Ingredient objects coming through.
Although we focused only on testing the IngredientRepository, the same techniques can be used to test any Spring Data R2BDC–generated repository.
So far, so good. We now have defined our domain types and their respective repositories. And we’ve written a test to verify that they work. We can use them as is if we like. But these repositories make persistence of a TacoOrder inconvenient in that we must first create and persist Taco objects that are part of that order and then persist the TacoOrder object that references the child Taco objects. And when reading the TacoOrder, we’ll receive only a collection of Taco IDs and not fully defined Taco objects.
It would be nice if we could persist TacoOrder as an aggregate root and have its child Taco objects be persisted along with it. Likewise, it would be great if we could fetch a TacoOrder and have it fully defined with complete Taco objects and not just the IDs. Let’s define a service-level class that sits in front of OrderRepository and TacoRepository to mimic the persistence behavior of chapter 3’s OrderRepository.
