Давайте представим себе гипотетическую ситауацию (в которой мы регулярно оказываемся). Вас назначили на проект «запилить» автоматизацию. Вам дают огромный тест план с большим количеством (тысячи их!) «ручных» тестов, и говорят что надо что-то сделать, и вотпрямщас. А еще, чтоб быстро и стабильно.
Писать Unit тесты, или даже думать о TDD — уже поздно, код продукта давным-давно написан. Ваше слово, товарищ автотестер!

К счастью, есть небольшой трюк, который позволит и coverage повысить, и сделать тесты стабильными и быстрыми — Subcutaneous tests («подкожные тесты»), но обо всем по порядку.
Первый условный рефлекс автоматизатора — это взять Selenium (ну, или там, Selenide, или еще какую вундервафлю для UI тестов). Это такой стандарт индустрии, но есть много причин, почему «не взлетит»:
Единственный реальный плюс UI-тестов заключается в том, что они позволяют «накидать» более-менее полезные проверки без необходимости погружения и изучения кода самого продукта. Что вряд ли плюс в долгосрочной перспективе. Более подробно с объяснением, почему так, можно услышать в этой презентации.
В качестве очень простого случая, давайте рассмотрим приложение, состоящее из формы, куда можно ввести валидное имя пользователя. Если вы ввели имя пользователя, соответствующее правилам — User будет создан в системе и записан в Базу Данных.

Исходный код приложения можно найти здесь: github.com/senpay/login-form. Вы были предупрежденны — в приложении куча багов и нет модных тулов и фреймворков.
Если попробовать «накидать» чек лист для данного приложения, можно получить что-то вроде:
Выглядит просто? Просто! Можно ли написать UI-тесты? Можно. Пример написанных тестов (вместе с полноценным трехуровневым фреймворком) можно найти в LoginFormTest.java если перейти на uitests метку в git (git checkout uitests):
Немного метрик для данного кода:
Время выполнения: ~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 и другие тяжеловесные протоколы.
Если мы поковыряем код продукта, мы можем найти кое-что интересненькое:
Когда мы кликаем что-то на UI — вызывается один из этим методов, или добавляется новый объект User, или возвращается список уже созданных объектов User. Что, если мы используем эти методы напрямую? Ведь это самый настоящий API! И самое главное, что REST и иные API тоже работают по тому же принципу — вызывают некий метод «уровня контроллера».
Используя напрямую эти методы, мы можем написать тест попроще да получше:
Этот код доступен по метке subctests:
Попробуем собрать метрики?
Time to execute: ~21 milliseconds
Покрытие кода:
Class: 77.8%
Method: 78.1 (30/32)
Line: 78.7 (75/77)
Насколько важна\существенна потеря покрытия в данном случае? Зависит от ситуации. Мы потеряли немного glue code, который может быть (а может и не быть) важным (рекомендую в качестве упражнения определить, какой код потерялся).
Оправдывает ли данная потеря покрытия введения тяжеловесного тестирования на уровне UI? Это тоже зависит от ситуации. Мы можем, например:
Версия статьи на Английском языке доступна здесь.
Если формат видео для вас больше подходит — можно посмотреть презентацию:
Можно также подписаться на мой Youtube канал тут.
Писать Unit тесты, или даже думать о TDD — уже поздно, код продукта давным-давно написан. Ваше слово, товарищ автотестер!

К счастью, есть небольшой трюк, который позволит и coverage повысить, и сделать тесты стабильными и быстрыми — Subcutaneous tests («подкожные тесты»), но обо всем по порядку.
Суть проблемы
Первый условный рефлекс автоматизатора — это взять Selenium (ну, или там, Selenide, или еще какую вундервафлю для UI тестов). Это такой стандарт индустрии, но есть много причин, почему «не взлетит»:
- UI-тесты медленные. От этого никуда не деться. Их можно запускать параллельно, допиливать напильником и делать чуть-чуть быстрее, но они останутся медленными.
- UI-тесты нестабильные. Отчасти потому, что они медленные. А еще потому, что Web-браузер и интерфейс пользователя не были созданы для того, чтобы ими управлял компьютер (в настоящее время данный тренд меняется, но не факт, что это хорошо).
- UI-тесты — это наиболее сложные тесты в написании и поддержки. Они просто тестируют слишком много. (Это усиливается тем фактом, что, зачастую, люди берут «ручные» тест-кейсы и начинают их автоматизировать как есть, без учета разницы в ручном и автоматическом тестировании).
- Нам говорят, что, якобы, UI-тесты эмулируют реального пользователя. Это не так. Пользователь не будет искать элемент на странице по ID или XPath локатору. Пользователь не заполняет форму со скоростью света, и не «упадет» если какой-то элемент страницы не будет доступен в какую-то конкретную миллисекунду. И даже теперь, когда браузеры разрабатываются с учетом того, что браузером можно программно управлять — это всего-лишь эмуляция, даже если очень хорошая.
- Кто-то скажет, что некоторый функционал просто нельзя протестировать иначе. Я скажу, что если есть функционал, который можно протестировать только UI тестами (за исключением самой UI логики) — это может быть хорошим признаком архитектурных проблем в продукте.
Единственный реальный плюс UI-тестов заключается в том, что они позволяют «накидать» более-менее полезные проверки без необходимости погружения и изучения кода самого продукта. Что вряд ли плюс в долгосрочной перспективе. Более подробно с объяснением, почему так, можно услышать в этой презентации.
Альтернативное решение
В качестве очень простого случая, давайте рассмотрим приложение, состоящее из формы, куда можно ввести валидное имя пользователя. Если вы ввели имя пользователя, соответствующее правилам — User будет создан в системе и записан в Базу Данных.

Исходный код приложения можно найти здесь: github.com/senpay/login-form. Вы были предупрежденны — в приложении куча багов и нет модных тулов и фреймворков.
Если попробовать «накидать» чек лист для данного приложения, можно получить что-то вроде:
| Number | Steps | Expected results |
|---|---|---|
| 1 | 1. Enter a valid user name 2. Click «Log in» button |
1. 2. A new user is created. |
| 2 | 1. Enter an empty user name 2. Click «Log in» button |
1. 2. The error message is given. |
Выглядит просто? Просто! Можно ли написать 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”
- Но даже при условии потери покрытия, прирост скорости и стабильности — значителен.
Версия статьи на Английском языке доступна здесь.
Если формат видео для вас больше подходит — можно посмотреть презентацию:
Можно также подписаться на мой Youtube канал тут.
