«1. Обзор

Мы уже некоторое время разрабатываем REST API нашего простого приложения Reddit — пришло время серьезно отнестись к нему и начать его тестирование.

И теперь, когда мы, наконец, перешли на более простой механизм аутентификации, сделать это стало еще проще. Мы собираемся использовать мощную надежную библиотеку для всех этих живых тестов.

2. Первоначальная настройка

Тесты API должны запускаться пользователем; чтобы упростить выполнение тестов API, мы создадим тестового пользователя заранее — при начальной загрузке приложения: в фактической логике настройки.

@Component
public class Setup {
    @Autowired
    private UserRepository userRepository;

    @Autowired
    private PreferenceRepository preferenceRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @PostConstruct
    private void createTestUser() {
        User userJohn = userRepository.findByUsername("john");
        if (userJohn == null) {
            userJohn = new User();
            userJohn.setUsername("john");
            userJohn.setPassword(passwordEncoder.encode("123"));
            userJohn.setAccessToken("token");
            userRepository.save(userJohn);
            final Preference pref = new Preference();
            pref.setTimezone(TimeZone.getDefault().getID());
            pref.setEmail("[email protected]");
            preferenceRepository.save(pref);
            userJohn.setPreference(pref);
            userRepository.save(userJohn);
        }
    }
}

3. Поддержка живых тестов

Прежде чем мы начнем писать наши тесты, давайте сначала настроим некоторые основные вспомогательные функции, которые мы затем сможем использовать.

Нам нужны такие вещи, как аутентификация, URL-пути и, возможно, некоторые возможности сортировки и десортировки JSON:


Мы просто определяем несколько простых вспомогательных методов и полей, чтобы упростить реальное тестирование:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
  classes = { TestConfig.class }, 
  loader = AnnotationConfigContextLoader.class)
public class AbstractLiveTest {
    public static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");

    @Autowired
    private CommonPaths commonPaths;

    protected String urlPrefix;

    protected ObjectMapper objectMapper = new ObjectMapper().setDateFormat(dateFormat);

    @Before
    public void setup() {
        urlPrefix = commonPaths.getServerRoot();
    }
    
    protected RequestSpecification givenAuth() {
        FormAuthConfig formConfig 
          = new FormAuthConfig(urlPrefix + "/j_spring_security_check", "username", "password");
        return RestAssured.given().auth().form("john", "123", formConfig);
    }

    protected RequestSpecification withRequestBody(RequestSpecification req, Object obj) 
      throws JsonProcessingException {
        return req.contentType(MediaType.APPLICATION_JSON_VALUE)
          .body(objectMapper.writeValueAsString(obj));
    }
}

GivenAuth() : для выполнения аутентификации с помощьюRequestBody(): для отправки JSON-представления объекта в качестве тела HTTP-запроса

    А вот и наш простой bean-компонент — CommonPaths — обеспечивающий чистую абстракцию URL-адресов системы :

И локальная версия файла свойств: web-local.properties:

@Component
@PropertySource({ "classpath:web-${envTarget:local}.properties" })
public class CommonPaths {

    @Value("${http.protocol}")
    private String protocol;

    @Value("${http.port}")
    private String port;

    @Value("${http.host}")
    private String host;

    @Value("${http.address}")
    private String address;

    public String getServerRoot() {
        if (port.equals("80")) {
            return protocol + "://" + host + "/" + address;
        }
        return protocol + "://" + host + ":" + port + "/" + address;
    }
}

Наконец, очень простая тестовая конфигурация Spring:

http.protocol=http
http.port=8080
http.host=localhost
http.address=reddit-scheduler

4. Протестируйте API /scheduledPosts

@Configuration
@ComponentScan({ "org.baeldung.web.live" })
public class TestConfig {
    @Bean
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }
}

Первый API, который мы собираемся протестировать, — это /scheduledPosts API:

Во-первых, давайте проверим планирование новой публикации:

public class ScheduledPostLiveTest extends AbstractLiveTest {
    private static final String date = "2016-01-01 00:00";

    private Post createPost() throws ParseException, IOException {
        Post post = new Post();
        post.setTitle("test");
        post.setUrl("test.com");
        post.setSubreddit("test");
        post.setSubmissionDate(dateFormat.parse(date));

        Response response = withRequestBody(givenAuth(), post)
          .post(urlPrefix + "/api/scheduledPosts?date=" + date);
        
        return objectMapper.reader().forType(Post.class).readValue(response.asString());
    }
}

Затем давайте проверим получение всех запланированных публикаций пользователя :

