Alex
Alex

Reputation: 51

Spring @Autowired constructor causes @Value to return null when instantiated in test class

I'm using an autowired constructor in a service that when instantiated in the test class causes the @Value annotations to return null. Autowiring the dependencies directly solves the problem but the project follows the convention of using constructor based autowiring. My understanding is that instantiating the service in the test class is not creating it from the Spring IoC container which causes @Value to return null. Is there a way to create the service from the IoC container using constructor based autowiring without having to directly access the application context?

Example Service:

@Component
public class UpdateService {

   @Value("${update.success.table}")
   private String successTable;

   @Value("${update.failed.table}")
   private String failedTable;

   private UserService userService

   @Autowired
   public UpdateService(UserService userService) {
      this.userService = userService;
   }
}

Example Test Service:

@RunWith(SpringJUnite4ClassRunner.class)
@SpringApplicationConfiguration(classes = {TestApplication.class})
@WebAppConfiguration
public class UpdateServiceTest {

   private UpdateService updateService;

   @Mock
   private UserService mockUserService;

   @Before
   public void setUp() {
      MockitoAnnotations.initMocks(this);

      updateService = new UpdateService(mockUserService);

   }
}

Upvotes: 4

Views: 975

Answers (2)

Arne Burmeister
Arne Burmeister

Reputation: 20614

The @Value is filled by a property placeholder configurer which is a post processor in the spring context. As your UpdateService is not part of the context it is not processed.

Your setup looks a little like a unclear mixture of unit and integration test. For a unit tests you will not need a spring context at all . Simply make the @Value annotated members package protected and set them or use ReflectionTestUtils.setField() (both shown):

public class UpdateServiceTest {

   @InjectMocks
   private UpdateService updateService;

   @Mock
   private UserService mockUserService;

   @Before
   public void setUp() {
      MockitoAnnotations.initMocks(this);
      ReflectionTestUtils.setField(updateService, "successTable", "my_success");
      updateService.failedTable = "my_failures";
   }
}

For an integration test all wiring should be done by spring.

For this I added a inner config class providing the mock user service (the @Primary is only for the case you have any other user service in your context) and the mock is stored in a static member here to have simple access to the mock from the tests afterwards.

@RunWith(SpringJUnite4ClassRunner.class)
@SpringApplicationConfiguration(classes = {TestApplication.class, UpdateServiceTest.TestAddOn.class})
@WebAppConfiguration
public class UpdateServiceTest {

   @Autowired
   private UpdateService updateService;

   private static UserService mockUserService;



   static class TestAddOn {
      @Bean
      @Primary
      UserService updateService() {
        mockUserService = Mockito.mock(UserService.class);
        return mockUserService;
      }
   }
}

Upvotes: 2

gevorg
gevorg

Reputation: 5055

To make @Value work updateService should be inside of spring context.

The best practice for spring framework integration tests is to include application context in test context and autowiring test source in test:

...
public class UpdateServiceTest  { 
    @Autowired 
    private UpdateService updateService;
    ...

Mock userService

Option with changing userService to protected and considering that test and source classes are in same package.

@Before
public void setUp() {
   MockitoAnnotations.initMocks(this);

   updateService.userService = mockUserService;
}

Option with reflection with Whitebox:

@Before
public void setUp() {
   MockitoAnnotations.initMocks(this);

   Whitebox.setInternalState(updateService, 'userService', mockUserService);
}

Upvotes: 2

Related Questions