Тестирование Spring bean с пост-конструкцией

У меня есть bean-компонент, подобный этому:

@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...
    }
}

Проблема в том, что PostConstruct вызывается перед setUp при запуске теста.


person Andy    schedule 23.07.2015    source источник
comment
@hzpz Класс A имеет другую логику, которая вызывается в последнем тесте. И отвечая на ваш вопрос, я хотел бы проверить логику класса A.   -  person Andy    schedule 23.07.2015


Ответы (3)


Если вы хотите написать модульный тест A, не используйте Spring. Вместо этого создайте экземпляр A самостоятельно и передайте заглушку/макет B (либо с помощью внедрения конструктора, либо ReflectionTestUtils для установки частного поля).

Например:

@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
    }

}

Если вам нужно использовать Spring, потому что вы хотите написать интеграционный тест, используйте другой контекст приложения, где вы замените B заглушкой/фиктивным.

Например, предположим, что B создается в классе Production следующим образом:

@Configuration
public class Production {

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

}

Напишите еще один класс @Configuration для ваших тестов:

@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;
    }

}

Ссылайтесь на него в своем тесте с аннотацией @SpringApplicationConfiguration:

@SpringApplicationConfiguration(classes = { Application.class, Tests.class })
person hzpz    schedule 23.07.2015
comment
Спасибо! Да, это почти правильно. Дополнительно @Configuration public class Tests { @Bean public B b() { B b = Mockito.mock(B.class); Mockito.when(b).thenReturn("smth"); return b; } } - person Andy; 23.07.2015
comment
эта 1-я часть решения позволяет избежать реальной проблемы, убив «интеграционную» часть теста. 2 часть намного лучше - person cahen; 23.07.2015
comment
@cahen Вот почему я начал с «Если вы хотите написать модульный тест». По моему опыту, люди часто склонны писать интеграционные тесты, когда им действительно нужен модульный тест. - person hzpz; 23.07.2015
comment
@ Энди, я обновил свой ответ в соответствии с вашим комментарием. - person hzpz; 23.07.2015
comment
В первой части решения вы избегаете проблемы, во второй части решения вы также избегаете проблемы, не используя аннотацию @Service. - person JJ Roman; 10.06.2021
comment
Это одно правильное решение, которое подходит к вопросу. Могут быть и другие. Если у вас есть другая проблема, для которой это не решение, задайте новый вопрос вместо того, чтобы отрицать этот ответ. - person hzpz; 10.06.2021

Только что возникла эта точная проблема в проекте, над которым я работаю. Вот решение, которое я использовал с точки зрения кода вопроса:

  1. @Autowire в компоненте с @PostConstruct для вашего теста.
  2. Выполните настройку в файле @Before.
  3. Явно вызовите @PostConstruct в конце вашего @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...
    }
}

Очевидно, что ваш метод @PostConstruct должен быть идемпотентным, так как он будет вызываться дважды. Также он предполагает поведение одноэлементного компонента по умолчанию.

person Paul Campbell    schedule 20.07.2020

другой альтернативой является создание экземпляра контекста приложения в тесте самостоятельно, а затем внедрение макетов перед обновлением контекста, например, как в:

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

Эта альтернатива полезна, когда в контексте есть @PostConstruct аннотации, и вы хотите установить ожидания для фиктивного априора.

person beluchin    schedule 31.08.2019