@Test
public void whenScheduleANewPost_thenCreated() 
  throws ParseException, IOException {
    Post post = new Post();
    post.setTitle("test");
    post.setUrl("test.com");
    post.setSubreddit("test");
    post.setSubmissionDate(dateFormat.parse(date));

    Response response = withRequestBody(givenAuth(), post)
      .post(urlPrefix + "/api/scheduledPosts?date=" + date);

    assertEquals(201, response.statusCode());
    Post result = objectMapper.reader().forType(Post.class).readValue(response.asString());
    assertEquals(result.getUrl(), post.getUrl());
}

Далее давайте проверим редактирование запланированного сообщения:

@Test
public void whenGettingUserScheduledPosts_thenCorrect() 
  throws ParseException, IOException {
    createPost();

    Response response = givenAuth().get(urlPrefix + "/api/scheduledPosts?page=0");

    assertEquals(201, response.statusCode());
    assertTrue(response.as(List.class).size() > 0);
}

Наконец, давайте проверим операцию удаления в API:

@Test
public void whenUpdatingScheduledPost_thenUpdated() 
  throws ParseException, IOException {
    Post post = createPost();

    post.setTitle("new title");
    Response response = withRequestBody(givenAuth(), post).
      put(urlPrefix + "/api/scheduledPosts/" + post.getId() + "?date=" + date);

    assertEquals(200, response.statusCode());
    response = givenAuth().get(urlPrefix + "/api/scheduledPosts/" + post.getId());
    assertTrue(response.asString().contains(post.getTitle()));
}

5. Протестируем API /sites

@Test
public void whenDeletingScheduledPost_thenDeleted() 
  throws ParseException, IOException {
    Post post = createPost();
    Response response = givenAuth().delete(urlPrefix + "/api/scheduledPosts/" + post.getId());

    assertEquals(204, response.statusCode());
}

Далее — давайте протестируем API, публикующий ресурсы Сайтов — сайты, определенные пользователем:

Давайте проверим получение всех сайтов пользователя:

public class MySitesLiveTest extends AbstractLiveTest {

    private Site createSite() throws ParseException, IOException {
        Site site = new Site("/feed/");
        site.setName("baeldung");
        
        Response response = withRequestBody(givenAuth(), site)
          .post(urlPrefix + "/sites");

        return objectMapper.reader().forType(Site.class).readValue(response.asString());
    }
}

А также получение статей сайта: ~ ~~

@Test
public void whenGettingUserSites_thenCorrect() 
  throws ParseException, IOException {
    createSite();
    Response response = givenAuth().get(urlPrefix + "/sites");

    assertEquals(200, response.statusCode());
    assertTrue(response.as(List.class).size() > 0);
}

Далее давайте проверим добавление нового сайта:

@Test
public void whenGettingSiteArticles_thenCorrect() 
  throws ParseException, IOException {
    Site site = createSite();
    Response response = givenAuth().get(urlPrefix + "/sites/articles?id=" + site.getId());

    assertEquals(200, response.statusCode());
    assertTrue(response.as(List.class).size() > 0);
}

И его удаление:

@Test
public void whenAddingNewSite_thenCorrect() 
  throws ParseException, IOException {
    Site site = createSite();

    Response response = givenAuth().get(urlPrefix + "/sites");
    assertTrue(response.asString().contains(site.getUrl()));
}

6. Протестируем API /user/preferences

@Test
public void whenDeletingSite_thenDeleted() throws ParseException, IOException {
    Site site = createSite();
    Response response = givenAuth().delete(urlPrefix + "/sites/" + site.getId());

    assertEquals(204, response.statusCode());
}

Наконец, давайте сосредоточимся на API, раскрывающем предпочтения пользователя.

Во-первых, давайте проверим получение пользовательских настроек:

И их редактирование:

@Test
public void whenGettingPrefernce_thenCorrect() {
    Response response = givenAuth().get(urlPrefix + "/user/preference");

    assertEquals(200, response.statusCode());
    assertTrue(response.as(Preference.class).getEmail().contains("john"));
}

7. Заключение

@Test
public void whenUpdattingPrefernce_thenCorrect() 
  throws JsonProcessingException {
    Preference pref = givenAuth().get(urlPrefix + "/user/preference").as(Preference.class);
    pref.setEmail("[email protected]");
    Response response = withRequestBody(givenAuth(), pref).
      put(urlPrefix + "/user/preference/" + pref.getId());

    assertEquals(200, response.statusCode());
    response = givenAuth().get(urlPrefix + "/user/preference");
    assertEquals(response.as(Preference.class).getEmail(), pref.getEmail());
}

В этой быстрой статье мы провели базовое тестирование нашего REST API.

Ничего особенного — нужны более продвинутые сценарии — но дело не в совершенстве, а в прогрессе и публичном повторении.

«