Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to prevent Spring from injecting @Autowired references inside a mock?

I want to test a class using Spring + JUnit + Mockito but I don't manage to make it work properly.

Let's say my class references a Service:

@Controller
public class MyController 
{

    @Autowired
    private MyService service;

    @PostConstruct
    public void init() {
        service.whatever();
    }

    public void doSomething() {
        service.create();
    }
}

And this Service references a Repository:

@Service
public class MyService {

    @Autowired
    private MyRepository repository;

    public void whatever() {}

    public void create() {
        repository.save();
    }
}

When testing the MyController class, I want the service to be mocked. The problem is: even when the service is mocked, Spring tries to inject the repository in the mock.

Here is what I did. Test class:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { MyControllerTestConfiguration.class })
public class MyControllerTest {

    @Autowired
    private MyController myController;

    @Test
    public void testDoSomething() {
        myController.doSomething();
    }

}

Configuration class:

@Configuration
public class MyControllerTestConfiguration {

    @Bean
    public MyController myController() {
        return new MyController();
    }

    @Bean
    public MyService myService() {
        return Mockito.mock(MyService.class);
    }

}

And the error I get: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [test.test.MyRepository] found for dependency

I tried to initialize the mock using Mockito's @InjectMocks annotation but this fails because the @PostConstruct method is called before the mocks injection, generating a NullPointerException.

And I cannot simply mock the repository because in real life that would make me mock A LOT of classes...

Can anyone help me on this?

like image 375
Thomas Lulé Avatar asked Mar 16 '23 11:03

Thomas Lulé


2 Answers

Use constructor instead of field injection. That makes testing a lot easier.

@Service
public class MyService {

    private final MyRepository repository;

    @Autowired
    public MyService(MyRepository repository) {
        this.repository = repository;
    }

    public void whatever() {}

    public void create() {
        repository.save();
    }
}

-

@Controller
public class MyController {

    private final MyService service;

    @Autowired
    public MyController(MyService service) {
        this.service = service;
    }

    @PostConstruct
    public void init() {
        service.whatever();
    }

    public void doSomething() {
        service.create();
    }
}

This has several advantages:

  • You don't need Spring in your tests. This allows you to do proper unit tests. It also makes the test incredibly fast (from seconds to milliseconds).
  • You cannot accidentally create an instance of a class without its dependencies which would result in a NullPointerException.
  • As @NamshubWriter pointed out:

    [The instance fields for the dependencies] can be final, so 1) they cannot be accidentally modified, and 2) any thread reading the field will read the same value.

Discard the @Configuration class and write a test like this:

@RunWith(MockitoJUnitRunner.class)
public class MyControllerTest {

    @Mock
    private MyRepository repository;

    @InjectMocks
    private MyService service;

    @Test
    public void testDoSomething() {
        MyController myController = new MyController(service);
        myController.doSomething();
    }

}
like image 69
hzpz Avatar answered Apr 06 '23 12:04

hzpz


Use interfaces, especially if you use some kind of AOP (transactions, security, etc), i.e. you'll have interface MyService and class MyServiceImpl.

In configuration you'll have:

 @Bean
    public MyService myService() {
        return Mockito.mock(MyService.class);
    }
like image 22
jny Avatar answered Apr 06 '23 12:04

jny