Как правильно (не) использовать тестировщиков
Как быть, когда вокруг вроде бы девопсы, аджайлы и скрамы, но разработка и тестирование по-прежнему не живут в одном пайплайне душа в душу?
Из-за того, что необходимо преодолевать эту стену и находить общий язык, мы даже создали конференцию Heisenbug, предназначенную одновременно для тестировщиков и разработчиков. А ещё мы проводим Java-конференции, и осенью Артём Ерошенко выступил там с докладом «Как правильно (не) использовать тестировщиков». На примере Java-проекта он поделился своей болью и рассказал, что считает нужным делать.
И теперь, в преддверии нового Heisenbug и нового JPoint (обе конференции пройдут в формате «офлайн + онлайн»), мы решили сделать хабрапост на основе его доклада. Дальше повествование от имени Артёма.
Откуда у меня такой опыт: я один из сооснователей компании Qameta Software, мы разрабатываем инструменты тестирования Allure Report, Allure TestOps. Также я занимаюсь обучением автоматизации тестирования и консалтингом в области автоматизации. Я повидал много команд и в докладе хочу поделиться этим опытом. Если у вас другой опыт, обязательно расскажите о нем в комментариях к посту.
Цель моего консалтинга не просто построить много автотестов, которые будут находиться в отдельном репозитории, а в том, чтобы это всё вместе связать, чтобы автотесты приносили пользу, чтобы каждый участник процесса, пользуясь автотестами, понимал, зачем они нам нужны.
Что не так с тестированием в командах
Представим некую стандартную команду (персонажи вымышлены, совпадения случайны). Конечно же, они все проповедуют DevOps. Почему?
Есть поставка продукта клиенту. При ошибке весь пайплайн ломается и клиенты переживают. Раньше поставку пытался чинить разработчик, но он не знал тонкостей деплоя. И вообще разработчики, которые могут самостоятельно всё починить — это суперзвезды, таких мало и они дорого обходятся. Если же поставку чинит админ, то это тоже проблема, потому что он не знает нюансов кода (либо это уже админ-суперзвезда).
Самый понятный способ устранить проблему — это когда поставку чинит команда:
Каждый вносит свою лепту и продукт постоянно эволюционирует через жизненный цикл. В итоге команда действует следующим образом:
- Возникла проблема.
- Разработчики улучшают продукт, дописывают код, чтобы всё работало стабильнее.
- Админы настраивают метрики.
- QA добавляют и настраивают новые тесты.
И с помощью таких постоянных итераций у нас улучшается пайплайн, ошибки реже попадают в прод. То есть каждая роль вносит свой вклад.
Так ли это на самом деле?
Конечно, не так. В книжках, может, и написано, что всё должно работать именно так, но в реальности происходит по-другому. Я на своем опыте вижу, что QA практически всегда находятся отдельно от остальной команды. Разработчики и админы между собой договорились, научились выстраивать у себя действительно хорошие пайплайны, стабильно следят за деплоем. Часто в командах, куда я прихожу, вопрос поставки в продакшен уже даже не стоит. Всё всегда автоматизировано, двигается очень четко. Но команда тестирования почему-то всегда находится сбоку. То есть картина такая: всё хорошо, идет сборка, запускаются тесты, появляется тестовое окружение, потом — черное окно, там что-то делают тестировщики, потом они говорят «вроде всё протестировали», и опять начинается какая-то автоматика. Из-за этого у меня ощущение, что QA находится отдельно от команды.
Рассмотрим пример поставки:
Если всё работает и локально, и у клиента, тогда всё хорошо. Если обнаружена ошибка на стороне разработчика, то он сразу ее чинит, в продукт ошибка не попадет. А если наоборот, в продукте есть ошибка, но мы о ней не знаем, а клиент к нам приходит с этой ошибкой, то мы пишем тесты и так далее. То есть в принципе всё работает хорошо. Но если ошибка не обнаружена (такое бывает, я уверен, что в каждом софте есть много ошибок, которые до сих пор еще не выявлены), то в целом вроде бы и так сойдет.
Как шутят, «Буква Т в DevOps — это «тестирование». Мы не знаем обо всех ошибках, может быть, они есть, может, их нет. Спросите сами себя: вот вы постоянно катите новые версии продуктов, вы понимаете, какие у вас есть автотесты со стороны тестирования? Что проверяет тестирование? Скорее всего, вы ответите, что нет, вы отдаете это на откуп тестировщикам и об этом не думаете. Ну и буква Т из шутки ровно это и означает — что вам особо не интересно, что там ребята делают, и так сойдет.
Как итог, я вижу, что появляется замкнутая экосистема: есть тестировщики, которые играют друг с другом в какую-то свою «тестировщицкую» игру, и есть отдельная команда админов и разработчиков, которые друг с другом договорились и отлично взаимодействуют. При этом можно точно сказать, что внутри QA всё хорошо: когда я прихожу в конкретную команду, я вижу, что у них неплохо выстроены процессы, просто эти процессы очень редко учитывают, что вокруг есть какая-то разработка. Часто продукт просто попадает в тестовое окружение, тестировщики говорят, что им нужно несколько дней, чтобы всё протестировать, и на этом всё. Меня это сильно печалит.
Как будем с этим разбираться
- Напишем автотесты: я почувствую себя разработчиком и покажу, с какими проблемами можно столкнуться.
- Сравним подходы: посмотрим на автотесты под разными углами.
- Соберем идеальную команду: по моему мнению, она существует.
Пишем автотесты
Представим, что нам пришла какая-то задача на разработку, сделать самую простую фичу — написать TodoList для Java-приложения. Для чистоты эксперимента я всё сделал самостоятельно: сгенерировал себе шаблон на JHipster, потому что мне было интересно, как пишется код, и добавил туда всё необходимое. Сейчас расскажу, как я вижу связь разработки и тестов.
Разработка и тесты
Формально разработчики не пишут тесты, они пишут фичи и пишут код, который описывает поведение фичи. И этот код случайно называется тестами. То есть писать тесты — не их основная задача.
Итак, если мы начнем писать этот код, то для начала мы сделаем обычную тудушку и todo-репозиторий.
Создаем класс Todo
@Entity
public class Todo implements Serializable {
@Id
@GeneratedValue(strategy = ..., generator = ...)
@SequenceGenerator(name = "sequenceGenerator")
private Long id;
@NonNull
@Size(max = 255)
public String title;
}
Может, нужно добавить еще какие-то поля, но пока нам точно нужен id, title 255, помечаем всё аннотацией Entity
, Serializable
— и поехали дальше.
Создаем репозиторий
Создаем репозиторий, помечаем его аннотацией @Repository
, прописываем туда несколько методов.
@Repository
public interface TodoRepository
extends JpaRepository {
Long findAllByUser(User user);
Optional findFirstByUser(User user);
// какие-то дополнительные запросы в базу
}
У меня нет задачи рассказать о каких-то тонкостях и сложностях, поэтому показываю просто на пальцах. Тут могут быть какие-то дополнительные запросы, но мне для начала этого достаточно.
Пишем тест, помечаем его аннотацией @DataJpaTest
, подключаем репозитории userRepository
(достался нам в наследство), todoRepository
.
@DataJpaTest(includeFilters = ...)
public class TodoRepositoryTest {
@Autowired
private UserRepository userRepository;
@Autowired
private TodoRepository todoRepository;
@Test
public void shouldSaveTodo() { ... }
}
Этого нам достаточно, чтобы описать, что будет происходить в нашем тесте.
Создаем пользователя, тудушку, проверяем результат
@DataJpaTest(includeFilters = ...)
public class TodoRepositoryTest {
@Test
void shouldSaveTodo() {
User user = userRepository.saveAndFlush(randomUser());
Todo todo = randomTodo((todo) -> todo.setUser(user));
todoRepository.saveAndFlush(todo);
List(todoList).hasSize(1)
.extracting(Todo::getTitle)
.contains(todo.getTitle);
}
}
Сохраняем нашего рандомного пользователя, создаем рандомную тудушку и говорим, что она будет от имени этого пользователя. Сохраняем эту связь и говорим Flush
. После этого мы делаем todoRepository
, берем список всех сущностей конкретного юзера и запускаем тест.
Тест находит первую ошибку (todoRepository.saveAndFlush(todo);
). Она заключается в том, что мы забыли добавить миграцию.
Добавляем миграцию
Мне для этих задач привычнее использовать XML. Вы можете делать это по-другому, но я использую Liquidbase с такой вот миграцией.
Добавляю его в общий список:
@DataJpaTest(includeFilters = ...)
public class TodoRepositoryTest {
@Test
void shouldSaveTodo() {
User user = userRepository.saveAndFlush(randomUser());
Todo todo = randomTodo((todo) -> todo.setUser(user));
todoRepository.saveAndFlush(todo);
List todoList = todoRepository.findAllByUser(user);
assertThat(todoList).hasSize(1)
.extracting(Todo::getTitle)
.contains(todo.getTitle);
}
}
Запускаю автотест, он проходит. Я радуюсь, на душе от этого очень хорошо.
Какие еще тесты нужны?
У вас могут быть специфические запросы в базу, которые не генерируют JPA. В этом случае вам надо запустить этот тест на разных версиях базы, на разных базах. Для этого вы, скорее всего, подключите какой-нибудь тест-контейнер. Сейчас это делается довольно просто. Также надо, наверное, проверить на максимальный тайтл. Потому что, может быть, кто-то поменяет вашу миграцию, поменяет размер ячейки, будет там не 256 символов, а 128, и у вас сразу же упадет тест. Так что лучше написать как минимум несколько тестов — проверить, что будет, если пользователь будет null, и другие подобные кейсы.
Делаем DTO и примитивный Mapper
Создаем класс TodoDTO
:
public class TodoDTO implements Serializable {
private Long id;
private Long userId;
private String title;
public Long getId() { ... };
public Long getUserId() { ... };
public String getTitle() { ... };
}
DTO будет выглядеть как-то так. Это совсем простая сущность, то, что мы будем потом отдавать по REST.
И делаем Mapper:
public class TodoMapper {
public TodoDTO daoToDto(final Todo todo) {
final TodoDto dto = new TodoDto();
dto.setId(todo.getId());
dto.setTitle(todo.getTitle());
dto.setUserId(todo.getUser().getId());
return dto;
}
public Todo dtoToDao(final TodoDTO dto) { ... }
}
Mapper в целом можно сгенерировать, но у меня была задача показать какую-то логику, как примерно выглядит Mapper, потому что потом его захочется протестировать.
Естественно, пишем тест.
@SpringTest
public class TodoMapperTest {
@Autowired
private TodoMapper todoMapper;
@Test
public void shouldConvertMap() {
Todo todo = randomTodo();
TodoDto dto = todoMapper.daoToDto(todo);
assertThat(dto)
.extracting(TodoDto::getTitle(), TodoDto::getUserId())
.constraints(todo.getTitle(), todo.getUser().getId());
}
}
Делаем TodoMapper
, добавляем зависимость от TodoMapper
и после этого генерируем рандомную тудушку. Сконвертируем ее, запустим наш тест и увидим, что у нас всё правильно работает. Мы создали юзера и потом получили userId
, который соответствует getUser().getId()
.
Какие еще тесты нужны?
В принципе, тут еще можно добавить еще какие-то тесты. Круто, что тесты у нас получаются очень короткие. В каждый момент времени можно проверить очень маленькую логику. Здесь можно придумать тесты на валидацию размера строки, что будет, если id или user будет null и т. д.
Создаем класс TodoService
для работы с тудушкой конкретного пользователя
@Service
@Transactional
public class TodoService {
public List findAllForCurrentUser() {
User user = getCurrentUser().orElseGet(...);
List todoList = repository.findAllByUser(user);
return todoList.stream()
.map(todoMapper::daoToDto)
.collect(Collectors.toList())
};
}
Сделаем TodoService
, напишем в нем единственный транзакционный метод. Внутри возьмем текущего пользователя или сделаем какое-либо другое условие (например, создание guest-пользователя или ошибка, что пользователь не авторизован). То есть просто представим, что либо у нас есть пользователь, либо это поведение нам пока не интересно. И дальше возьмем все тудушки текущего пользователя, Mapper, сконвертируем его в DTO и отдадим полным списком.
Пишем тест:
@DataJpaTest (includeFilters = ...)
public class TodoServiceTest {
@Autowirwed
private TodoService todoService;
@Autowirwed
private TodoRepository todoRepo;
@Test
void shouldFindTodos() { ... }
}
Для этого теста нам понадобится todoService
и todoRepo
.
Создаем тудушки:
@DataJpaTest(includeFilters = ...)
public class TodoServiceTest {
@Test
void shouldFindTodos() {
Todo firstTodo = todoRepo.saveAndFlush(randomTodo(firstUser));
Todo secondTodo = todoRepo.saveAndFlush(randomTodo(firstUser));
todoRepo.saveAndFlush(randomTodo(secondUser);
List todoList = todoService.findAllForCurrentUser();
assert.That(todoList).hasSize(2)
.extracting(TodoDTO::getTitle())
.contains(firstTodo.getTitle(), secondTodo.getTitle());
}
}
Делаем сначала firstTodo
saveAndFlush
, потом secondTodo
saveAndFlush
и создаем еще одну тудушку от второго пользователя. В итоге у нас получается две тудушки от первого пользователя и одна от второго пользователя. После этого найдем в сервисе все тудушки текущего пользователя, их должно быть две, и у них должны быть тайтлы, которые мы создали. Получается такой простенький тест.
Но если мы его запустим, у нас опять будет ошибка, потому что мы не авторизовались. Дело в findAllForCurrentUser()
— сейчас у нас нет текущего пользователя, потому что у нас нет контекста авторизации, а значит, нам надо его добавить.
Забыли авторизацию
@DataJpaTest(includeFilters = ...)
public class TodoServiceTest {
@Test
@WithMockUser("...")
void shouldFindTodos() {
User firstUser = randomUser((user) -> user.setLogin("..."));
Todo firstTodo = todoRepo.saveAndFlush(randomTodo(firstUser));
Todo secondTodo = todoRepo.saveAndFlush(randomTodo(firstUser));
todoRepo.saveAndFlush(randomTodo(secondUser);
...
List todoList = todoService.findAllForCurrentUser();
assert.That(todoList).hasSize(2)
.extracting(TodoDTO::getTitle())
.contains(firstTodo.getTitle(), secondTodo.getTitle());
}
}
Авторизация делается очень просто: пишем @WithMockUser("...")
, можно указать user name
и потом сделать user set login
с конкретным юзернеймом. Или можно просто сказать @WithMockUser("...")
, потом взять текущий юзернейм и создать в базе пользователя с таким юзернеймом. Тут как вам больше захочется.
В чем прелесть: чтобы победить авторизацию, мне надо добавить всего одну аннотацию. Но вы это всё и так знаете, это всё довольно просто.
Какие тесты еще нужны?
Как минимум надо проверить, что будет, если пользователь не авторизован. Это поведение надо просто как минимум заложить. Кидать какой-то тупой exception здесь не надо, лучше корректно обработать эту ситуацию. Иногда бывают сессионные пользователи, которые не авторизованы, но у которых есть какая-то сессия. То есть в нашей системе не существует такого пользователя, но может, это какая-то демка, где пользователь может зайти, что-то покликать, через какое-то время вернуться на страницу, и у него отобразятся его тудушки. Пользователю это понравится, и он решит зарегистрироваться.
Мы можем обработать кучу логик в этом сервисе, и на всё мы должны написать тесты. Так что думаю, что тут еще пятерка тестов легко добавится на разные сложные кейсы, которые могут возникнуть.
Делаем TodoController
для работы с сервисом по API
Это наш финальный этап. Создаем класс TodoController
:
@RestController
@RequestMapping("/api/todo")
public class TodoController {
private TodoService todoService;
@GetMapping("/")
public List findAll() {
return todoService.findAllByCurrentUser();
}
@PostMapping("/")
public List create(@RequestBody TodoDTO dto){ ... }
}
Делаем RequestMapping("/api/todo")
, добавляем todoService
и пишем вот такой очень простой код. findAllByCurrentUser()
или создание тудушки. В целом создание тудушки нам здесь не нужно. Мы можем его добавить, но сейчас и без него хорошо.
Пишем тест:
@AutoConfigureMockMvc
public class TodoControllerTest {
@MockBean
private TodoService todoService
@Autowired
private MockMvc mockMvc
@Test
void shouldGetAll() { ... }
}
Всё, что касается todoService
, мы протестировали в отдельном тесте. Сейчас мы мокаем todoService
и тестируем чисто связку контроллера и todoService
, потому что здесь может находиться какой-то дополнительный код. У меня тут вообще получается линейный код, но в реальности тут может находиться какой-то дополнительный код, может быть какая-то конвертация, дефолтные значения и т. д., так что всё равно надо написать тест.
Используем Mock
Мы будем мокать todoService
, и после напишем вот такой тест:
@AutoConfigureMockMvc
public class TodoControllerTest {
@Test
void shouldGetAll() {
TodoDTO firstTodo = randomTodoDTO();
TodoDTO secondTodo = randomTodoDTO();
when(todoService.findAllForCurrentUser())
.thenReturn(Arrays.asList(firstTodo, secondTodo));
mockMvc.perform(get("/api/todo")).andExpect(status().ok())
.andExpect(jsonPath("$.[*].title").value(hasItems(
firstTodo.getTitle(), secondTodo.getTitle()
)));
}
}
Создаем TodoDTO
, вторую TodoDTO
, потом говорим: если вызовется метод findAllForCurrentUser()
, тогда возвращай список вот этих DTO.
После этого делаем запрос по API, проверяем, что у нас status().ok()
, берем тайтлы (jsonPath
для всех сущностей в массиве) и проверяем, что они содержат значение firstTodo.getTitle()
и secondTodo.getTitle()
.
Получается тоже довольно простой тест, но интересно, что у нас опять нет какого-то контекста авторизации, у нас уже всё работает правильно, и тестом мы проверяем очень маленькую задачу.
Какие еще тесты нужны?
Здесь, как я уже и говорит, можно проверить всякие дефолтные значения, если они есть, пагинацию. Пагинация не используется в моем коде, но никто не будет писать List
, все будут использовать pageable
. Еще можно проверить конвертацию и роутинг, потому что бывает, что одна и та же ручка (хэндлер) в зависимости от разных параметров ведет на разные методы сервиса.
Ничего не написал, а уже получилось около 20 тестов
С точки зрения кода я не написал ничего, написал просто минимум кода, и получилось около 20 тестов. То есть на каждом этапе у меня было где-то пять тестов. Я показал, как примерно выглядит один из них, но за ним можно сразу увидеть еще штук 5 тестов. И получается, что на такую простецкую функциональность у нас уже находится 20 тестов. И мне, скажу честно, все эти 20 тестов было бы очень лень писать. В реальности их еще больше.
Типы тестов
Есть разные типы тестов.
Есть очень маленькие тесты (тест на Mapper, тест на репозиторий, на сервис и т. д).
Есть тесты среднего размера, медиум-тесты. Я специально не использую терминологию «unit-тесты», «интеграционные тесты», мне сейчас проще указать, что есть маленькие тесты, которые тестируют каждую конкретную функциональность, есть медиум-тесты, которые тестируют некоторую связность. И естественно, у нас нет ни одного большого теста. Я не написал ни одного теста, который проверяет всю функциональность целиком, потому что, если честно, пока я писал это код, я не почувствовал надобности в таком тесте. А зачем он мне нужен? Физически я могу его написать, но я каждый раз тестировал по чуть-чуть, и за счет этого мне кажется, что я покрыл все необходимые сценарии.
Тестирование и тесты
Теперь давайте посмотрим, как работают тестирование и тесты.
Если вы начнете погружаться в эту область, первое, что вы узнаете — это что есть автотесты и ручные тесты.
Второе: чтобы начать что-то тестировать, нам нужно тестовое окружение. Нужно попросить админа сделать нам его, чтобы фича, которую мы хотим протестировать, как-то доставилась в это окружение.
Дальше в ход идет ручное тестирование либо аналитик-тестирование. Когда я говорю «ручное тестирование», я не имею в виду людей, которые сидят и нажимают кнопочки. Это скорее «аналитик-тестирование», давайте пока назовем это так. С помощью Swagger проверяем, всё ли работает.
Довольно частая ситуация, когда выкатывается новая функциональность, она либо сразу же не соответствует требованиям, либо в Swagger она описана как-то по-другому. Я понимаю, что у всех Swagger автоматически генерируется, но поверьте, со стороны тестирования это постоянно выглядит как какая-то неразбериха. Поэтому мы:
- Берем Swagger и смотрим список запросов.
- В своей песочнице для ручных тестов Postman проверяем, что всё работает.
- Делаем дымовые тесты (smoke tests), чтобы проверить работоспособность.
После этого идет стадия сохранения тест-кейсов для будущей автоматизации. Нам надо всё оформить в виде тестовой документации. Тестовая документация нужна в проекте, чтобы понимать, что и как у нас тестируется с точки зрения функциональности, с точки зрения пользователя, какие тестовые сценарии проверяем, а какие нет. Поэтому тестировщик идет в Test case Management System (TMS) и пишет вот такие тесты:
Тесты следующие:
- Авторизуемся под новым пользователем.
- Создаем Todo с Title «Купить молока».
- Проверяем, что в списке Todo пользователя есть задача «Купить молока».
Тесты очень формально описаны, на языке, к которому привыкли тестировщики. Тесты могут быть очень подробными или менее подробными, но тем не менее зачастую появляются тест-кейсы, в которых описано, что и как надо делать.
Тест-кейс в TMS
Тест-кейсов на новую функциональность получается много. В этот момент тестировщик как раз прорабатывает разные варианты: какую длину тайтла можно прописать, какие действия доступны от авторизованных и неавторизованных пользователей, можно ли поделиться списком с другими пользователями и так далее. Тестировщик описывает много разнообразных тест-кейсов и добавляет их в TMS.
Дальше мы выбираем тест-кейсы на автоматизацию. Тут начинается работа ручного тестировщика и автоматизатора. Они составляют матрицу, в которой находятся два приоритета, критичность и сложность автоматизации. Тестировщики расставляют в тест-кейсе свои критичные тесты, а автоматизатор пишет, просто это или сложно. Получается такая матрица Эйзенхауэра:
И мы выбираем только критичные тесты, которые просто автоматизировать.
Если у нас много сил, мы можем автоматизировать все тесты. Но зачастую мы выбираем конкретные тесты. Это сделано для того, чтобы мы не распылялись. Например, если мы не умеем тестировать PDF, то мы скорее всего сначала возьмем другие тесты, пока не научимся тестировать PDF.
То есть в основном мы пытаемся выбирать критичные тесты, которые мы уже умеем писать. Как только они заканчиваются, мы берем все остальные тесты и тоже их автоматизируем.
Автоматизатор автоматизирует тест-кейсы
Сначала автоматизатор создает пользователя, потому что если наши автотесты запустить в параллель, они будут конфликтовать. Просто запустится тест, один создаст тудушки, второй зачитает тудушки и увидит, что, внезапно, там не две тудушки, а три. Это и будет первой проблемой, поэтому начнем с создания пользователей.
public class TodoTest {
@Test
void testTodoList() {
RequestSpecification base = given()
.baseUri("https://testing.company.com")
.contentType("application/json");
UserDTO user = randomUser();
base.body(user).post("/api/user")
.then().statusCode(200);
...
}
}
Мы идем к разработчикам и просим их сделать нам ручки, которые будут создавать пользователя. Проблема авторизации довольно распространенная. Часто бывает, что когда ты создаешь пользователя, тебе надо прочитать какое-нибудь СМС или какой-то код. Так что обычно это первый камень преткновения. В такие моменты мы обычно приходим к разработке, вместе думаем, как это можно замокать, чтобы код приходил нам в конкретный модуль, откуда мы его сразу же можем собрать, иначе нам не создать пользователя. Поэтому много команд сидят под статическими пользователями, и у них с этим большая проблема. В нашем примере мы тоже сразу же утыкаемся в эту проблему, но решаем ее.
Получаем токен:
public class TodoTest {
@Test
void testTodoList() {
...
Map authData = Maps.of(
"username", user.getUsername(),
"password", user.getPassword(),
)
String token = base.body(authData)
.post("/api/authenticate")
.path("id_token").toString();
}
}
Вводим логин/пароль, отправляем запрос на /api/authenticate
, получаем id_token
и начинаем выполнять реальную авторизацию.
public class TodoTest {
@Test
void testTodoList() {
...
String token = base.body(authData)
.post("/api/authenticate")
.path("id_token")
.toString();
RequestSpecification authorized = base
.header("Authorization", "Bearer" + token)
}
}
После этого мы авторизуемся: получаем токен, выставляем authorization-заголовок и проставляем его вот таким образом:
public class TodoTest {
@Test
void testTodoList() {
...
TodoDTO firstTodo = randomTodo();
TodoDTO secondTodo = randomTodo();
authorized.body(firstTodo).post("/api/todo")
.then().statusCode(200);
authorized.body(secondTodo).post("/api/todo")
.then().statusCode(200);
}
}
После этого мы создаем тудушки. К сожалению, мы не можем сразу же в базу записать какие-то данные, как это делали разработчики. У них там и с авторизацией всё просто — ставишь аннотацию, и ты авторизован. У нас же это всё через реальные данные. Пишем randomTodo()
на первую и на вторую тудушку — и тут, к сожалению, разработчик не предусмотрел создание списка тудушек, поэтому мы каждый раз создаем по одной тудушке. Пострадаем немного, но что поделать.
Проверяем Todo:
public test TodoTest {
@Test
void testTodoList() {
...
List todoList = authorized.get("/api/todo")
.body().as(new TypeRef>(){});
assertThat(todoList).hasSize(2)
.extracting(TodoDTO::getTitle)
.contains(firstTodo.getTitle(), secondTodo.getTitle());
}
}
И под конец мы получаем от авторизованного пользователя некоторый список тудушек, проверяем, что тудушек действительно две, и пишем самый простенький сценарий.
Проверяем, что автотест всё делает правильно
Дальше включается ручное тестирование, оно проверяет, что автотест всё делает правильно. Для этого мы строим Allure-отчет:
Тест, как вы видите, получается очень длинный: надо создать пользователя, авторизоваться под каким-то пользователем, создать первую тудушку, потом вторую и так далее. И из-за того, что тестовые окружения бывают нестабильными, некоторые ручки меняются, потому что мы не близки к разработке. У нас выкатилось обновление к окружению, там находится сразу десяток пулл-реквестов, и каждый из этих пулл-реквестов может что-то поменять. И чтобы нам быть в курсе дела, мы очень много всего логируем, проверяем и отдаем этот отчет ручным тестировщикам, чтобы они убедились, что в случае ошибки они смогут по этому отчету быстро разобраться и понять, что произошло.
И если посмотреть на наши разные типы тестов, то у нас наоборот:
В основном мы сразу же пишем большие тесты, тесты end-to-end, которые всё проверяют без каких-либо моков. Но моки тоже бывают, когда мы можем подменить куски сервисов, например, чтобы у нас отсылалась ненастоящая СМС, чтобы ее отправлял какой-нибудь мок-сервер, и мы могли ее оттуда забрать. Обо всём этом мы договариваемся с разработкой, и делается это не быстро. Ведь это нужно нам, а у разработчика почти никогда нет на это времени. Приоритеты общие, но приоритеты сервиса зачастую важнее наших, поэтому мы находимся в хвосте. В итоге средние тесты мы пишем в меньшем количестве, а маленькие тесты вообще не пишем, потому что мы физически не можем их написать, у нас не такая область видимости, мы не можем так близко подлезть к коду.
Сравниваем подходы
В принципе кажется, что тесты не сильно отличаются: тут написал тест на тудушки, тут написал тест через REST на тудушки. Конечно, в первом случае тесты поменьше, во втором случае тесты будто бы больше, ну и ладно.
Давайте посмотрим, чем же они отличаются.
Сравним время создания такого теста
Разработка и тесты: «Можно написать за 10 минут, на сложный тест иногда может уйти до двух часов».
Сложный тест — это, например, новые PDF, какая-то функциональность, которой раньше не было. Надо подключить какую-нибудь библиотеку, с ее помощью посмотреть, как генерируется PDF, убедиться, что там есть какой-то текст, и в принципе этого достаточно. В основном тесты пишутся очень быстро и просто.
Тестирование и тесты: «На один тест уходит минимум 2 часа, на самом деле обычно больше».
Это происходит из-за самого процесса: нам надо соблюсти все формальности. Сама процедура написания тестов довольно долгая. И это уже не говоря о том, что сам тест долгий, и когда ты его дебажишь, у разработчиков проходит, грубо говоря, 30 секунд, а у нас создается пользователь, авторизуется, создаются тудушки, упали мы на тудушках — уже прошло где-то полторы минуты, пока всё собралось и т. д. Перезапустили тест — надо опять полторы минуты ждать. Тесты бывают и длиннее. Соответственно, на каждый тест уходит 2 часа, просто потому что они сами по себе большие.
Проблема заключается в том, что сначала мы пишем ручные тесты, после этого расставляем приоритеты, потом мы тест автоматизируем, валидируем, и всё это занимает время.
Поговорим про тестируемость продукта
Разработка и тесты: «Если мне что-то неудобно в тестах, то я изменяю код продукта»
Например, если бы мне было неудобно писать эти тудушки, я бы сразу в сервисе написал метод, который создает список тудушек. И я бы на это практически ничего не потратил. Для меня это такая минорная функциональность, которая занимает несколько минут. Поэтому как только разработчикам что-то неудобно, они подстраивают под тесты код продукта. Это повышает тестируемость, и многие разработчики предлагали писать больше тестов, чтобы код был красивее. И я с ними в принципе согласен.
Тестирование и тесты: «Я работаю с тем, что в тестовом окружении, прошу доработать что-то, когда совсем тяжело».
Тестировщики практически всегда работают с тем, что в тестовом окружении, куда очень редко доезжают какие-то доработки. Я могу рассказать вам кучу историй, в которых тестовое окружение тормозит. Команда тестирования работает с этим каждый день, окружение постоянно отваливается, все на это жалуются, в ответ говорят, что завтра появится новое тестовое окружение, новые ручки, но этого почему-то никогда не происходит.
Если у вас это не так, если у вас всё круто, вы слушаете свое тестирование, я считаю, что вы большие молодцы, я очень этому рад, продолжайте в том же духе. Но зачастую почему-то тестируемость продукта и тестирование как QA — разные вещи, и нам приходится есть кактус.
Запуск тестов
Разработка и тесты: «Все тесты запускаются на PR, если они упали, то фиксим».
Это круто, потому что даже думать не надо — тесты прошли, джоба зеленая, осталось пройти код-ревью, а так всё хорошо, код рабочий.
У разработчика получается такая картина: он создает PR, у него запускаются тесты, они чуть-чуть попадали, он их все озеленил, всё стало ок, можно делать код-ревью и в прод.
Тестирование и тесты: «Мы запускаем тесты после выкладки в тестовое окружение».
И это большая проблема, потому что когда запускается несколько пулл-реквестов, очень сложно угадать, какой пулл-реквест какую проблему нам принес.
Например, здесь у нас всё зелено, а на самом деле проблема в третьем PR. Но нам надо разобрать большое количество информации, собрать логи, поведение сервисов, чтобы точно указать, что ошибка здесь. Зачастую этого не происходит. Кроме того, ошибки, которые у нас находятся, постоянно фонят. Разработка их не правит моментально, она правит их на следующий день или еще позже. А тесты запускаются постоянно, так что постоянно с нами хвост падений от разработки. То есть известные проблемы постоянно присутствуют, и это раздражает.
Разбор падения тестов
Разработка и тесты: «Берешь StackTrace и вставляешь в IDE:)».
Можно вообще ни о чём не переживать. Открываете отчет, потом копипастите его в IDE, у вас сразу подсвечиваются тестовые методы:
Нажимаете прямо на метод и сразу разбираетесь, в чём проблема. Вы можете сразу его запустить, у вас запустится код продукта, вы можете пофиксить этот тест, убедиться, что он стал работать, и пойти работать дальше.
Тестирование и тесты: «Мы пишем много логов и строим подробные отчеты».
Очень часто причина падения тестов даже не всегда в продукте. Тормозит тестовое окружение, не работает Selenium, прокси загнулась, физический мок-сервер, который поднят в докер-контейнере, начал тормозить под нагрузкой, и прочее. Определять причины падения тестов для нас сложная штука.
Например, мы запустили 100 автотестов, падает 5% — повторюсь, что это в принципе более или менее нормально, потому что у нас постоянно есть тесты, которые падают из-за найденных ошибок. Это нормально, потому что тесты запускаются не на PR. Ну и получается в итоге 25 минут времени. А вот если у нас 1000 тестов и упало 5%, то 5 минут на одну проблему — это 250 минут, уже 4 часа времени.
И это сильно раздражает, особенно если еще и тормозит тестовое окружение, всё это превращается в большую проблему.
Мы строим Allure-отчет и анализируем тренды. Кстати, на этом отчете как раз видно, как у нас строится тренд, что тесты падают совсем по чуть-чуть:
Success rate наших тестов 99,14%, и это довольно круто.
Еще мы постоянно следим за трендами, за временем выполнения тестов, потому что, еще раз повторюсь, тесты зависят от разных источников, и надо постоянно держать себя в тонусе.
Анализ покрытия
Разработка и тесты: «Мы используем метрику «покрытие по строчкам кода», запускаем на каждый PR».
Еще разработчики добавляют Codecov Report в пулл-реквест:
На мой взгляд, это правильная практика. Если ты написал код и не написал тесты вооб