Тестирование _ситуаций_, а не данных или поведения
От: 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 — главное, чтобы комментарий был.


Чем плоха идея? Может изобрели уже?
Re: Тестирование _ситуаций_, а не данных или поведения
От: Rinbe Россия  
Дата: 11.10.14 19:59
Оценка: 6 (1) +1
Здравствуйте, andyag, Вы писали:

A>Чем плоха идея? Может изобрели уже?


Да уже изобрели и для этого язык свой есть, смотри Gherkin.
Отредактировано 13.10.2014 11:49 kaa.python . Предыдущая версия .
Re[2]: Тестирование _ситуаций_, а не данных или поведения
От: andyag  
Дата: 11.10.14 20:20
Оценка:
Здравствуйте, Rinbe, Вы писали:

R>Здравствуйте, andyag, Вы писали:


A>>Чем плоха идея? Может изобрели уже?


R>Да уже изобрели и для этого язык свой есть, смотри Gherkin.


Вот только что JBehave щупал (я так понял, примерно то же самое) — какое-то страшное оно
Re: Тестирование _ситуаций_, а не данных или поведения
От: Joie de vivre  
Дата: 12.10.14 10:14
Оценка: 6 (1) +1
Здравствуйте, andyag, Вы писали:

A>Чем плоха идея? Может изобрели уже?


Вы на верном пути, осталось сделать последний шаг.

Если вы хотите использовать какое-то стороннее решение (вы используете какие-то спеки), то пункт 3 сойдет, но вряд ли долго протянет.
Если хотите узнать как решать подобные задачи, то ознакомьтесь с опытом людей, которые с ними сталкивались не одну пятилетку

http://www.amazon.com/Growing-Object-Oriented-Software-Guided-Tests/dp/0321503627

Не удивляйтесь, что после знакомства с материалом у вас возникнет мысль (а хотя она должна быть у вас и сейчас), что большинство решений в этой области (кроме автоматического создания моков и каких-нибудь вебдрайверов) непригодны. Да, это правда. Тем более, что у вас что-то похожее на java, а для java это абсолютная правда.

Не удивляйтесь, что для написания самых качественных тестов для вашего приложения никакие фреймворки кроме мейнстримного тест раннера вам практически не понадобятся. Это тоже правда.
Re: Тестирование _ситуаций_, а не данных или поведения
От: Sharov Россия  
Дата: 13.10.14 09:04
Оценка:
Здравствуйте, andyag, Вы писали:

A>Чем плоха идея? Может изобрели уже?


Пункт третий неплох, но если писать самому, то кода будет не меньше чему во втором.
Я когда писал интеграционные тесты для своего сервиса (завести пользователя, завести проект для данного пользователя, добавить файлы
и т.д.), то пошел по второму варианту: абстрактные классы, интерфейсы и проч.
Кодом людям нужно помогать!
Re[3]: Тестирование _ситуаций_, а не данных или поведения
От: . Великобритания  
Дата: 13.10.14 22:48
Оценка: 6 (1)
Здравствуйте, 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 так же применим.
avalon/1.0.432
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.