Тестирование API приложения Reddit

Тестирование API приложения Reddit

1. обзор

Мы уже некоторое время занимаемся созданием REST APIour simple Reddit App - пора заняться серьезным иstart testing it.

И теперь, когда мыfinally switched перешли к более простому механизму аутентификации, это тоже стало проще. Мы собираемся использоватьthe powerful rest-assured library для всех этих живых тестов.

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

Обратите внимание, чтоSetup - это простой bean-компонент, и мы используем аннотацию@PostConstruct для подключения фактической логики настройки.

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

Прежде чем мы начнем писать наши тесты, мы можем использоватьlet’s first set up some basic supporting functionality.

Нам нужны такие вещи, как аутентификация, 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(): для аутентификации

  • withRequestBody(): для отправки JSON-представленияObject в качестве тела HTTP-запроса

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

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

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

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

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

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

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

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

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

Во-первых, давайте проверимscheduling a new post:

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

Затем давайте проверимretrieving all scheduled posts пользователя:

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

Затем давайте проверимediting a scheduled post:

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

Наконец, давайте протестируемthe delete operation в API:

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

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

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

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

public class MySitesLiveTest extends AbstractLiveTest {

    private Site createSite() throws ParseException, IOException {
        Site site = new Site("/feed/");
        site.setName("example");

        Response response = withRequestBody(givenAuth(), site)
          .post(urlPrefix + "/sites");

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

Давайте проверимretrieving all the sites пользователя:

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

Затем давайте проверимadding a new Site:

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

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

Иdeleting это:

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

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

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

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

Во-первых, давайте проверимgetting user’s preferences:

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

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

Иediting их:

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

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

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

Ничего особенного - нужны более сложные сценарии - ноthis isn’t about perfection, it’s about progress and iterating in public.