Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Testing spring bean with post construct

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.

like image 921
Andy Avatar asked Jul 23 '15 12:07

Andy


2 Answers

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.

like image 94
Paul Campbell Avatar answered Oct 22 '22 09:10

Paul Campbell


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 })
like image 18
hzpz Avatar answered Oct 22 '22 08:10

hzpz