Тестирование _ситуаций_, а не данных или поведения
От: andyag  
Дата: 11.10.14 19:50
Оценка:
Пусть есть некий сервис блоггинга — пользователи, блоги, посты, комментарии, френды, плюсадины, и т.д. Пусть у этого сервиса есть что-то отдалённо похожее на REST API.
Ставится задача — покрыть сервис функционально-интеграционными тестами: нужно как минимум проверить позитивные и негативные сценарии создания всех сущностей. Предлагается тестировать снаружи — непосредственно через REST API.

Рассмотрим тест "пользователь не может отредактировать чужой комментарий":

Дано:
1. Комментарий Comment1 от пользователя UserA
2. UserB, который не UserA

Действие: Попробовать отредактировать Comment1 от имени UserB.

Результат: Нельзя.

Есть объективная проблема: часть "Дано" внешне выглядит просто, но технически требует подготовки очень объёмного контекста:
1. Завести юзера — UserA
2. Завести блог
3. Написать в блог — пост
4. К этому посту написать комментарий Comment1
5. Завести другого юзера — UserB

Сильно похожая ситуация будет ещё для кучи сценариев: "пользователь не может плюсануть свой комментарий", "пользователь не может плюсануть комментарий дважды", и т.д.
Меня мучает вопрос — как бороться со сложностью подготовки контекста для всех этих разных случаев.

Вариант 1 — тупо копипастить вызовы уровня CreateBlog, CreatePost, CreateComment из теста в тест. Плохо, потому что тесты будут длинными, а если добавится какой-то новый обязательных шаг между CreateBlog и CreatePost, потребуется вставлять это в куче мест.

Вариант 2 — объединять вызовы нескольких базовых методов в отдельные методы типа CreateBlogWithPostAndComment. Проблема в том, что количество кода снова растёт, а "объединяющих методов" в итоге может стать очень много. В результате нужно будет следить не только за базовыми методами, но ещё и за их сочетаниями.

Вариант 3 — крутится в голове. Хочется описывать контекст декларативно и привязывать к нему тесты:
// описываю как сделать пользователя
class ThereIsAUser implements ContextSpec {
  public ThereIsAUser(EntityId entityId) {...}

  // ничего не нужно
  public ContextSpec[] specs() { return NoSpecs; }
  public void apply() { делаем юзера }
}

// описываю как сделать блог, автор уже есть
class ThereIsABlog implements ContextSpec {
  public ThereIsABlog(EntityId entityId) {...}

  // нужен владелец
  public ContextSpec[] specs() { return []{ new ThereIsAUser("anyUser") }; }
  public void apply() { делаем блог }
}

// описываю как сделать пост, блог уже есть
class ThereIsAPost implements ContextSpec {
  public ThereIsAPost(EntityId entityId) {...}

  // нужен блог
  public ContextSpec[] specs() { return []{ new ThereIsABlog("anyBlog") }; }
  public void apply() { делаем пост }
}
...

Далее, тесты:
...
void canEditPost(@InjectPost("anyPost") Post post) {
  post.text = "free porn";
  Post updatedPost = post.save();
  assertEquals("free porn", updatedPost.text);
}
...

Вижу вот такие плюсы:
1. Спецификации — сами по себе тесты. В явном виде описывают каким должен быть "новый юзер" (ни одного блога, ни одного комментария, и т.д.), "новый блог" (ни одного поста), и т.д.
2. При написании тестов не нужно будет в 20 строчек настраивать предусловия, либо писать убер-библиотеку, где есть 200 методов настройки предусловий на любой вкус и цвет.
3. Появляется возможность построить граф зависимостей между спецификациями и тестами и прогонять тесты в оптимальной порядке, не грохая весь контекст перед каждым тестом. Например, для проверки редактирования комментария не важно, если в базе 1 блог или 10 — главное, чтобы комментарий был.


Чем плоха идея? Может изобрели уже?
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.