Подтвердить что ты не робот

Как обмануть удаленный REST API в unit test с помощью Spring?

Предположим, что я сделал простой клиент в своем приложении, который использует удаленную веб-службу, которая подвергает RESTful API некоторому URI /foo/bar/{baz}. Теперь я хочу unit test мой клиент, который делает вызовы этой веб-службы.

В идеале, в моих тестах, Id хотел бы издеваться над ответами, которые я получаю от веб-службы, учитывая конкретный запрос, например /foo/bar/123 или /foo/bar/42. Мой клиент предполагает, что API на самом деле работает где-то, поэтому мне нужен локальный "веб-сервис" для запуска на http://localhost:9090/foo/bar для моих тестов.

Я хочу, чтобы мои модульные тесты были автономными, аналогично тестированию контроллеров Spring с помощью Spring MVC Test framework.

Некоторые псевдокоды для простого клиента, выборки из удаленного API:

// Initialization logic involving setting up mocking of remote API at 
// http://localhost:9090/foo/bar

@Autowired
NumberClient numberClient // calls the API at http://localhost:9090/foo/bar

@Test
public void getNumber42() {
    onRequest(mockAPI.get("/foo/bar/42")).thenRespond("{ \"number\" : 42 }");
    assertEquals(42, numberClient.getNumber(42));
}

// ..

Каковы мои альтернативы, используя Spring?

4b9b3361

Ответ 2

Лучший способ - использовать WireMock. Добавьте следующие зависимости:

    <dependency>
        <groupId>com.github.tomakehurst</groupId>
        <artifactId>wiremock</artifactId>
        <version>2.4.1</version>
    </dependency>
    <dependency>
        <groupId>org.igniterealtime.smack</groupId>
        <artifactId>smack-core</artifactId>
        <version>4.0.6</version>
    </dependency>

Определите и используйте wiremock, как показано ниже

@Rule
public WireMockRule wireMockRule = new WireMockRule(8089);

String response ="Hello world";
StubMapping responseValid = stubFor(get(urlEqualTo(url)).withHeader("Content-Type", equalTo("application/json"))
            .willReturn(aResponse().withStatus(200)
                    .withHeader("Content-Type", "application/json").withBody(response)));

Ответ 3

Если вы хотите unit test вашего клиента, то вы издеваетесь над сервисами, вызывающими вызовы API REST, то есть с mockito - Я предполагаю, что у вас есть служба, которая делает эти вызовы API для вас, не так ли?

Если, с другой стороны, вы хотите "выкрикивать" остальные API-интерфейсы в том, что есть какой-то сервер, который дает вам ответы, которые будут больше в рамках тестирования интеграции, вы можете попробовать одну из многих фреймворков например restito, rest-driver или betamax.

Ответ 4

Вы можете легко использовать Mockito для насмешки REST API в Spring Boot.

Поместите заглушенный контроллер в ваше тестовое дерево:

@RestController
public class OtherApiHooks {

    @PostMapping("/v1/something/{myUUID}")
    public ResponseEntity<Void> handlePost(@PathVariable("myUUID") UUID myUUID ) {
        assert (false); // this function is meant to be mocked, not called
        return new ResponseEntity<Void>(HttpStatus.NOT_IMPLEMENTED);
    }
}

Ваш клиент должен будет вызвать API на localhost при запуске тестов. Это можно настроить в src/test/resources/application.properties. Если в тесте используется RANDOM_PORT, RANDOM_PORT клиент должен будет найти это значение. Это немного сложно, но проблема решена здесь: Spring Boot - Как получить работающий порт

Сконфигурируйте ваш тестовый класс для использования WebEnvironment (работающий сервер), и теперь ваш тест может использовать Mockito стандартным способом, возвращая объекты ResponseEntity мере необходимости:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class TestsWithMockedRestDependencies {

  @MockBean private OtherApiHooks otherApiHooks;

  @Test public void test1() {
    Mockito.doReturn(new ResponseEntity<Void>(HttpStatus.ACCEPTED))
      .when(otherApiHooks).handlePost(any());
    clientFunctionUnderTest(UUID.randomUUID()); // calls REST API internally
    Mockito.verify(otherApiHooks).handlePost(eq(id));
  }

}

