3

My code is setup like this.

abstract class BaseController {
   @Inject Store store; 
}

class MyController extends BaseController {
   private final Validator validator;

   @Inject
   public MyController(Validator validator) {
      this.validator = validator;
   }

   public boolean someMethod() {
      a = store.storingMethod();
      b = validator.validate(a);
      ...
      ...
      return true;
   }
}

Now I wanted to write tests for myController. In the test, I want to use the injected Store but I want to mock out the Validator. I tried something like this.

@RunWith(MockitoJUnitRunner.class)
public class MyControllerTest() {
   private MyController myController;
   @Mock private Validator validator;

   @Before
   public void before() {
      myController = new MyController(validator);
   }
}

I know, if I move the Store store from the BaseController to MyController, I can initialize it in the constructor (just like I did for the validator). But, I want to have the Store in the Base class as it will be used by other classes extending it.

With the way my classes are set up, How do I inject the Store while testing?

Razneesh
  • 1,147
  • 3
  • 13
  • 29
gandalf_grey
  • 287
  • 1
  • 3
  • 11

3 Answers3

5

Don't use field injection. Use constructor injection.

abstract class BaseController {
    final Store store; 

    BaseController(Store store) {
        this.store = store;
    }
}

class MyController extends BaseController {
   private final Validator validator;

   @Inject
   public MyController(Validator validator, Store store) {
      super(store);
      this.validator = validator;
   }
}

There is a bit of debate on the subject, but your example is a clear example of a situation in which the use of field injection makes a class much harder to test.

Spring @Autowire on Properties vs Constructor

Dependency Injection: Field Injection vs Constructor Injection?

It is also worth noting that

The Spring team generally advocates constructor injection

Source

Michael
  • 41,989
  • 11
  • 82
  • 128
2

I usually solve this using the following pattern:

abstract class BaseController {
    private final Store store; 

    protected BaseController (Store store) {
        this.store = store;
    }

    protected Store getStore() {
        return this.store;
    }
}

class MyController extends BaseController {
    private final Validator validator;

    @Inject
    public MyController(Store store, Validator validator) {
       super(store);
       this.validator = validator;
    }

    public boolean someMethod() {
        a = getStore().storingMethod();
        b = validator.validate(a);
        ...
        ...
        return true;
    }
}

So the base class can be used regardless of any injection framework available.

Smutje
  • 17,733
  • 4
  • 24
  • 41
1

You can use ReflectionTestUtils to set your field value.

Import it in your pom.xml:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.1.2.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.1.2.RELEASE</version>
    <scope>test</scope>
</dependency>

Use it to set your store:

@RunWith(MockitoJUnitRunner.class)
public class MyControllerTest() {
   private MyController myController;
   @Mock private Validator validator;

   @Before
   public void before() {
      myController = new MyController(validator);
      ReflectionTestUtils.setField(myController, "store", new YourTestStore());

      // more testing
   }
}

More info on that @ https://www.baeldung.com/spring-reflection-test-utils

Also, note that I do not think this is best practice.

Demogorii
  • 656
  • 5
  • 16
  • Good alternative for cases where changing the base class is not possible. It is worth noting that this may be quite fragile as an IDE is not going to understand the relationship between the `store` field and the string in the test. – Michael Jan 06 '20 at 16:01