I'm testing a class which injects an optional Bean and does some special handling when such bean is not available. E.g
@Service
public class FlexibleService {
@Autowired(required=false)
private UnreliableService unreliableService;
doSomething() {
if (unreliableService == null) {
// Handle it
}
}
}
The question is: how do I setup this scenario in my JUnit and Mockito test case?
So far, this is what I have.
@ExtendWith(MockitoExtension.class)
class FlexibleServiceTest {
@InjectMocks
FlexibleService target;
@Mock
UnreliableService unreliableService;
@Test
void shouldHandleItWhenUnreliableServiceIsNotAvailable() {
unreliableService = null;
target.doSomething();
// expect handling to be made
}
However, this doesn't work as expected. The bean is injected and my handling code is not reached. How can I force the UnreliableService
bean to be provided as null
in my test case?
Of course, if the unreliableService
was public or had some setter, or even if we had an all args constructor for FlexibleService
, we could easily pass unreliableService
as null. However, none of these are supposed to happen. Is there any way to work around this?
I'm testing a class which injects an optional Bean and does some special handling when such bean is not available. E.g
@Service
public class FlexibleService {
@Autowired(required=false)
private UnreliableService unreliableService;
doSomething() {
if (unreliableService == null) {
// Handle it
}
}
}
The question is: how do I setup this scenario in my JUnit and Mockito test case?
So far, this is what I have.
@ExtendWith(MockitoExtension.class)
class FlexibleServiceTest {
@InjectMocks
FlexibleService target;
@Mock
UnreliableService unreliableService;
@Test
void shouldHandleItWhenUnreliableServiceIsNotAvailable() {
unreliableService = null;
target.doSomething();
// expect handling to be made
}
However, this doesn't work as expected. The bean is injected and my handling code is not reached. How can I force the UnreliableService
bean to be provided as null
in my test case?
Of course, if the unreliableService
was public or had some setter, or even if we had an all args constructor for FlexibleService
, we could easily pass unreliableService
as null. However, none of these are supposed to happen. Is there any way to work around this?
4 Answers
Reset to default 3I think you're overthinking things here - @InjectMocks
will inject the respective mocks to all of the class' fields. The easiest way of keeping a field null
is just not to provide a mock for it:
@ExtendWith(MockitoExtension.class)
class FlexibleServiceTest {
@InjectMocks
FlexibleService target;
@Test
void shouldHandleItWhenUnreliableServiceIsNotAvailable() {
// Since there's no mock for unreliableService, it's implicitly null
target.doSomething();
// expect handling to be made
}
}
Note that this may mean that this test case has its own test class because you do need to mock the unreliable service for other test cases.
As @knittl rightly noted in the comments, it is better to switch to injection via the constructor, because using InjectMocks we cannot change the injected bean. With constructor injection it can look like this:
class FlexibleServiceTest {
private FlexibleService flexibleService;
@Test
void testWhenUnreliableServiceIsPresent() {
UnreliableService unreliableServiceMock = mock(UnreliableService.class);
when(unreliableServiceMock.foo()).thenReturn(something);
flexibleService = new FlexibleService(unreliableServiceMock);
flexibleService.doSomething();
}
@Test
void testWhenUnreliableServiceIsNull() {
flexibleService = new FlexibleService(null);
flexibleService.doSomething();
}
}
To autowire an optional bean via constructor, you can use @Nullable
annotation :
import .springframework.lang.Nullable;
import .springframework.stereotype.Service;
@Service
public class FlexibleService {
private final UnreliableService unreliableService;
public FlexibleService(@Nullable UnreliableService unreliableService) {
this.unreliableService = unreliableService;
}
}
But if you cannot switch to constructor injection, there is an option to inject values via ReflectionTestUtils
:
import .springframework.test.util.ReflectionTestUtils;
class FlexibleServiceTest {
private FlexibleService flexibleService = new FlexibleService();
@Test
void testWhenUnreliableServiceIsPresent() {
UnreliableService unreliableServiceMock = mock(UnreliableService.class);
ReflectionTestUtils.setField(flexibleService, "unreliableService", unreliableServiceMock);
flexibleService.doSomething();
}
@Test
void testWhenUnreliableServiceIsNull() {
// unreliableService is null by default
flexibleService.doSomething();
}
}
In addition to the other answers, there is also Spring's ReflectionTestUtils
which you can use to reach into the class under test and reflectively set fields:
import .springframework.test.util.ReflectionTestUtils;
@Test
void shouldHandleItWhenUnreliableServiceIsNotAvailable() {
ReflectionTestUtils.setField(target, "unreliableService", null);
target.doSomething();
// expect handling to be made
}
Unlike some other approaches, this won't require you to have a separate test class for this one test.
It's unclear why you have Mockito create a Mock instance when you explicitely don't want to have an instance.
If there is no Mock instance of the right type known to Mockito, nothing will be injected and the field will remain null
.
@ExtendWith(MockitoExtension.class)
public class FlexibleServiceTest {
@InjectMocks
FlexibleService target;
// don't have Mockito create an UnreliableService Mock instance
@Test
void shouldHandleItWhenUnreliableServiceIsNotAvailable() {
target.doSomething();
// expect handling to be made
}
}
In fact, you can even drop the @InjectMocks
annotation and get rid of Mockito altogether:
public class FlexibleServiceTest {
@Test
void shouldHandleItWhenUnreliableServiceIsNotAvailable() {
final FlexibleService target = new FlexibleService();
target.doSomething();
// expect handling to be made
}
}
@Mock UnreliableService unreliableService;
from your class definition? Then there's no mock to inject and the field will remain null. Not sure why you create mock when you specifically want to test for absence? – knittl Commented Jan 29 at 20:40@InjectMocks
is a Mockito annotation and has nothing to do with Spring dependency injection. – knittl Commented Jan 29 at 20:41