Вы также можете использовать это для сквозного тестирования всего вашего микросервиса в среде с макетом, созданным выше. Один из способов сделать это - TestRestTemplate в ваш тестовый класс и использовать его для вызова вашего REST API вместо clientFunctionUnderTest из примера.

@Autowired private TestRestTemplate restTemplate;
@LocalServerPort private int localPort; // you're gonna need this too

Как это работает

Поскольку OtherApiHooks является @RestController в дереве тестов, Spring Boot автоматически установит указанную службу REST при запуске SpringBootTest.WebEnvironment.

Здесь используется Mockito для насмешки над классом контроллера, а не над службой в целом. Следовательно, Spring Boot будет управлять обработкой на стороне сервера до того, как будет запущен макет. Это может включать в себя такие вещи, как десериализация (и проверка) UUID пути, показанного в примере.

Из того, что я могу сказать, этот подход является надежным для параллельных тестовых прогонов с IntelliJ и Maven.

Ответ 5

Что вы ищете, это поддержка Клиентские тесты REST в тестовой структуре MVC Spring.

Предполагая, что ваш NumberClient использует Spring RestTemplate, эта вышеупомянутая поддержка - это путь!

Надеюсь, что это поможет,

Сэм

Ответ 7

Вот базовый пример того, как издеваться над классом контроллера с Mockito:

Класс контроллера:

@RestController
@RequestMapping("/users")
public class UsersController {

    @Autowired
    private UserService userService;

    public Page<UserCollectionItemDto> getUsers(Pageable pageable) {
        Page<UserProfile> page = userService.getAllUsers(pageable);
        List<UserCollectionItemDto> items = mapper.asUserCollectionItems(page.getContent());
        return new PageImpl<UserCollectionItemDto>(items, pageable, page.getTotalElements());
    }
}

Настройте beans:

@Configuration
public class UserConfig {

    @Bean
    public UsersController usersController() {
        return new UsersController();
    }

    @Bean
    public UserService userService() {
        return Mockito.mock(UserService.class);
    }
}

UserCollectionItemDto - это простой POJO, и он представляет то, что пользователь API отправляет на сервер. UserProfile является основным объектом, используемым на уровне сервиса (классом UserService). Это поведение также реализует шаблон DTO.

Наконец, макет ожидаемого поведения:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader = AnnotationConfigContextLoader.class)
@Import(UserConfig.class)
public class UsersControllerTest {

    @Autowired
    private UsersController usersController;

    @Autowired
    private UserService userService;

    @Test
    public void getAllUsers() {
        initGetAllUsersRules();
        PageRequest pageable = new PageRequest(0, 10);
        Page<UserDto> page = usersController.getUsers(pageable);
        assertTrue(page.getNumberOfElements() == 1);
    }

    private void initGetAllUsersRules() {
        Page<UserProfile> page = initPage();
        when(userService.getAllUsers(any(Pageable.class))).thenReturn(page);
    }

    private Page<UserProfile> initPage() {
        PageRequest pageRequest = new PageRequest(0, 10);
        PageImpl<UserProfile> page = new PageImpl<>(getUsersList(), pageRequest, 1);
        return page;
    }

    private List<UserProfile> getUsersList() {
        UserProfile userProfile = new UserProfile();
        List<UserProfile> userProfiles = new ArrayList<>();
        userProfiles.add(userProfile);
        return userProfiles;
    }
}

Идея заключается в использовании чистого контроллера bean и макета его членов. В этом примере мы издевались над объектом UserService.getUsers(), чтобы содержать пользователя, а затем проверяли, вернет ли контроллер правильное количество пользователей.

С помощью той же логики вы можете протестировать Сервис и другие уровни вашего приложения. В этом примере также используется Pattern-Service-Repository Pattern:)