4

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);

   }
}
gevorg
  • 4,835
  • 4
  • 35
  • 52
Alex
  • 51
  • 5

2 Answers2

2

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);
}
gevorg
  • 4,835
  • 4
  • 35
  • 52
  • Unfortunately directly autowriing UpdateService does not help me because I need to be able to construct the object with a mocked UserService object – Alex Jun 22 '16 at 18:08
  • @Alex That is ok, for that you have two options, you can make `private UserService userService` protected and swap real bean with mock by assigning mock instance or use reflection to change private field. – gevorg Jun 22 '16 at 18:09
  • I want to avoid having to set the UserService directly. Using reflection with Whitebox did the trick though, thanks! – Alex Jun 22 '16 at 18:23
  • I realized that reflection with Whitebox works only when using autowired annotation on updateService in test class. It does not work when trying to build the service with a constructor and then calling .setInternalState() afterwards. I would like to stick with the solution but my boss is not satisfied :/ – Alex Jun 22 '16 at 20:04
  • If you don't autowire updateService it will be out of Spring's context and you cannot use Spring Annotations out of it's context. So you don't have many options there. – gevorg Jun 22 '16 at 20:07
2

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;
      }
   }
}
Arne Burmeister
  • 20,046
  • 8
  • 53
  • 94