2

I searched SO and found bunch of other questions that looked similar but not exactly, so I'll ask another one.

I have Spring application and say I created custom aspect (looking for CatchMe annotation) to log exceptions in a specific way. I want to test the aspect by mocking the behavior of one of my Spring @Service class's method so it throws exception when it is called. Then in another method, annotated with my custom annotation @CatchMe, I call the first method. What I expect to happen is the exception to get logged. Unfortunatelly the exception is thrown but the aspect is not triggered. So how can I make the aspect to get triggered in this test using Mockito?

Note: I've checked those (plus a bunch more):

but most of them are Controller related and not Service related and I want to test only the service.

The Test

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {BeanConfig.class})
public class MyServiceTest {

    @Autowired
    @InjectMocks
    private MyService service;

    @Mock
    private MyServiceDependency serviceDep;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
        ReflectionTestUtils.setField(service, "serviceDep", serviceDep);
    }

    @Test
    public void test() {
        when(serviceDep.process()).thenAnswer(new Answer<Object>() {

                @Override
                public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
                    throw new Exception("Sample message.");
                }

            });

        service.execute();
    }
}

Services

@Service
public class MyService {

    @Autowired
    private MyServiceDependency serviceDep;

    @CatchMe
    public void execute() {
        serviceDep.process();
    }
}


@Service
public class MyServiceDependency {

    public Object process() {
        // may throw exception here
    }
}

Configuration and Aspect

@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages = {"com.example.services"})
public class BeanConfig { .. }


@Aspect
@Component
public class CatchMeAspect {

    @Around("@annotation(CatchMe)")
    public Object catchMe(final ProceedingJoinPoint pjp) throws Throwable {
        try {
            pjp.proceed();
        } catch (Throwable t) {
            // fency log
        }

    }
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CatchMe {}

EDIT: The functionality works but I want to verify it with the test.

nyxz
  • 6,918
  • 9
  • 54
  • 67
  • 1
    If it is only retry you want use `spring-retry` instead of reinventing the wheel. Why on earth is your test class a `@Configuration` all the annotations do nothing as it isn't a spring based testclass. You should have a `@ContextConfiguration` and `@RunWith(SpringJUnit4Runner.class)` to have spring do things. – M. Deinum Feb 04 '16 at 14:39
  • Also the fact of something is a controller or whatever doesn't matter (at least for a unit test). – M. Deinum Feb 04 '16 at 14:39
  • Yes, you're right. I messed up the sample. Let me fix it. About the Controller vs Service - if you check the answers the fix that is proposed is to use `MockMvcBuilders` to build `MockMvc` that is used to call the controller. This is not applicable in my case. – nyxz Feb 04 '16 at 14:43
  • @M.Deinum how about now? – nyxz Feb 04 '16 at 14:48
  • As mentioned I was mentioning UNIT tests not INTEGRTATION/SYSTEM tests! But then we are discussing definitions and not your issue. – M. Deinum Feb 04 '16 at 14:52
  • Is the aspect even in the package that gets scanned? Also I believe the `@annotation` requires the FQN of the annotation and not just the classname and you need to switch on `proxyTargetClass=true` on the `@EnableAspectJAutoProxy` – M. Deinum Feb 04 '16 at 14:53
  • The functionality is working - when I run the application I see the proper results, but I want to test it. I updated the question for that right now. I will check the proxyTargetClass. – nyxz Feb 04 '16 at 14:57
  • The point is that you are resetting a dependency in a class based proxy. You are setting it on the proxy and NOT on the actual object instance. Use `AopTestUtils.getUltimateTargetObject(service)` to get the actual object instance to set the field on. – M. Deinum Feb 04 '16 at 15:03
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/102610/discussion-between-nyxz-and-m-deinum). – nyxz Feb 04 '16 at 15:17

2 Answers2

2

Actually it is working as expected, however you are running in a side effect of proxy based AOP, especially class based proxies in this case.

Currently you are setting the field on the proxy and not on the actual object inside the proxy. Which is what you actually want. To obtain the actual instance use AopTestUtils.getUltimateTargetObject and then use that in the ReflectionTestUtils.setField method.

@Autowired
@InjectMocks
private MyService service;

@Mock
private MyServiceDependency serviceDep;

@Before
public void setUp() throws Exception {
    MockitoAnnotations.initMocks(this);
    MyService serviceToInject = AopTestUtils.getUltimateTargetObject(service);
    ReflectionTestUtils.setField(serviceToInject, "serviceDep", serviceDep);
}

However I think that approach is wrong, when you start messing around like this there is a better way. Simply use Spring to inject the mock. Create a specific @Configuration class for this test case. Make it a internal public static class and for the dependency add a mocked @Bean.

@Configuration
@Import(BeanConfig.class) 
public static class TestBeanConfig {

    @Bean
    public MyServiceDependency myServiceDependency() {
        return Mockito.mock(MyServiceDependency.class);
    }
}

Now in your test class you can simply @Autowire both beans and not need to use reflection or whatever to set dependencies.

@RunWith(SpringJUnit4ClassRunner.class)
public class MyServiceTest {

    @Autowired
    private MyService service;

    @Autowired
    private MyServiceDependency serviceDep;

    @Test
    public void test() {
        when(serviceDep.process()).thenAnswer(new Answer<Object>() {

                @Override
                public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
                    throw new Exception("Sample message.");
                }

            });

        service.execute();
    }
}

Which will take care of the correct dependencies.

M. Deinum
  • 115,695
  • 22
  • 220
  • 224
  • Should I add `@ContextConfiguration(classes = {TestBeanConfig.class})` at `MyServiceTest` ? – nyxz Feb 04 '16 at 15:23
  • No. It will be auto detected... If you made it in inner bean of course. – M. Deinum Feb 04 '16 at 15:25
  • I like your approach - it's much cleaner. But the aspect is still not triggered :/ – nyxz Feb 04 '16 at 15:42
  • Make sure that there aren't 2 instances of the dependency and that your mock isn't used. Also check that the bean you want to test is actually a proxy. – M. Deinum Feb 04 '16 at 16:04
  • The bean is a proxy - I test it with AopUtils.isAopProxy. There aren't two instances - I verify by running the test and checking that there isn't "Found 2 instances" error or something like that. I added another one on purpose and the error occurred. I assume this simply doesn't work with Mockito :/ – nyxz Feb 05 '16 at 12:21
  • I wonder if the mockito mock is actually injected or that it is overriden by the actual bean . Hmm on second thought that cannot be the issue if you are autowiring it and recording behavior it is mock (you aren't recreating the mocks are you!). – M. Deinum Feb 05 '16 at 12:31
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/102702/discussion-between-nyxz-and-m-deinum). – nyxz Feb 05 '16 at 12:33
1

I had the same problem as @nyxz and this is intentional, see https://github.com/spring-projects/spring-boot/issues/7243.

Inspired by @M. Deinum following solution worked for me with Spring Boot 2.3.4.RELEASE and JUnit 5. We will just provide a mocked bean without @MockedBean

@SpringBootTest
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
class MyServiceTest {

    @Autowired
    private MyService service;

    @Test
    public void test() {
        service.execute();
    }

    static class TestBeanConfig {

         @Bean
         @Primary
         public MyServiceDependency myServiceDependency() {
             MyServiceDependency myServiceDependency = Mockito.mock(MyServiceDependency.class)
             // Add behavior of mocked bean here
             return myServiceDependency;
         }
    }
}
RoBeaToZ
  • 1,113
  • 10
  • 18