15

I have a bean similar to this:

@Service
public class A {

    @Autowired
    private B b;

    @PostConstruct
    public void setup() {
       b.call(param);
    }
}

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = { Application.class, Config.class })
@WebIntegrationTest(randomPort = true)
public class Test {

    @Autowired
    B b;

    @Before
    public void setUp() throws Exception {
        when(b.call(any())).thenReturn("smth");
    }

    @Test
    public void test() throws Exception {
        // test...
    }
}

The problem is that PostConstruct is called before setUp when the test is run.

moffeltje
  • 4,521
  • 4
  • 33
  • 57
Andy
  • 953
  • 1
  • 8
  • 18
  • @hzpz Class A have other logic which is called in test latter. And answering your question I would like to test logic of the class A. – Andy Jul 23 '15 at 12:44

3 Answers3

19

If you want to write a unit test of A, then don't use Spring. Instead, instantiate A yourself and pass a stub/mock of B (either by using constructor injection or ReflectionTestUtils to set the private field).

For example:

@Service
public class A {

    private final B b;    

    @Autowired
    public A(B b) {
        this.b = b;
    }

    @PostConstruct
    public void setup() {
       b.call(param);
    }
}

-

public class Test {

    @Test
    public void test() throws Exception {
        B b = mock(b);
        A a = new A(b);
        // write some tests for A
    }

}

If you have to use Spring, because you want to write an integration test, use a different application context, where you replace B with a stub/mock.

For example, assuming B is instantiated in a Production class like this:

@Configuration
public class Production {

    @Bean
    public B b() {
        return new B();
    }

}

Write another @Configuration class for your tests:

@Configuration
public class Tests {

    @Bean
    public B b() {
        // using Mockito is just an example
        B b = Mockito.mock(B.class); 
        Mockito.when(b).thenReturn("smth"); 
        return b;
    }

}

Reference it in your test with the @SpringApplicationConfiguration annotation:

@SpringApplicationConfiguration(classes = { Application.class, Tests.class })
hzpz
  • 7,536
  • 1
  • 38
  • 44
  • Thanks! Yes, it is almost right. Additionally `@Configuration public class Tests { @Bean public B b() { B b = Mockito.mock(B.class); Mockito.when(b).thenReturn("smth"); return b; } }` – Andy Jul 23 '15 at 13:04
  • this 1st part of the solution is avoiding the real problem by killing the “integration” part of the test. The 2nd part is much better – cahen Jul 23 '15 at 13:12
  • 2
    @cahen That is why I started with "If you want to write a **unit** test". In my experience, people often tend to write integration test when they actually want a unit test. – hzpz Jul 23 '15 at 13:19
  • @Andy I updated my answer according to your comment. – hzpz Jul 23 '15 at 13:20
  • In first part of solution you avoiding a problem, in second part of solution you also avoiding the problem by not using @Service annotation – JJ Roman Jun 09 '21 at 21:39
  • This is _one_ valid solution that fits the question. There may be others. If you have a different problem for which this is not a solution, please ask a new question instead of downvoting this answer. – hzpz Jun 10 '21 at 19:52
2

Just had this exact problem on a project I'm working on, Here is the solution I used in terms of the question code:

  1. @Autowire in the bean with the @PostConstruct to your test.
  2. Do your setup in the @Before.
  3. Explicitly call the @PostConstruct at the end of your @Before.
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = { Application.class, Config.class })
@WebIntegrationTest(randomPort = true)
public class Test {

    // wire in the dependency as well
    @Autowired
    A a;

    @Autowired
    B b;

    @Before
    public void setUp() throws Exception {
        when(b.call(any())).thenReturn("smth");
        
        // "manual" call to @PostConstruct which will now work as expected
        a.setup(); 
    }

    @Test
    public void test() throws Exception {
        // test...
    }
}

Obviously your @PostConstruct method has to be idempotent as its going to get called twice. Also it assumes default singleton bean behaviour.

tgallei
  • 827
  • 3
  • 13
  • 22
0

another alternative is to instantiate the application context on the test yourself and then inject the mocks prior to refreshing the context, for example, as in:

@Configuration
@ComponentScan
public class TestConfiguration {}
...
ClassToMock mock = mock(ClassToMock.class);
AnnotationConfigApplicationContext c = new AnnotationConfigApplicationContext();
c.getDefaultListableBeanFactory().registerResolvableDependency(
        ClassToMock.class,
        mock);
c.register(TestConfiguration.class);
c.refresh();

This alternative is useful when there are @PostConstruct annotations on the context and you want to set expectations on the mock prior.

beluchin
  • 12,047
  • 4
  • 24
  • 35