Оригинальная публикация Автор: Александр Пушкарев
Давайте представим себе гипотетическую ситауацию (в которую мы регулярно, вляпываемся). Вас назначили на проект «запилить» автоматизацию. Вам дают огромный тест план с большим количеством (тысячи их!) «ручных» тестов, и говорят что надо что-то сделать, и вотпрямщас. А еще, чтоб быстро и стабильно.
Писать Unit тесты, или даже думать о TDD — уже поздно, код продукта давным-давно написан. Ваше слово, товарищ автотестер!
К счастью, есть небольшой трюк, который позволит и coverage повысить, и сделать тесты стабильными и быстрыми — Subcutaneous tests («подкожные тесты»), но обо всем по порядку.
Суть проблемыПервый условный рефлекс автоматизатора — это взять Selenium (ну, или там, Selenide, или еще какую вундервафлю для UI тестов). Это такой стандарт индустрии, но есть много причин, почему «не взлетит»:
- UI-тесты медленные. От этого никуда не деться. Их можно запускать параллельно, допиливать напильником и делать чуть-чуть быстрее, но они останутся медленными.
- UI-тесты нестабильные. Отчасти потому, что они медленные. А еще потому, что Web-браузер и интерфейс пользователя не были созданы для того, чтобы ими управлял компьютер (в настоящее время данный тренд меняется, но не факт, что это хорошо).
- UI-тесты — это наиболее сложные тесты в написании и поддержки. Они просто тестируют слишком много. (Это усиливается тем фактом, что, зачастую, люди берут «ручные» тест-кейсы и начинают их автоматизировать как есть, без учета разницы в ручном и автоматическом тестировании).
- Нам говорят, что, якобы, UI-тесты эмулируют реального пользователя. Это не так. Пользователь не будет искать элемент на странице по ID или XPath локатору. Пользователь не заполняет форму со скоростью света, и не «упадет» если какой-то элемент страницы не будет доступен в какую-то конкретную миллисекунду. И даже теперь, когда браузеры разрабатываются с учетом того, что браузером можно программно управлять — это всего-лишь эмуляция, даже если очень хорошая.
- Кто-то скажет, что некоторый функционал просто нельзя протестировать иначе. Я скажу, что если есть функционал, который можно протестировать только UI тестами (за исключением самой UI логики) — это может быть хорошим признаком архитектурных проблем в продукте.
Единственный реальный плюс UI-тестов заключается в том, что они позволяют «накидать» более-менее полезные проверки без необходимости погружения и изучения кода самого продукта. Что вряд ли плюс в долгосрочной перспективе. Более подробно с объяснением, почему так, можно услышать в этой презентации.
Альтернативное решениеВ качестве очень простого случая, давайте рассмотрим приложение, состоящее из формы, куда можно ввести валидное имя пользователя. Если вы ввели имя пользователя, соответствующее правилам — User будет создан в системе и записан в Базу Данных.
Исходный код приложения можно найти здесь: github.com/senpay/login-form. Вы были предупрежденны — в приложении куча багов и нет модных тулов и фреймворков.
Если попробовать «накидать» чек лист для данного приложения, можно получить что-то вроде:
Выглядит просто? Просто! Можно ли написать UI-тесты? Можно. Пример написанных тестов (вместе с полноценным трехуровневым фреймворком) можно найти в LoginFormTest.java если перейти на uitests метку в git (git checkout uitests):
public class LoginFormTest {
SelenideMainPage sut = SelenideMainPage.INSTANCE;
private static final String APPLICATION_URL = "http://localhost:4567/index";
@BeforeClass
public static void setUpClass() {
final String[] args = {};
Main.main(args);
Configuration.browser = "firefox";
}
@Before
public void setUp() {
open(APPLICATION_URL);
}
@After
public void tearDown() {
close();
}
@Test
public void shouldBeAbleToAddNewUser() {
sut.setUserName("MyCoolNewUser");
sut.clickSubmit();
Assert.assertEquals("Status: user MyCoolNewUser was created", sut.getStatus());
Assert.assertTrue(sut.getUsers().contains("Name: MyCoolNewUser"));
}
@Test
public void shouldNotBeAbleToAddEmptyUseName() {
final int numberOfUsersBeforeTheTest = sut.getUsers().size();
sut.clickSubmit();
Assert.assertEquals("Status: Login cannot be empty", sut.getStatus());
Assert.assertEquals(numberOfUsersBeforeTheTest, sut.getUsers().size());
}
}
Немного метрик для данного кода:
Время выполнения: ~12 секунд (12 секунд 956 миллисекунд в последний раз, когда я запускал эти тесты)
Покрытие кода
Class: 100%
Method: 93.8% (30/32)
Line: 97.4% (75/77)
Теперь давайте предположим, что Функциональные автотесты могут быть написаны на уровне «сразу под» UI. Эта техника и называется Subcutaneous tests («подкожные тесты» — тесты, которые тестируют сразу под уровнем логики отображения) и была предложена Мартином Фаулером достаточно давно [1].
Когда люди думают о «не UI» автотестах, зачастую они думают сразу о REST/SOAP или иже с ним API. Но API (Application Programming Interface) — куда более широкое понятие, не обязательно затрагивающее HTTP и другие тяжеловесные протоколы.
Если мы поковыряем код продукта, мы можем найти кое-что интересненькое:
public class UserApplication {
private static IUserRepository repository = new InMemoryUserRepository();
private static UserService service = new UserService(); {
service.setUserRepository(repository);
}
public Map<String, Object> getUsersList() {
return getUsersList("N/A");
}
public Map<String, Object> addUser(final String username) {
final String status = service.addUser(username);
final Map<String, Object> model = getUsersList(status);
return model;
}
private Map<String, Object> getUsersList(String status) {
final Map<String, Object> model = new HashMap<>();
model.put("status", status);
model.put("users", service.getUserInfoList());
return model;
}
}
Когда мы кликаем что-то на UI — вызывается один из этим методов, или добавляется новый объект User, или возвращается список уже созданных объектов User. Что, если мы используем эти методы напрямую? Ведь это самый настоящий API! И самое главное, что REST и иные API тоже работают по тому же принципу — вызывают некий метод «уровня контроллера».
Используя напрямую эти методы, мы можем написать тест попроще да получше:
public class UserApplicationTest {
private UserApplication sut;
@Before
public void setUp() {
sut = new UserApplication();
}
@Test
public void shouldBeAbleToAddNewUser() {
final Map<String, Object> myCoolNewUser = sut.addUser("MyCoolNewUser");
Assert.assertEquals("user MyCoolNewUser was created", myCoolNewUser.get("status"));
Assert.assertTrue(((List) myCoolNewUser.get("users")).contains("Name: MyCoolNewUser"));
}
@Test
public void shouldNotBeAbleToAddEmptyUseName() {
final Map<String, Object> usersBeforeTest = sut.getUsersList();
final int numberOfUsersBeforeTheTest = ((List) usersBeforeTest.get("users")).size();
final Map<String, Object> myCoolNewUser = sut.addUser("");
Assert.assertEquals("Login cannot be empty", myCoolNewUser.get("status"));
Assert.assertEquals(numberOfUsersBeforeTheTest, ((List) myCoolNewUser.get("users")).size());
}
}
Этот код доступен по метке subctests:
git checkout subctests
Попробуем собрать метрики?
Time to execute: ~21 milliseconds
Покрытие кода:
Class: 77.8%
Method: 78.1 (30/32)
Line: 78.7 (75/77)
Мы потеряли немного покрытия, но скорость тестов выросла в 600 раз!!! Насколько важна\существенна потеря покрытия в данном случае? Зависит от ситуации. Мы потеряли немного glue code, который может быть (а может и не быть) важным (рекомендую в качестве упражнения определить, какой код потерялся).
Оправдывает ли данная потеря покрытия введения тяжеловесного тестирования на уровне UI? Это тоже зависит от ситуации. Мы можем, например:
- Добавить один UI-тест для проверки glue code, или
- Если мы не ожидаем частых изменений glue code — оставить его без автотестов, или
- Если у нас есть какой-то объем «ручного» тестирования — есть отличный шанс, что проблемы с glue code будут замечены тестировщиком, или
- Придумать что-то еще (тот же Canary deployment)
В итоге- Функциональные автотесты не обязательно писать на UI or REST/SOAP API уровне. Применение «Подкожных тестов» во многих ситуациях позволит протестировать тот же функционал с бОльшей скоростью и стабильностью
- Один из минусов подхода — определенная потеря покрытия
- Один из способов избежать потери покрытия — “Feature Tests Model”
- Но даже при условии потери покрытия, прирост скорости и стабильности — значителен.
Версия статьи на Английсом языке доступна здесь.
Если формат видео для вас больше подходит — можно посмотреть презентацию:
Обсудить в форуме |