Пусть есть некий сервис блоггинга — пользователи, блоги, посты, комментарии, френды, плюсадины, и т.д. Пусть у этого сервиса есть что-то отдалённо похожее на 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 — главное, чтобы комментарий был.
Чем плоха идея? Может изобрели уже?
Re: Тестирование _ситуаций_, а не данных или поведения
Здравствуйте, Rinbe, Вы писали:
R>Здравствуйте, andyag, Вы писали:
A>>Чем плоха идея? Может изобрели уже?
R>Да уже изобрели и для этого язык свой есть, смотри Gherkin.
Вот только что JBehave щупал (я так понял, примерно то же самое) — какое-то страшное оно
Re: Тестирование _ситуаций_, а не данных или поведения
Здравствуйте, andyag, Вы писали:
A>Чем плоха идея? Может изобрели уже?
Вы на верном пути, осталось сделать последний шаг.
Если вы хотите использовать какое-то стороннее решение (вы используете какие-то спеки), то пункт 3 сойдет, но вряд ли долго протянет.
Если хотите узнать как решать подобные задачи, то ознакомьтесь с опытом людей, которые с ними сталкивались не одну пятилетку
Не удивляйтесь, что после знакомства с материалом у вас возникнет мысль (а хотя она должна быть у вас и сейчас), что большинство решений в этой области (кроме автоматического создания моков и каких-нибудь вебдрайверов) непригодны. Да, это правда. Тем более, что у вас что-то похожее на java, а для java это абсолютная правда.
Не удивляйтесь, что для написания самых качественных тестов для вашего приложения никакие фреймворки кроме мейнстримного тест раннера вам практически не понадобятся. Это тоже правда.
Re: Тестирование _ситуаций_, а не данных или поведения
Здравствуйте, andyag, Вы писали:
A>Чем плоха идея? Может изобрели уже?
Пункт третий неплох, но если писать самому, то кода будет не меньше чему во втором.
Я когда писал интеграционные тесты для своего сервиса (завести пользователя, завести проект для данного пользователя, добавить файлы
и т.д.), то пошел по второму варианту: абстрактные классы, интерфейсы и проч.
Кодом людям нужно помогать!
Re[3]: Тестирование _ситуаций_, а не данных или поведения
Здравствуйте, andyag, Вы писали:
a> Вот только что JBehave щупал (я так понял, примерно то же самое) — какое-то страшное оно
Оно не страшное, оно большое. Когда у тебя есть команда 200 человек, то приходится.
В каком-то роде это исполнимая документация — бизнес-аналитики и тестеры пишут сценарии в notepad, программисты пишут код, выполняющий эти сценарии.
Для небольшого проекта — из пушки по воробьям. Для такого лучше просто писать тупой-простой код повязанный каким-нибудь Guice, именно IoC-контейнеры и предназначены для управления контекстами.
Вначале тебе нужно написать java-обёртку (или любую другую, на чём тесты хочешь писать) для твоего REST API. Эту библиотеку можно и официально выложить, юзерам понравится.
Чтобы вдохновиться поищи что-нибудь похожее, например или другие.
Дальше просто. Пишем тупой тест:
class CommentEditTest
{
@Inject UserService userService;
@Inject BlogService blogService;
@Inject PostService postService;
@Inject CommentService commentService;
@Before
void setUp()
{
Guice.createInjector(new TestModule()).injectMembers(this);
}
@Test
void testNonOwnerEdit()
{
User userA = userService.create("a");
Blog blog = blogService.create(userA);
Post post = postService.create(blog, "hello, world!");
Comment comment = commentService.create(post, "Comment1");
User userB = userService.create("b");
expectedException.expect(AccessDeniedException.class);
expectedException.expectMessage("How dare you?!");
postService.editPost(userB, post, "");
}
}
Если вдруг выясняем, что вообще-то создание юзера, поста и коммента — частая операция, можно просто вынести в @Before (и для большинства тестов так и получится). Но рассмотрим более тяжелый случай, когда некоторый функционал часто повторяется среди разных классов тестов, что ж, рефакторим:
class UserWithComment
{
final User user;
final Blog blog;
final Post post;
final Comment comment;
@Inject public UserWithComment(
UserService userService,
BlogService blogService,
PostService postService,
CommentService commentService)
{
user = userService.create("a");
blog = blogService.create(userA);
post = postService.create(blog, "hello, world!");
comment = commentService.create(post, "Comment1");
}
}
class CommentEditTest
{
@Inject UserWithComment userWithComment;
@Inject UserService userService;
@Before
void setUp()
{
Guice.createInjector(new TestModule()).injectMembers(this);
}
@Test
void testNonOwnerEdit()
{
Post post = userWithComment.post;
User userB = userService.create("b");
expectedException.expect(AccessDeniedException.class);
expectedException.expectMessage("How dare you?!");
postService.editPost(userB, post, "");
}
}
Короче, код интеграционных тестов такой же обычный код как и всё остальное, и TDD так же применим.