I am unable to run JUnit5 tests concurrently when they involve using SpringExtension. When I run sequentially, there are no issues. When I run concurrently, only one test in the class is able to successfully access any injected or autowired fields. Any other tests will throw a NullPointerException when attempting to reference injected or autowired fields. Since JUnit5 parallelization is using ForkJoinPool under the hood, I was under the impression one Spring test context would be created and threads could safely use a cached context. Am I missing something in configuring my testing suite?
Here's a simplified example where the problem can be observed:
DummyTest:
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import javax.inject.Inject;
#Execution(ExecutionMode.CONCURRENT)
#ExtendWith(SpringExtension.class)
#ContextConfiguration(classes = {DummyTestConfig.class})
public class DummyTest {
#Inject
private StringBuilder stringBuilder;
#Test
public void testA() {
Assertions.assertEquals(stringBuilder.toString(), "test");
}
#Test
public void testB() {
Assertions.assertEquals(stringBuilder.toString(), "test");
}
}
DummyTestConfig:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
#Configuration
public class DummyTestConfig {
public DummyTestConfig() {}
#Bean
public StringBuilder stringBuilder() {
return new StringBuilder("test");
}
}
Related
#RunWith(SpringRunner.class)
public class TestService {
#Test
#EnabledIf("Instant.now().atZone(ZoneId.of(\"Asia/Kolkata\")).getDayOfWeek() == DayOfWeek.MONDAY")
public void test() {
System.out.println("The day is Monday");
}
}
running using mvn -Dtest=TestService test
#EnabledIf is not working, it is executing the test even the day is not Monday
Your code has two big problems:
First, the test class is annotated with #RunWith(SpringRunner.class) and (because the test runs) the test method is annotated with #org.junit.Test
This means that the test is running using the JUnit4 testing framework and JUnit4 knows nothing about the #EnabledIf annotation and ignores it.
If you want to use the #EnabledIf annotation you must make your test into a JUnit5 test and for that you must change your test class to
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIf;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.test.context.junit.jupiter.SpringExtension;
#ExtendWith(SpringExtension.class)
public class TestService {
#Test
#EnabledIf("Instant.now().atZone(ZoneId.of(\"Asia/Kolkata\")).getDayOfWeek() == DayOfWeek.MONDAY")
public void test() {
System.out.println("The day is Monday");
}
}
Because of the second problem that doesn't completely solve your problem. The documentation for #EnabledIf states:
A container or test may be enabled or disabled based on the boolean return of a method via the #EnabledIf and #DisabledIf annotations. The method is provided to the annotation via its name.
That means that you cannot provide some arbitrary expression as the argument of the #EnabledIf annotation - you need to implement the expression in a method.
When adding that method your test class might look like this:
import java.time.DayOfWeek;
import java.time.Instant;
import java.time.ZoneId;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIf;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.test.context.junit.jupiter.SpringExtension;
#ExtendWith(SpringExtension.class)
public class TestService {
#Test
#EnabledIf("onMonday")
public void test() {
System.out.println("The day is Monday");
}
static boolean onMonday() {
return Instant.now().atZone(ZoneId.of("Asia/Kolkata")).getDayOfWeek() == DayOfWeek.MONDAY;
}
}
If however you intended to use org.springframework.test.context.junit.jupiter.EnabledIf (instead of org.junit.jupiter.api.condition.EnabledIf - unfortunately your question is not clear about that) then you need to adjust the imports and provide a SpEL-Expression for the condition:
import java.time.DayOfWeek;
import java.time.Instant;
import java.time.ZoneId;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.test.context.junit.jupiter.EnabledIf;
import org.springframework.test.context.junit.jupiter.SpringExtension;
#ExtendWith(SpringExtension.class)
public class TestService {
#Test
#EnabledIf("#{T(java.time.Instant).now().atZone(T(java.time.ZoneId).of(\"Asia/Kolkata\")).getDayOfWeek() == T(java.time.DayOfWeek).MONDAY}")
public void test() {
System.out.println("The day is Monday");
}
}
Intro:
Our product needs to have integrations tests for 3 different databases:
Oracle
Postgres
MSSQL
We are using Spring Boot as our framework and TestContainers to start up the databases mentioned above.
The problem:
We need to run the same tests for each container (database).
After a lot of digging on the net the only way that I could think of was using a BaseClass where we write all the test cases and for each container, we create a class that inherits from the BaseClass and we override the method and annotate it with #Test.
Below in the code, you will a single JUnit5 extension for Postgres that starts a TestContainer, base test class, and a test class that gets extended from the Postgres extension, starts a Spring Application context, and runs the tests from the base class.
The code:
import com.company.itest.AutoConfig;
import com.company.itest.BaseIntegrationTest;
import com.company.itest.db.mssql.MSSqlTest;
import com.company.itest.db.oracle.OracleTest;
import com.company.itest.db.postgres.PostgresTest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
public class TestTheTest extends BaseIntegrationTest {
public void contextLoads() {
Assertions.assertEquals(1, 1);
}
public void contextLoads2() {
Assertions.assertNotEquals(1, 2);
}
}
#SpringBootTest(
classes = AutoConfig.class,
webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
#PostgresTest
class TestPostgres extends TestTheTest {
#Test
public void contextLoads() {
super.contextLoads();
}
#Test
public void contextLoads2() {
super.contextLoads2();
}
}
import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.PostgreSQLContainer;
public class PostgresqlTestContainersExtension implements BeforeAllCallback, AfterAllCallback {
private final Logger log = LoggerFactory.getLogger(PostgresqlTestContainersExtension.class);
private PostgreSQLContainer<?> postgres;
#Override
public void beforeAll(ExtensionContext context) {
log.info("Setting up postgres container");
postgres = new PostgreSQLContainer<>("postgres:13").withReuse(true);
postgres.start();
System.setProperty("spring.datasource.url", postgres.getJdbcUrl());
System.setProperty("spring.datasource.username", postgres.getUsername());
System.setProperty("spring.datasource.password", postgres.getPassword());
}
#Override
public void afterAll(ExtensionContext context) {
postgres.stop();
}
}
package com.company.itest.db.postgres;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.testcontainers.junit.jupiter.Testcontainers;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
#Target({TYPE, ANNOTATION_TYPE})
#Retention(RUNTIME)
#ExtendWith(SpringExtension.class)
#ExtendWith({PostgresqlTestContainersExtension.class})
#Testcontainers
public #interface PostgresTest {}
The question:
How can I create a single JUnit test class and then rerun it with a different JUnit5 extension without doing this polymorphism?
If you are using maven, you could try to have a different profile per db
I have a spring project, which contains a class called EnvUtils:
package com.example;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Objects;
#Component
public class EnvUtils {
#Value("${env}")
private String env;
/**
* Develop environment
*/
public boolean isDev() {
return Objects.equals(env, "dev");
}
/**
* Production environment
*/
public boolean isProd() {
return Objects.equals(env, "prod");
}
}
I wrote a simple test, which set env's value to dev, and checked the result of isDev():
package com.example;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.util.ReflectionTestUtils;
#SpringBootTest
#ExtendWith(MockitoExtension.class)
class MainControllerTests {
#MockBean
private EnvUtils envUtils;
#Test
void fieldTest() {
ReflectionTestUtils.setField(envUtils, "env", "dev");
Assertions.assertTrue(envUtils.isDev());
}
}
The test should pass because the value of env is dev, which means envUtils.isDev() should definitely return true. However, this test failed. I debugged it and found that envUtils.env was dev, as expected, but envUtils.isDev() returned false. How could it be? Have I done something wrong?
You are mocking EnvUtils class, so the method isDev is mocked also, the real method of the class will not be invoked.
In your case, you don't need #MockBean, remove it. And don't forget to initialize the envUrils object using default constructor.
#SpringBootTest
#ExtendWith(MockitoExtension.class)
class MainControllerTests {
private EnvUtils envUtils = new EnvUtils();
#Test
void fieldTest() {
ReflectionTestUtils.setField(envUtils, "env", "dev");
Assertions.assertTrue(envUtils.isDev());
}
}
You mocked whole EnvUtils class.
If you change it to Spy it would be work.
Read this:mock vs spy
Try this:
#ExtendWith(MockitoExtension.class)
class MainControllerTests {
#SpyBean
private EnvUtils envUtils;
#Test
void fieldTest() {
ReflectionTestUtils.setField(envUtils, "env", "dev");
Assertions.assertTrue(envUtils.isDev());
}
}
I am getting
java.lang.NoSuchMethodError: org.junit.jupiter.api.extension.ExtensionContext.getRequiredTestInstances()Lorg/junit/jupiter/api/extension/TestInstances;
at org.mockito.junit.jupiter.MockitoExtension.beforeEach(MockitoExtension.java:143)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$null$0(TestMethodTestDescriptor.java:126)
at org.junit.jupiter.engine.execution.ThrowableCollector.execute(ThrowableCollector.java:40)
error for below code.
what the proper way of running below code as junit test?
package com.safnas.unittesting.unittesting.business;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
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 com.safnas.unittesting.unittesting.data.SomeDataService;
#ExtendWith(MockitoExtension.class)
class SomeBusinessMockTest {
#InjectMocks
SomeBusinessImpl business = new SomeBusinessImpl();
#Mock
SomeDataService dataServiceMock;
#Test
void calculateSumUsingDataService_basic() {
when(dataServiceMock.retrieveAllData()).thenReturn(new int[] {1,2,3});
assertEquals(6, business.calculateSumUsingDataService());
}
#Test
void calculateSum_empty() {
when(dataServiceMock.retrieveAllData()).thenReturn(new int[] {});
assertEquals(0, business.calculateSumUsingDataService());
}
#Test
void calculateSum_oneValue() {
when(dataServiceMock.retrieveAllData()).thenReturn(new int[] {5});
assertEquals(5, business.calculateSumUsingDataService());
}
}
I removed #ExtendWith(MockitoExtension.class) and used below
#BeforeEach
void beforeEach() {
MockitoAnnotations.openMocks(this);
}
it worked fine
as per javadocs
AutoCloseable org.mockito.MockitoAnnotations.openMocks(Object testClass)
Initializes objects annotated with Mockito annotations for given testClass:#org.mockito.Mock, #Spy, #Captor, #InjectMocks
See examples in javadoc for MockitoAnnotations class.
Parameters:testClass Returns:A closable to close when completing any tests in testClass.
one more observation.
without any change, my original question is working in intelliJ
eclipse is only having issue it seems
I have a RestController that I want to test:
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RestController;
#RestController
public class PetController implements PetApi {
#Autowired
PetRepository pr;
#Override
public ResponseEntity<Pet> addPet(#Valid Pet pet) {
pr.save(new PetBE(9L, "dummy"));
return new ResponseEntity<Pet>(pet, HttpStatus.OK);
}
}
import org.springframework.data.repository.CrudRepository;
public interface PetRepository extends CrudRepository<PetBE, Long> {
}
I want to mock PetRepository and test if the object passed is the object returned:
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import com.example.petstore.backend.api.model.Pet;
import com.example.petstore.backend.db.PetRepository;
import com.example.petstore.backend.db.PetBE;
import static org.mockito.Mockito.when;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.AdditionalAnswers.returnsFirstArg;
#SpringBootTest
public class PetControllerTest {
#InjectMocks
private PetController petController;
#MockBean
private PetRepository pr;
#Test
void testAddPet() {
when(pr.save(any(PetBE.class))).then(returnsFirstArg());
Pet p1 = new Pet().id(5L).name("Klaus");
assertNotNull(petController);
/*L35*/ ResponseEntity<Pet> r = petController.addPet(p1);
assertEquals(new ResponseEntity<Pet>(p1, HttpStatus.OK), r);
}
}
When I run this method as a gradle test, I get
com.example.petstore.backend.api.implementation.PetControllerTest > testAddPet() FAILED
java.lang.NullPointerException at PetControllerTest.java:35
which is petController.addPet(p1);.
My printlns in addPet are not displayed and I can't set any breakpoints there because it is mocked. The only reference in addPet that could be null is pr, but it works fine when I send a request with curl.
I've also tried adding
#BeforeAll
public void setup() {
MockitoAnnotations.initMocks(this);
}
because it was suggested here but that gave an InitializationException:
com.example.petstore.backend.api.implementation.PetControllerTest > initializationError FAILED
org.junit.platform.commons.JUnitException at LifecycleMethodUtils.java:57
How can I debug this?
How can I get this to work?
You're mixing annotations from various testing frameworks here. If you wish to use the Mockito annotation #InjectMocks then I'd recommend not using any Spring-related mocking annotations at all, but rather the #Mock annotation to create a mocked version of the bean you want to inject (into the #InjectMocks-annotated field). Also make sure you bootstrap the Mockito extension with #ExtendWith(MockitoExtension.class). Something like:
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import com.example.petstore.backend.api.model.Pet;
import com.example.petstore.backend.db.PetRepository;
import com.example.petstore.backend.db.PetBE;
import static org.mockito.Mockito.when;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.AdditionalAnswers.returnsFirstArg;
#ExtendWith(MockitoExtension.class)
public class PetControllerTest {
#InjectMocks
private PetController petController;
#Mock
private PetRepository pr;
#Test
void testAddPet() {
when(pr.save(any(PetBE.class))).then(returnsFirstArg());
Pet p1 = new Pet().id(5L).name("Klaus");
assertNotNull(petController);
ResponseEntity<Pet> r = petController.addPet(p1);
assertEquals(new ResponseEntity<Pet>(p1, HttpStatus.OK), r);
}
}
EDIT: Calling MockitoAnnotations.initMocks(this) inside for example a #BeforeEach-annotated method is necessary if you don't want to use the MockitoExtension. They're essentially the same thing, but it's less necessary in JUnit Jupiter because you can extend a test class with multiple extensions, which was not possible in JUnit 4.x. So if you wanted to bootstrap your test with both a Spring context and Mockito, then you had to pick one of them and setup the other one yourself.