Как издеваться над сервером REST перед инициализацией контекста Spring?

У меня есть Spring Bean, который отправляет запрос внутри своего конструктора с помощью RestTemplateBuilder. Во время тестирования я хотел бы смоделировать ответ с помощью MockRestServiceServer (или, может быть, смоделировать RestTemplateBuilder, который используется для отправки запроса) с некоторыми предопределенными данными только для загрузки контекста приложения. Адрес записан в файл application.properties.

Я пробовал это сделать:

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.autoconfigure.web.client.AutoConfigureMockRestServiceServer;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.test.web.client.match.MockRestRequestMatchers;

import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;

@RunWith(SpringRunner.class)
@AutoConfigureMockRestServiceServer
@SpringBootTest
public class CollectorApplicationTest {

    @Autowired
    MockRestServiceServer server;

    @Value("${components.web-admin-portal.rest.schemas}")
    String webAdminPortal;

    @Before
    public void init() {

        server.expect(MockRestRequestMatchers.requestTo(webAdminPortal))
              .andRespond(withSuccess("{}", MediaType.APPLICATION_JSON));
    }

    @Test
    public void contextLoads() {

    }
}

Но контекст загружается до выполнения метода @Before и завершается ошибкой с сообщением о том, что MockRestServiceServer не ожидал запроса.

Затем я попробовал использовать ApplicationContextInitializer:

import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.http.MediaType;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.test.web.client.match.MockRestRequestMatchers;

import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;

public class AppInit implements ApplicationContextInitializer<ConfigurableApplicationContext> {

    @Override
    public void initialize(final ConfigurableApplicationContext context) {

        context.refresh();
        MockRestServiceServer server         = context.getBean(MockRestServiceServer.class);
        String                webAdminPortal = context.getEnvironment()
                                                      .getProperty("components.web-admin-portal.rest.schemas");

        server.expect(MockRestRequestMatchers.requestTo(webAdminPortal))
              .andRespond(withSuccess("{}", MediaType.APPLICATION_JSON));
    }
}

Но затем он жалуется, что MockServerRestTemplateCustomizer has not been bound to a RestTemplate. Я подумал, что эту проблему можно решить, используя аннотацию @RestClientTest в тестовом классе, поскольку она отключит автоматическую настройку RestTemplateBuilder и позволит настроить MockRestServiceServer:

@RunWith(SpringRunner.class)
@SpringBootTest
@RestClientTest
@ContextConfiguration(initializers = AppInit.class)
public class CollectorApplicationTest {

Но это ничего не изменило.

Заранее спасибо.

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
0
2 418
3

Ответы 3

эта настройка работает для меня:

@Service
public class MyService implements ApplicationListener<ContextRefreshedEvent> {

    private final RestTemplate restTemplate;

    public MyService(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
        //make your call here
        restTemplate.getForEntity("http://www.localhost:9090", String.class);
    }

}

Это вызывает вызов тогда и только тогда, когда контекст Spring полностью инициализирован.

и в тестовых классах:

@TestConfiguration
public class MockServiceCallConfiguration {

    @Autowired
    private RestTemplate restTemplate;

    @Bean
    public MockRestServiceServer mockRestServiceServer() {
        MockRestServiceServer server = MockRestServiceServer.createServer(restTemplate);

        server.expect(MockRestRequestMatchers.requestTo("http://www.localhost:9090"))
                .andRespond(withSuccess("{}", MediaType.APPLICATION_JSON));

        return server;
    }
}

@SpringBootTest(classes = {MockServiceCallConfiguration.class})
@RunWith(SpringRunner.class)
public class MyServiceTest {


    @Test
    public void test() {

    }

}

Это довольно утомительно, если вы можете реорганизовать свой код и избежать вызова службы отдыха во время настройки инициализации, а скорее на каком-то бизнес-мероприятии, это наверняка будет лучше

Я попробую это завтра и предоставлю обновление, спасибо за идею!

Sam 05.04.2018 17:47

@Sam без проблем дайте мне знать

slemoine 06.04.2018 09:21

Не уверен, что это меняет. @PostConstruct запускается сразу после конструктора, поэтому абсолютно ничего не изменилось.

Sam 06.04.2018 13:00

@Sam извините, похоже, я неправильно понял ваш вариант использования, я публикую новое решение. Я видел, что ты нашел свой, выбери для себя лучшее

slemoine 06.04.2018 14:54

Спасибо за обновление ответа! Я не могу выполнить рефакторинг кода, потому что приложение должно получить некоторые настройки от другого компонента через HTTP, чтобы иметь возможность функционировать, поэтому необходимо разместить его при запуске. Кроме того, вы используете RestTemplate, а я - RestTemplateBuilder, так что это немного другое. Я подумаю о переносе запроса в метод слушателя, спасибо

Sam 10.04.2018 11:52

Я нашел способ (если у кого-то есть решение получше, оставьте, пожалуйста, другой ответ). @RestClientTest делает это, что довольно близко к тому, что необходимо в данном случае, но недостаточно. Нам нужен RestTemplateBuilder, чтобы всегда генерировать один и тот же RestTemplate, и нам также нужен MockRestServiceServer, связанный с этим RestTemplate. Я сделал это, заменив определение компонента RestTemplateBuilder на макет, а также предварительно сконфигурировав MockRestServiceServer прямо в определении компонента, чтобы убедиться, что этот код выполняется до отправки любых запросов.

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.test.web.client.match.MockRestRequestMatchers;
import org.springframework.web.client.RestTemplate;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;

@RunWith(SpringRunner.class)
@SpringBootTest
public class CollectorApplicationTest {

    @Test
    public void contextLoads() {

    }

    @TestConfiguration
    static class A {

        @Primary
        @Bean
        public RestTemplateBuilder restTemplateBuilder(@Value("${components.web-admin-portal.rest.schemas}")
                                                               String webAdminPortal) {

            RestTemplate          restTemplate = new RestTemplate();
            MockRestServiceServer server       = MockRestServiceServer.createServer(restTemplate);
            server.expect(MockRestRequestMatchers.requestTo(webAdminPortal))
                  .andRespond(withSuccess("{}", MediaType.APPLICATION_JSON));

            RestTemplateBuilder mockBuilder = mock(RestTemplateBuilder.class);
            when(mockBuilder.build()).thenReturn(restTemplate);
            return mockBuilder;
        }
    }
}

Таким образом приложение запустится успешно.

Глядя на свой старый вопрос в 2021 году, я бы сделал это по-другому.

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

@Service
class MyService {

  MySettingsFromRemote settings;

  MyService(RestTemplateBuilder builder, @Value("${my-url}") String url){
    var rt = builder.build();
    setting = rt.getForEntity(url, MySettingsFromRemote.class);
  }
  
  ...
}

Поэтому вместо этого я бы реорганизовал код следующим образом:

@Service
class MyService {

  MySettingsFromRemote settings;

  MyService(MySettingsFromRemote settings){
    this.settings = settings;
  }
  
  ...
}


@Configuration
class MyConfig {

  // maybe Spring provides this bean now?
  // rest template is kinda outdated already, webclient is preferred.
  @Bean RestTemplate myRestTemplate(RestTemplateBuilder builder) {
    return builder.build();
  }

  @Bean MySettingsFromRemote mySettingsFromRemote(RestTemplate restTemplate, @Value("${my-url}") String url){
    return restTemplate.getForEntity(url, MySettingsFromRemote.class);
  }
}

Тогда это очень легко проверить.

Другие вопросы по теме