We are using Spring 5.2.x (with Spring Boot 2.3.x) & Mockito 3.3.x.
I need to call a @Transactional method in my service from within it. Hence I had to resort to self injection.
@Service
@RequiredArgsCostructor // lombok
public class MyClass {
private final dep1;
private final dep2;
@Autowired
private MyClass self;
public void someMethod() {
self.someTransactionalMethod();
// do something
}
@Transactional
public void someTransactionalMethod() {
// do something
}
}
public class MyClassTest {
@Mock
private dep1;
@Mock
private dep2;
@InjectMocks
private MyClass myClass;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
}
@Test
public void someMethodTest() {
// NPE when calling self.someTransactionalMethod()
}
}
From the code it is relevant that we are using constructor injection using lombok. For obvious reasons, I cannot declare the instance variable self as final, hence using @Autowired for this one. The class works fine but I see issues with the unit test.
There are 2 parts to my question -
- Regarding the way I am doing self injection - is this the best way of doing self injection? Is there any other approach recommended by the authors of Spring framework in recent versions of Spring?
- Mockito is by no means able to inject the self bean into the object of CUT (Class Under Test). Is there a way to achieve this in a clean way?
I have already considered the following:
- Introduce a setter for
selfand use it to inject self in setup method. Don't want to add a setter only for tests. - Use
ReflectionUtilsto setself. Reflection seems unclean to me. I would prefer@Setterover this. - Tried using
@RunWith(SpringJunit4Runner.class)but that failed as expected because I am not creating a proper context and stuff.
2 Answers 2
In your setUp() method call:
ReflectionTestUtils.setField(myClass, "self", myClass)
You'll also need to add spring-test as a dependency if you haven't already.
2 Comments
If injecting self references is difficult or you need ReflectionTestUtils to fulfill it, it is because the test is not a pure unit test and in your case because the service does not have a well defined interface. If we change the service to implement an interface and change the test to be a pure unit test, the code becomes as follows and the problems disappear:
public class MyClass implements MyInterface {
@Inject
private MyInterface self;
@Override
public void someMethod() {
self.someTransactionalMethod();
}
@Override
@Transactional
public Object someTransactionalMethod() {
throw new UnsupportedOperationException("Not implemented yet");
}
}
And the test becomes:
@RunWith(MockitoJunitRunner.class)
public class MyClassTest {
@Mock
private MyInterface self;
@InjectMocks
private MyClass service;
@Test
public void someMethodShouldCallSelf() {
when(self.someTransactionalMethod()).thenReturn("test");
service.someMethod();
verify(self, times(1)).someTransactionalMethod();
}
}
Now the test for someMethod() follows unit testing principles by actually testing only the code in someMethod() instead of including someTransactionalMethod() in it's complexity. Then you write separate unit tests for someTransactionalMethod() to ensure that it functions correctly.
Comments
Explore related questions
See similar questions with these tags.
someTransactionalMethodis well defined in this service. Only for testing, dont think it good idea to refactor.