Пусть есть некий сервис блоггинга — пользователи, блоги, посты, комментарии, френды, плюсадины, и т.д. Пусть у этого сервиса есть что-то отдалённо похожее на 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 — главное, чтобы комментарий был.
Чем плоха идея? Может изобрели уже?