To do TDD it is very important to have a framework like mockito, JUnit, to test behaviors and results without the need to test the code in broad strokes.
TDD is writing the tests first before writing a line of production code, writing the tests that test the production code, although it could be difficult to do if you don’t have code to compile, so you should write the minimum for a happy compilation. A team does TDD and at the same time it gets the tests done, it also implements the functionality at the same time.
The agile manifesto says value working code over comprehensive documentation
, with TDD you have to work with code and test that the code works at the same time, if we use the JavaDocs and TDD, we could get a nice and good documentation in parallel with the delivery of the test functionalities.
A simple code
CupCake will be our class to test in which we will use the annotation @InjectMocks
/**
* Simple CupCake class for @InjectMocks
*/
public class CupCake {
public static final String INGREDIENT_FOR_CUPCAKE = "Ingredient is: ";
private final Ingredient ingredient;
private List<String> ingredients;
private final SpyMePlease spyMePlease;
public CupCake(final Ingredient ingredient, final List<String> ingredients, final SpyMePlease spyMePlease) {
this.ingredient = ingredient;
this.ingredients = ingredients;
this.spyMePlease = spyMePlease;
}
public String getIngredient() {
return INGREDIENT_FOR_CUPCAKE.concat(ingredient.getFlavor());
}
public int getIngredients() {
return ingredients.size();
}
/**
*
* Aquí podemos obtener un resultado diferente al usar @Mock o @Spy
*
* @return boolean
*/
public boolean canISpyThere() {
System.out.println("Can I Spy there ?");
spyMePlease.hola();
return true;
}
}
Our Mock
The Ingredient class will be our Mock since this is a dependency of CupCake, all our dependencies will be annotated with @Mock, we could also use @Spy to be injected inside the bean annotated with @InjectMocks.
/**
* Simple class Ingredient to @Mock
*/
public class Ingredient {
private String flavor;
public Ingredient(final String flavor) {
this.flavor = flavor;
}
public String getFlavor() {
return flavor;
}
}
/**
* Clase a usar con @Spy para inspeccionar funcionalidad
* interna y externa
*/
public class SpyMePlease {
public void hola() {
System.out.println("Your are Spying this!");
}
}
With the @Spy annotation we can test internal and external behaviors of the methods, however with @Mock only the external behaviors.
/**
* En este caso las dependencias como Ingredient e Ingredients serán nuestros mocks anotadas con
* @Mock donde esta creara las implementaciones que necesitemos.
*
* Mientras que nuestra clase CupCake sera la que vamos a testear, anotada con @InjectMock sin necesidad
* de instanciarla, luego esta inyectara todos los mocks que están marcados con @Mock, @Spy etc
*/
@ExtendWith(MockitoExtension.class)
class MockingAndSpyTest {
private static final String BLACK_BERRY = "BlackBerry";
@Mock
private Ingredient ingredient;
@Mock
private SpyMePlease spyMePlease;
@Spy
private List<String> ingredients = new ArrayList<>();
@InjectMocks
private CupCake cupCake;
@Test
void mockAndSpy() {
Mockito.when(ingredient.getFlavor()).thenReturn(BLACK_BERRY);
ingredients.add("Banana");
ingredients.add("Orange");
ingredients.add("BlueBerry");
final int actualIngredients = 3;
assertThat(actualIngredients, is(cupCake.getIngredients()));
final String actualFlavor = CupCake.INGREDIENT_FOR_CUPCAKE.concat(BLACK_BERRY);
assertThat(actualFlavor, is(cupCake.getIngredient()));
assertThat(true, is(cupCake.canISpyThere()));
}
}
If we run the test the result would be GREEN and also the following:
Can I Spy there ?
Process finished with exit code 0
But what if we change
@Spy
private SpyMePlease spyMePlease;
Can I Spy there ?
Your are Spying this!
Process finished with exit code 0
We have this warning thanks to the @Spy annotation and openjdk-11 version
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by org.mockito.internal.util.reflection.ReflectionMemberAccessor (file:/C:/Users/Rubn/.m2/repository/org/mockito/mockito-core/3.6.0/mockito-core-3.6.0.jar) to field java.util.ArrayList.elementData
WARNING: Please consider reporting this to the maintainers of org.mockito.internal.util.reflection.ReflectionMemberAccessor
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
It is solved with 2 dependencies:
-
mockito-core
-
mockito-inline
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-core -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.6.0</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-inline -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>3.6.0</version>
<scope>test</scope>
</dependency>
These above dependencies are not needed if we use Springboot |
Refactoring the test a little
... imports removed for brevity
@DisplayName("We use @InyectMocks and pass the dependencies via constructor")
@ExtendWith(MockitoExtension.class) (1)
class CupCakeTest {
@InjectMocks
private CupCake cupCake; (2)
@Mock
private Ingredient ingredient; (3)
@Mock
private List<Ingredient> ingredientList;
@Mock
private SpyMePlease spyMePlease;
@Test
@DisplayName("The tomato ingredient is tested here and other different ingredients are tested here")
void getIngredient() {
when(ingredient.getFlavor()).thenReturn("tomate"); (4)
when(ingredientList.get(0)).thenReturn(new Ingredient("Banana"));
when(ingredientList.get(1)).thenReturn(new Ingredient("Fresa"));
when(ingredientList.get(2)).thenReturn(new Ingredient("Coco"));
assertThat("Ingredient is: tomate").isEqualTo(cupCake.getIngredient());
assertThat("Banana").isEqualTo(cupCake.getIngredients().get(0).getFlavor());
assertThat("Fresa").isEqualTo(cupCake.getIngredients().get(1).getFlavor());
assertThat("Coco").isEqualTo(cupCake.getIngredients().get(2).getFlavor());
verify(ingredient).getFlavor();
}
@Test
@DisplayName("The ingredient list shall have a size of 3 ingredients")
void getSizeOfAllIngredients() {
when(ingredientList.size()).thenReturn(3);
assertThat(3).isEqualTo(cupCake.getSizeOfAllIngredients());
}
@Test
@DisplayName("The list should be the same as our cupCake, and contain a Banana flavored ingredient.")
void getIngredients() {
when(this.ingredientList.size()).thenReturn(1);
when(this.ingredientList.get(0)).thenReturn(new Ingredient("Banana"));
assertAll( (5)
() -> assertEquals(this.ingredientList.size(), cupCake.getIngredients().size()),
() -> assertEquals(this.ingredientList.get(0), this.cupCake.getIngredients().get(0))
);
verify(this.ingredientList, times(2)).size(); (6)
verify(this.ingredientList, times(2)).get(0);
}
@Test
@DisplayName("The method will return true stupidly")
void canISpyThere() {
assertThat(cupCake.canISpyThere()).isTrue();
}
}
1 | Annotation required for JUnit 5 |
2 | Our class to test CupCake |
3 | Everything annotated with @Mock we must simulate it, that is, give it behavior |
4 | We make sure to invoke the when in this method to avoid an UnnecessaryStubbingException type error. |
5 | assertAll of JUnit 5 |
6 | We verify that size and get are invoked only 2 times. |
TIPS @BeforeEach y @InyectMocks invocar a contructor o no
|
Simple testing of empty or void method
class SpyMePleaseTest {
private SpyMePlease spyMePlease = new SpyMePlease();
@Test
@DisplayName("Testing hello method, and it does not throw any exception.")
void hola() {
assertThatCode(() -> spyMePlease.hola()).doesNotThrowAnyException(); (1)
}
}
1 | We can help ourselves with the AssertionsForClassTypes class to test empty methods, which sometimes are a bit annoying. |
Verify and Mockito times
Let’s see a simple service that displays only 10 numbers in a reactive way and in case one of them fails our stream triggers a fallback.
With reactive programming the testing part is usually very rough, but with StepVerifier a dependency that is useful to do TDD of reactive streams since the create
method accepts a publisher as such, either from project reactor or even RxJava or some other implementation of the reactive stream specification.
It can happen with project reactor that our reactive stream triggers a fallback many times, which should be avoided.
|
public class MyReactorServiceImpl implements MyReactorService {
private FallBackService service; (1)
public Flux<Integer> createTenItem() {
return Flux.just(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)//Flux.range :D
.doOnError(error -> log.info("Error with item {}", error))
.flatMap(item -> this.launchError(item)
.onErrorReturn(-1)) (2)
.doOnNext(onNext -> log.info("onNext {}", onNext));
}
public Mono<Integer> launchError(final Integer item) {
if (item == 6) {
return Mono.error(new RuntimeException("Error con item " + item));
}
return Mono.just(item);
}
}
1 | Without using our fallback service yet. |
2 | When our item is 6 we will return with onErrorReturn ( a fallback of help, but not so smart) a -1, |
@Test
@DisplayName("When it is 6, an error is produced and we return -1")
void createTenItem() {
StepVerifier.create(myReactorService.createTenItem())
.expectNext(1, 2, 3, 4, 5, -1, 7, 8, 9 , 10) (1)
.verifyComplete();
}
1 | We are working with a Flux, that is, item`s…. and when it is 6 it is actually an error that we will convert to -1. |
The -1 we return in case of error with the number 6, but the Stream operation continues as normal.
More powerful fallback
public class MyReactorServiceImpl implements MyReactorService {
private final MyFallbackServiceImpl service;
public Flux<Integer> createTenItem() {
return Flux.just(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)// Flux.range :D
.doOnError(error -> log.info("Error with item {}", error))
.flatMap(item -> this.launchError(item) (1)
.switchIfEmpty(this.fallback())) (3)
.doOnNext(onNext -> log.info("onNext {}", onNext));
}
public Mono<Integer> launchError(final Integer item) {
if (item == 6) {
return Mono.empty(); (2)
}
return Mono.just(item);
}
public Mono<Integer> fallback() {
log.info("fallback {}", -1);
return Mono.just(-1); (4)
}
}
1 | We fire in case of error when it is 6 |
2 | When it is 6 we send a Mono.empty to use it with the switchIfEmpty instead of a RuntimeException |
3 | We go for the SwitchIfEmpty |
4 | We are already in the fallback and return -1 |
Our fallback is printed many times, and besides that it is badly tested, that is to say, it lacks a Mockito.verify and Mockito.times , the ideal would be.
|
We refactored a little MyReactorServiceImpl
to use our fallback service to mock it up.
public Flux<Integer> createTenItem() {
return Flux.just(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)// Flux.range :D
.doOnError(error -> log.info("Error with item {}", error))
.flatMap(item -> this.launchError(item)
.switchIfEmpty(this.service.fallback())) (1)
.doOnNext(onNext -> log.info("onNext {}", onNext));
}
public Mono<Integer> launchError(final Integer item) {
if (item == 6) {
return Mono.empty();
}
return Mono.just(item);
}
1 | We use our fallback service, i.e. our dependency, to check its behavior in a more intelligent way. |
We refactored our test a bit
@Test
@DisplayName("When it is 6, an error occurs and we return -1.")
void createTenItem() {
when(service.fallback()).thenReturn(Mono.just(-1)); (1)
StepVerifier.create(myReactorService.createTenItem())
.expectNext(1, 2, 3, 4, 5, -1, 7, 8, 9, 10)
.verifyComplete();
Mockito.verify(service, Mockito.times(1)).fallback(); (2)
}
1 | A small mock of our fallback service |
2 | We verify that our fallback is executed only once, which would be ideal, right? |
Surprise, switchIfEmpty dangerous
Our fallback has been invoked 10 damn times which is what we don’t want, ufff the DevOps
will hate me if an incidence comes out at 12am because I blow up mongo lmao
Using the defer operator as our ally
To solve it we use the operator defer, which will practically make the fallback
to fire when we have an empty just with the number 6 (our error) a little crazy isn’t it? and that the SwitchIfEmpty works correctly.
public Flux<Integer> createTenItem() {
return Flux.just(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)// Flux.range :D
.doOnError(error -> log.info("Error with item {}", error))
.flatMap(item -> this.launchError(item)
.switchIfEmpty(Mono.defer(this.service::fallback)
.doOnNext(onNext -> log.info("Fallback {}", -1)))) (1)
.doOnNext(onNext -> log.info("onNext {}", onNext));
}
1 | Added the defer operator and a doOnNext that will print Fallback -1 once. |
Our test will pass quietly now
package com.example.mockitobasicuse.reactorfallback.impl;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import static org.mockito.Mockito.when;
@DisplayName("Manejo de excepcion por fallback")
@ExtendWith(MockitoExtension.class)
class MyReactorServiceImplTest {
@InjectMocks
private MyReactorServiceImpl myReactorService;
@Mock
private MyFallbackServiceImpl service;
@Test
@DisplayName("Cuando es 6 se produce error y retornamos -1")
void createTenItem() {
when(service.fallback()).thenReturn(Mono.just(-1));
StepVerifier.create(myReactorService.createTenItem())
.expectNext(1, 2, 3, 4, 5, -1, 7, 8, 9, 10)
.verifyComplete();
Mockito.verify(service, Mockito.times(1)).fallback();
}
@Test
@DisplayName("Al introducir 6 internamente se produce un Mono.empty()")
void launchError() {
StepVerifier.create(myReactorService.launchError(6))
.verifyComplete();
}
}
Using ArgumentsSource for Parameterized tests
With JUnit5 we can use a parameterized list with data, much more data, that we can even read from some file, etc, in this case we create them ourselves, for example
public class CupCakeArgumentsProvider implements ArgumentsProvider { (1)
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext) throws Exception {
final Ingredient pera = new Ingredient("pera");
final Ingredient manzana = new Ingredient("manzana");
final Ingredient coco = new Ingredient("coco");
final List<Ingredient> ingredientList = Arrays.asList(pera, manzana, coco);
final Ingredient pina = new Ingredient("piña");
final Ingredient frutaDelDragon = new Ingredient("Fruta del dragon");
final Ingredient uva = new Ingredient("uva");
final List<Ingredient> ingredientList1 = Arrays.asList(pina, frutaDelDragon, uva);
return Stream.of(
Arguments.of(ingredientList, pera, manzana, coco),
Arguments.of(ingredientList1, pina, frutaDelDragon, uva)); (2)
}
}
1 | We extend the ArgumentsProvider interface |
2 | We return a Stream with 2 Arguments.of parameters with this order that we must respect in the signature of our test |
@ParameterizedTest (1)
@ArgumentsSource(CupCakeArgumentsProvider.class) (2)
@DisplayName("Using many more ingredients")
void masFrutas(final List<Ingredient> ingredientsList, Ingredient ingredientExpected, Ingredient ingredient2Expected,
Ingredient ingredient3Expected) { (3)
when(this.ingredientList.get(0)).thenReturn(ingredientsList.get(0));
when(this.ingredientList.get(1)).thenReturn(ingredientsList.get(1));
when(this.ingredientList.get(2)).thenReturn(ingredientsList.get(2));
assertNotNull(ingredientsList);
assertThat(ingredientExpected).isEqualTo(cupCake.getIngredients().get(0)); (4)
assertThat(ingredient2Expected).isEqualTo(cupCake.getIngredients().get(1));
assertThat(ingredient3Expected).isEqualTo(cupCake.getIngredients().get(2));
assertThat(ingredientList)
.usingRecursiveComparison()
.isEqualTo(cupCake.getIngredients());
verify(ingredientList, times(1)).get(0);
}
1 | Necessary notation |
2 | ArgumentsSource goes at the method level and is used with the class that implements the ArgumentsProvider |
3 | This method signature must match the parameters set in the Arguments.of i.e. a list followed by 3 Ingredients for this to match, also that this method will be invoked twice, because we use two Arguments.of |
4 | We pass an expected and parameterized Ingredient that must be the same that should have the list of ingredients inside our cupCake. |
If our method signature does not match, we will get an error type ParameterResolutionException
|
Integration test with BlockHound
Here we load the spring context, with the annotation @ContextConfiguration
and apply stereotyping to the service we are going to need, that is to say a real service as is, which can even invoke a database if desired, ideally the test name should also end in IT(integration).
According to the documentation, BlockHound is scanning the Scheduler parallel by default and if we in our code or the internal code of any dependency at that time has a blocking call, BlockHound will complain and throw exception. You can change this behavior by instantiating your Builder as indicated in the documentation.
@DisplayName("<= Using blockhound to detect blocking calls in parallel thread =>")
@Log4j2
@ContextConfiguration(classes = {ReactiveRandomNumbers.class}) (1)
@ExtendWith(SpringExtension.class) (2)
class UsingBlockHoundIT {
@Autowired (3)
private ReactiveRandomNumbers reactiveRandomNumbers;
@BeforeEach
void setup() {
BlockHound.install(); (4)
}
@Test
@DisplayName("Blocking call! in line 37")
void detectBlockingCall1() {
StepVerifier.create(Mono.just(1)
.doOnNext(e -> this.blockMe()) (5)
.subscribeOn(Schedulers.parallel()))
.expectErrorMatches(error -> error.getMessage().contains("Blocking call!"))
.verify();
}
@Test
@DisplayName("Blocking call! in line 46")
void detectBlockingCall2() {
StepVerifier.create(this.reactiveRandomNumbers.monoWithBlockingCallInside(500L)
.subscribeOn(Schedulers.parallel())) (6)
.expectErrorMatches(error -> error.getMessage().contains("Blocking call!"))
.verify();
}
@Test
@DisplayName("Blocking call! in line 57, but using boudendElastic to avoid that")
void avoidBlockingCall() {
StepVerifier.create(Mono.just(1)
.doOnNext(e -> this.blockMe())
.subscribeOn(Schedulers.boundedElastic())) (7)
.expectNext(1)
.verifyComplete();
}
@RepeatedTest(10)
@DisplayName("Blocking call! in line 67, but using boudendElastic to avoid that")
void avoidBlockingCall2() {
StepVerifier.create(this.reactiveRandomNumbers.monoWithBlockingCallInside(500L)
.subscribeOn(Schedulers.boundedElastic())) (8)
.expectNextMatches(map -> map.size() == 6)
.verifyComplete();
}
@SneakyThrows
void blockMe() { (9)
Thread.sleep(1000);
}
}
1 | We load our ReactiveRandomNumbers service. |
2 | We enable JUnit 5 with Spring. |
3 | We inject our service as is, to make a real invocation, without mockeying. |
4 | The magic, to install our BlockHound agent, and with @BeforeEach to trigger on every test. |
5 | We insert a blocking call to check that our agent detects it, and it does detect it. |
6 | We use the Scheduler parallel, to scan it and blockHound detects a blocking internal call. |
7 | Now we use a boundedElastic to avoid that blocking call. |
8 | The same in the invoked service. |
9 | The additional test blocking method with a 1 second sleep. |
This service ReactiveRandomNumbers internally when creating pseudo random numbers bursts here, but it is already an internal thing that we are not handling, and there is useful blockhound that indicates us that there is contention
to take into account.
Caused by: reactor.blockhound.BlockingOperationError: Blocking call! java.io.FileInputStream#readBytes