Что пишут в блогах

Подписаться

Онлайн-тренинги

Конференции

Что пишут в блогах (EN)

Разделы портала

Про инструменты

.
Пишем автотесты эффективно — Subcutaneous tests
04.06.2020 00:00

Оригинальная публикация
Автор: Александр Пушкарев

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

Писать Unit тесты, или даже думать о TDD — уже поздно, код продукта давным-давно написан. Ваше слово, товарищ автотестер!

image

К счастью, есть небольшой трюк, который позволит и 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
  • Но даже при условии потери покрытия, прирост скорости и стабильности — значителен.


Версия статьи на Английсом языке доступна здесь.

Если формат видео для вас больше подходит — можно посмотреть презентацию:

Обсудить в форуме