6

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 -

  1. 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?
  2. 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:

  1. Introduce a setter for self and use it to inject self in setup method. Don't want to add a setter only for tests.
  2. Use ReflectionUtils to set self. Reflection seems unclean to me. I would prefer @Setter over this.
  3. Tried using @RunWith(SpringJunit4Runner.class) but that failed as expected because I am not creating a proper context and stuff.
asked Jul 31, 2020 at 7:01
2
  • 1
    Might this be a good case for refactoring the service and someTransactionalMethod() into a different class so that it can be mocked? Commented Jul 31, 2020 at 7:20
  • someTransactionalMethod is well defined in this service. Only for testing, dont think it good idea to refactor. Commented Jul 31, 2020 at 8:33

2 Answers 2

8

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.

Vlad Schnakovszki
8,6126 gold badges84 silver badges115 bronze badges
answered May 20, 2021 at 11:23
Sign up to request clarification or add additional context in comments.

2 Comments

I have already mentioned that this is not the approach I would want to go ahead with.
Self-injection in itself is a hack, so I think the proposed answer is a good one.
0

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.

answered Dec 12, 2022 at 7:20

Comments

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.