9 способов сделать медленные тесты более быстрыми

От разработчиков до тестировщиков, от бизнес-аналитиков до руководства — все в вашей организации должны быть привержены поддержанию тестов в идеальном состоянии. Если у вас обширный набор тестов, вам понадобится план, чтобы сосредоточить усилия. Если вы не знаете, с чего начать, ознакомьтесь с нашей 5-этапной схемой выявления и оптимизации тестов.

Разработка программного обеспечения зависит от поддерживающих ее инструментов. Из них тестирование является самым распространенным и оказывает наибольшее влияние. Быстрая и оперативная работа тестов ведет к повышению производительности, улучшению качества кода и увеличению частоты развертывания.

Когда тестирование замедляется, разработка следует его примеру. Команды расстраиваются, когда они не могут достичь своих целей, а организации развертываются так быстро, как им хотелось бы. В Semaphore мы видели свою долю тестов и определили девять факторов, которые замедляют тесты.

Насколько быстрыми должны быть тесты?

Тесты в первую очередь используются разработчиками, которые сначала запускают их на своих машинах перед фиксацией изменений. Быстрые тесты поддерживают продуктивность разработчиков, позволяя им сохранять творческий фокус, который так важен для решения проблем.

По этой мерке набор тестов, занимающий больше времени, чем время, необходимое для того, чтобы размять ноги и выпить чашку кофе, является слишком медленным.

Разработчики должны иметь возможность запускать тесты локально — по крайней мере, ту часть набора, которая непосредственно связана с кодом, над которым они работают. Быстрый и простой в использовании набор тестов будет стимулировать разработчиков запускать тесты до, во время и после внесения изменений.

Тесты также влияют на скорость CI/CD. Чем больше времени они занимают, тем медленнее ваш конвейер. Это важно, потому что конвейер диктует, как часто вы можете выпускать релизы. Медленный конвейер нарушает цикл разработки.

Точнее говоря, вы не занимаетесь непрерывной интеграцией, если ваш конвейер не занимает менее 10 минут. А поскольку тесты выполняются в конвейере, они должны укладываться в это 10-минутное окно.

Как сделать медленные тесты снова быстрыми

Как исправить медленные тесты? И как ускорить конвейер CI/CD? Вот девять наиболее распространенных проблем с производительностью и их решения:

  1. Мои тесты слишком большие: разбейте их на части.
  2. Мои тесты имеют множество зависимостей: изолируйте тесты с помощью заглушек или мокинга.
  3. Мои тесты тесно связаны между собой: рефакторить их, чтобы сделать независимыми.
  4. У меня есть устаревшие тесты: удалите мертвый код и устаревшие тесты.
  5. Мои тесты используют sleep и wait: замените операторы sleep на механизмы синхронизации.
  6. Мои тесты используют базу данных: убедитесь, что запросы оптимизированы.
  7. Мои тесты всегда проходят через пользовательский интерфейс: сократите количество взаимодействий с пользовательским интерфейсом. Например, если доступен API, тестируйте его.
  8. Я настраиваю свои тесты через UI: используйте внеполосные каналы для настройки и разрыва тестов.
  9. Мои тесты охватывают каждый крайний случай в пользовательском интерфейсе: сосредоточьтесь только на самых критических путях для ваших пользователей.

Разбиение больших тестов

  • Проблема: большие тесты требуют много времени для завершения, их трудно поддерживать, и они не могут использовать преимущества параллелизма.
  • Решение: разделяй и властвуй. Разделите набор тестов на небольшие блоки и настройте параллельные задания в конвейере.

Длинные, однопоточные тесты не получают преимущества от множества ядер, которыми оснащены современные машины. Давайте посмотрим на следующий псевдокод, который тестирует одну функцию тремя разными способами:

// tests 1 to 3
setup()
value1 = function(input1)
assert value1 == output1
cleanup()

setup()
value2 = function(input2)
assert value2 == output2
cleanup()

setup()
value3 = function(input3)
assert value3 == output3
cleanup()
Войти в полноэкранный режим Выход из полноэкранного режима

Разбиение этого теста на части может ускорить его выполнение в 3 раза. В зависимости от вашего решения для тестирования, это может быть просто распределение кода между тремя файлами и настройка вашего фреймворка на использование всех доступных процессоров.

Тест1 Тест2 Тест3
setup() setup() setup()
value1 = function(input1) value2 = function(input2) value3 = function(input3)
assert value1 == output1 assert value2 == output2 assert value3 == output3
cleanup() cleanup() cleanup()

Параллелизация, однако, не лишена опасностей. Тесты, выполняемые последовательно, занимают больше времени, но их легче понять. С другой стороны, параллелизм сложен. Он может привести к неожиданным результатам, если тесты имеют побочные эффекты и не изолированы. Небольшая ошибка может привести к трудноотлаживаемым проблемам, состояниям гонки и неработающим тестам.

Параллельные задания в CI/CD

Если вы вынесли из всей этой статьи только одно, то пусть это будет следующее: распараллеливание дает самые непосредственные преимущества при наименьших усилиях — в основном при применении к конвейеру CI/CD — несмотря на вышеупомянутые препятствия в его реализации. Как только распараллеливание перестанет быть экономически эффективным, продолжайте выполнять остальные рекомендации, приведенные в статье.

Разделение тестов открывает совершенно новый уровень оптимизации. Облачные сервисы, такие как Semaphore, позволяют расширить масштабы непрерывной интеграции и доставки за пределы одной машины.

Сделайте тесты независимо запускаемыми

  • Проблема: тесно связанные тесты снижают скорость и затрудняют рефакторинг.
  • Решение: разделите компоненты так, чтобы они были независимо тестируемыми. Не нарушайте инкапсуляцию тестов.

Разработка тестируемого кода означает создание небольших, изолированных и развязанных компонентов. Мы уже видели, что распараллеливание может помочь нам сократить время выполнения тестирования. Разделение компонентов — необходимая часть этого процесса.

Тесты добавляют ценность в той степени, в которой они сохраняют дистанцию от реализации. Когда они слишком тесно связаны друг с другом, они ломаются легче и по неправильным причинам. Тест должен проверять внешнее поведение, а не то, как он работает внутри. Тесты, слишком тесно связанные с кодом, препятствуют рефакторингу.

Представьте, что у нас есть класс, представляющий корзину для покупок, как показано ниже:

class ShoppingCart {

    Item _lastItemAdded;
    Item[] _cartContents;
    float _totalSum;

    function AddItem(Item) {
        _lastItemAdded = Item;
        _cartContents.Add(Item);
        _totalSum =+ Item.cost;
    }

    function getTotal() {
        return _totalSum;
    }

    function getItem(index) {
        return _cartContents[index];
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

И мы пишем следующий тест:

// test item added
item = new Item("bottle", 10.50);
cart = new ShoppingCart();
cart.AddItem(item);

assert _totalSum == 10.50;
assert _lastItemAdded.name == "bottle";
assert _cartContents.toString() == "bottle-10.50";
Войти в полноэкранный режим Выйти из полноэкранного режима

Тест знает слишком много о внутренних деталях тестируемого класса. Он нарушает инкапсуляцию, что делает невозможным рефакторинг класса без нарушения теста:

  • Тест проверяет lastItemAdded вместо того, чтобы использовать геттер.
  • Тест обращается к приватным атрибутам, таким как _cartContents.
  • Тест зависит от способа сериализации объектов.

Подумайте вот о чем: вы хотите, чтобы тест не сработал, только если поведение общедоступного интерфейса изменится. Поэтому давайте сделаем тесты, которые будут определять поведение.

// test item added
item = new Item("bottle", 10.50);
cart = new ShoppingCart();
cart.AddItem(item);

assert cart.getTotal() == 10.50;
assert cart.getItem[0].name == "bottle";
assert cart.getItem[0].price == 10.50;
Вход в полноэкранный режим Выйти из полноэкранного режима

Ветвление с помощью абстракции

Масштабный рефакторинг не происходит за одну ночь — на это уйдет несколько недель или даже месяцев. Постепенного изменения можно добиться с помощью разветвления по абстракции. Эта техника позволяет вам продолжать выпускать релизы, пока происходят изменения.

Ветвление путем абстракции начинается с выбора компонента, который нужно отделить от остальной части кодовой базы:

Затем оберните компонент в слой абстракции и перенаправьте все вызовы на новый слой.

Слой абстракции скрывает детали реализации. Если компонент большой и громоздкий, создайте его копию и работайте над ней, пока она не будет готова. Затем рефакторите компонент до тех пор, пока он не станет пригодным для самостоятельного тестирования.

После завершения рефакторинга переключите слой абстракции на новую реализацию.

Создание самодостаточных тестов

  • Проблема: внешние зависимости замедляют тестирование и вносят неопределенность в результаты.
  • Решение: замените внешние компоненты тестовыми дублями, заглушками и моками. Исключите из уравнения сервисы, базы данных и API, чтобы сделать модульные тесты самодостаточными.

Макет — это упрощенный экземпляр компонента, который реагирует как настоящий, по крайней мере, в рамках рассматриваемого теста. Ему не нужно реализовывать весь спектр ответов, только те, которые считаются релевантными. Например, вместо запроса реальных данных от конечной точки API, как это сделано ниже:

function precipitation
      weather = fetch 'https://api.openweathermap.org/data'
      return weather.precipitation
end function

function leave_the_house
      if precipitation() > 0 then
            return "Don't forget your umbrella. It's raining!"
      else
            return "Enjoy the sun."
      end if
end function
Войти в полноэкранный режим Выйти из полноэкранного режима

Мы можем заменить реальную функцию precipitation консервированным ответом. Это сделает тест самодостаточным и, таким образом, более надежным. Это также позволит избежать непреднамеренных злоупотреблений API. Пока интерфейс поддерживается, код теста остается действительным.

// Override the precipitation function
function precipitation()
    return 10
end function

message = leave_the_house()
assert message == "Don't forget your umbrella. It's raining!"
Вход в полноэкранный режим Выход из полноэкранного режима

Конечно, теперь, когда мы отделены от реального API, любые разрушающие изменения не будут замечены тестом. Мы можем реализовать отдельный тест, который периодически опрашивает API и проверяет результат, чтобы снизить риски. Этот тип тестов называется контрактным тестированием.

Мокинг, заглушки и тесты-двойники позволяют запускать часть кода в изоляции. При этом вы делаете тесты намного быстрее, поскольку не ограничены ограничениями среды. Без них вы получаете противоречивые результаты, поскольку ваши тесты зависят от внешних компонентов, которые, скорее всего, находятся вне вашего контроля.

Чтобы помочь вам изолировать тесты, Semaphore принудительно создает чистую среду для каждого задания.

Удаление устаревших тестов и мертвого кода

  • Проблема: мертвый код и устаревшие тесты теряют время и увеличивают технический долг проекта.
  • Решение: сделайте небольшую уборку в доме. Удалите весь устаревший код из вашего проекта.

Не бойтесь удалять код и тесты. Тесты устаревают, когда тестируемый код выходит из обращения. Устаревшие тесты — это часть технического долга, с которым рано или поздно придется столкнуться команде разработчиков.

Устаревшие тесты не обязательно являются единственными, которые заслуживают удаления. Иногда тест покрывает рабочий код, как в примере ниже. Однако если тест тривиален и не дает большой ценности, его не должно быть в вашем наборе. В таких случаях его лучше удалить.

person = new Person()
person.setName("John")
name = person.getName()
assert name == "John"
Вход в полноэкранный режим Выход из полноэкранного режима

Как удалить тест

Безопаснее удалять устаревшие тесты в два этапа:

  1. Запустите тестовый набор локально и проверьте, что ВСЕ ПРОЙДЕНЫ.
  2. Удалите протестированный код.
  3. Если ничего не сломалось, удалите тест.

Тесты должны быть фальсифицируемыми. Другими словами, удаление кода должно привести к отказу теста — если это не так, то что вы тестировали?

Убедившись, что удаление кода не нанесло никакого побочного ущерба, удаляем тест.

Исключите из тестов операторы wait/sleep

  • Проблема: операторы ожидания замедляют работу тестового пакета и скрывают целый ряд проблем проектирования.
  • Решение: используйте надлежащие механизмы синхронизации.

Наличие утверждений sleep в тесте показывает, что разработчику нужно было чего-то ждать, но он не знал, как долго. Поэтому они установили фиксированный таймер, который заставил тест работать.

Утверждения сна обычно встречаются между вызовом функции и ее проверяющим утверждением:

output1 = my_async_function(parameters)
// wait 2000 milliseconds
wait(2000)
assert(output1 == value1)
Войти в полноэкранный режим Выход из полноэкранного режима

Хотя это может работать. Это хрупко и неполированно. Зачем ждать две секунды? Потому что это работало на машине разработчика? Вместо сна следует использовать какой-то механизм опроса или синхронизации, чтобы свести ожидание к минимуму. В JavaScript, например, await/async решает эту проблему:

async function my_async_function() {
    // function definition
}

output1 = await my_async_function()
assert(output1 == value1)
Вход в полноэкранный режим Выход из полноэкранного режима

Многие языки также имеют обратные вызовы для цепочки действий:

function callback(output) {
      assert(output == value)
}

function my_function(callback) {
    // function definition

    callback(output)
}

my_async_function(callback)
Вход в полноэкранный режим Выход из полноэкранного режима

В обоих случаях наиболее правильная реализация также будет включать некоторую форму таймаута для обработки неотвечающих конечных точек.

Часто ожидание добавляется в качестве обходного пути для нестабильного теста — теста, который то удается, то не удается без видимых причин. Однако ожидание не является правильным решением; оно просто скрывает проблему.

Ожидание сервисов

Утверждения Sleep следует заменить надлежащим опросом или передачей сообщений, чтобы определить, когда необходимый ресурс будет готов. Это работает, и в некоторых случаях это может быть единственной альтернативой, которая у вас есть:

while port not open
      sleep(100)
end while

// test that needs port available...
Войти в полноэкранный режим Выйти из полноэкранного режима

Но когда есть возможность, гораздо лучше открыть порт и передать тестовую функцию в качестве обратного вызова. Это обеспечит цепочку действий без необходимости явного ожидания.

function run_test()
      // the port is open, run your test here
end function

connect(port, run_test)
Вход в полноэкранный режим Выход из полноэкранного режима

Оптимизация запросов к базе данных в тестах

  • Проблема: неоптимальные запросы к базе данных расходуют ресурсы и замедляют работу тестов.
  • Решение: профилируйте запросы и оптимизируйте их.

Если тест вызывает запрос к базе данных, сначала проверьте, можно ли заменить его предварительно заполненным набором данных. Основная часть ваших модульных тестов не должна зависеть от базы данных. Это неэффективно и обычно не нужно.

Как высмеивать вызовы базы данных? Представьте, что мы хотим подсчитать всех пользователей в таблице. У нас есть функция, которая подключается к базе данных и выполняет запрос.

import mysql

function countUsers(host, port, user, password)
    connection = new mysql(host, port, user, password)
    users = connection.exec("SELECT USERNAME FROM USERTABLE")
    return users.count()
end function

print countUsers('localhost', 3306, 'dbuser', 'dbpassword')
Вход в полноэкранный режим Выход из полноэкранного режима

Несмотря на то, что эта функция проста, ее трудно сымитировать. Вам придется написать замену MySQL, которая принимает те же параметры и ведет себя достаточно похоже, чтобы быть полезной. Лучшей альтернативой является использование инверсии управления. Этот паттерн подразумевает внедрение кода в тестируемую функцию для обеспечения большего контроля.

function countUsers(dbobject)
    if dbobject.connected is false
        throw "database not connected"
    end if

    users = dbobject.exec("SELECT USERNAME FROM USERTABLE")
    return users.count()
end function
Вход в полноэкранный режим Выход из полноэкранного режима

Здесь мы отправляем объект базы данных в качестве параметра, что делает вызов функции немного сложнее:

import mysql

connection = new mysql(host, port, user, password)
print countUsers(connection)
Войти в полноэкранный режим Выход из полноэкранного режима

Тестирование, однако, более простое:

Class MockedDB
      // always connected
    connected = true

      // returns a canned answer, no db connection required
    function exec
        return ["Mary", "Jane", "Smith"]
    end function

end class

usercount = countUsers(MockedDB)
assert usercount == 3
Вход в полноэкранный режим Выход из полноэкранного режима

Запрос к реальной базе данных

Макеты не всегда работают. Иногда требуется реальная база данных, особенно во время интеграции и сквозного тестирования. Semaphore предоставляет популярные сервисы баз данных из коробки, которые можно быстро запустить в тестовой среде.

Когда дело доходит до использования базы данных, существует ряд классических ошибок. Возможно, наиболее распространенной является проблема N+1, которая возникает из-за объединения циклов и SQL-запросов.

users = db.exec("SELECT id, name FROM Users")
foreach user in users
    email = db.exec("SELECT email FROM Emails WHERE userid = $user['id']")
    body = "Hello $user['name']"
    sendEmail(body, email)
end foreach
Вход в полноэкранный режим Выход из полноэкранного режима

В чем здесь проблема? На первый взгляд, нет ничего страшного: получить всех пользователей из таблицы Users, затем их адрес электронной почты из другой таблицы. Но подумайте о том, сколько запросов попадает в базу данных: один для получения всех пользователей и еще один для каждого найденного пользователя. Если у вас 10 000 пользователей, то это 10 001 запрос.

Базы данных, в частности базы данных SQL, предназначены для работы с наборами. Они ненавидят итерации, потому что каждый запрос требует больших предварительных затрат на обработку. Мы можем сократить 10 001 запрос до одного, запросив все записи за один раз и воспользовавшись предложением JOIN:

users = db.exec("SELECT id, name, email FROM Users JOIN Emails ON Users.id = Emails.userid")
foreach user in users
    body = "Hello $user['name']"
    sendEmail(body, $user['email'])
end foreach
Войти в полноэкранный режим Выход из полноэкранного режима

Выбор всех столбцов

Выборка всех столбцов — еще одна легко совершаемая ошибка. Запрос SELECT * создает несколько проблем:

  • Извлекает ненужные столбцы, что приводит к увеличению объема ввода-вывода и использования памяти.
  • Отменяет некоторые преимущества, предоставляемые существующими индексами.
  • Легче ломается при изменении порядка столбцов или имен столбцов при использовании в JOIN.

Поэтому вместо выбора всех столбцов

SELECT *
FROM Users U
JOIN Orders O on O.userid = O.id
Войти в полноэкранный режим Выйти из полноэкранного режима

Вы должны быть более явными и запрашивать только те данные, которые вам нужны:

SELECT U.id, U.name, O.shipping_address
FROM Users U
JOIN Orders O on O.userid = O.id
Войти в полноэкранный режим Выйти из полноэкранного режима

Пакетные операции

Пакетная обработка — это возможность изменения нескольких записей в одной транзакции. Достигая скорости обработки данных намного выше, чем при обычных транзакциях, пакетная обработка является отличным способом инициализации теста. Таким образом, вместо того чтобы вставлять по одной строке за раз, как это делается здесь:

INSERT INTO founders (name, surname) values ('John', 'Romero');
INSERT INTO founders (name, surname) values ('John', 'Carmack');
INSERT INTO founders (name, surname) values ('Tom', 'Hall');
Войти в полноэкранный режим Выйти из полноэкранного режима

Рассмотрим возможность объединения значений в одном операторе:

INSERT INTO founders (name, surname) values ('John', 'Romero'), ('John', 'Carmack'), ('Tom', 'Hall');
Ввести полноэкранный режим Выйти из полноэкранного режима

Аналогичные полезные устройства существуют при настройке теста через ORM. Возьмем пример RSpec:

before(:each) do
    @user = User.create(name: "Adrian", surname: "Carmack")
end

after(:each) do
    @user.destroy
end

context 'model test' do
    subject { @user }
    it { should validate_presence_of(:name) }
    it { should validate_presence_of(:surname) }
    it { should have_many(:projects) }
end
Войти в полноэкранный режим Выход из полноэкранного режима

Нужно немного знать Ruby, чтобы понять, что before(:each) и after(:each) здесь не являются оптимальными вариантами. Тест создает и удаляет пользователя для каждого оператора it. Поскольку ни один из тестов не изменяет данные, вы можете инициализировать набор данных один раз с помощью before(:all) и after(all), которые запускаются только один раз в начале и в конце теста. Подумайте о том, чтобы загрузить данные образца один раз и повторно использовать их в своих тестах, когда это возможно.

Тестируйте API вместо пользовательского интерфейса

  • Проблема: слишком большое количество тестов пользовательского интерфейса снижает производительность пакета.
  • Решение: избегайте пользовательского интерфейса, когда есть более подходящие для тестирования цели, например, конечная точка API.

API предназначен для программного доступа, и он всегда будет лучше подходить для проведения тестов, чем пользовательский интерфейс. Это не означает, что пользовательский интерфейс не следует тестировать, просто если у вас есть выбор между тестированием через API и через пользовательский интерфейс, первый вариант проще и менее ресурсоемкий.

В следующем примере показан базовый тест пользовательского интерфейса. Он посещает URL-адрес, выполняет поиск и проверяет полученное значение:

driver = new ChromeDriver();
driver.get("http://myapp/users");

// search for a user
driver.findElement(By.linkText("User")).sendKeys("John");
driver.findElement(By.linkText("Search")).click();

// validate result
firstTableCell = driver.findElement(By.Xpath("//table/tbody/tr[0]/td[0]"));
assertEquals(firstTableCell.getText(), "John")
Войти в полноэкранный режим Выход из полноэкранного режима

Даже такой простой тест требует многого: нам нужно открыть браузер, дождаться рендеринга страницы и смоделировать все взаимодействия. Не говоря уже о том, что все приложение должно быть запущено еще до того, как мы начнем.

Сравните это с более тонким тестом, пингующим ту же конечную точку API, которую потребляет приложение.

request = fetch("http://myapi/v1/users$?name=John");

assertEquals(request.status(), 200);
assertEquals(request.header.contentType, "application/json");
assertEquals(request.body.users.name, "John");
Вход в полноэкранный режим Выход из полноэкранного режима

Здесь мы проверяем, что API ведет себя в соответствии со спецификацией, и добиваемся того же, что и в тесте, который проходил через пользовательский интерфейс. Этот вид тестирования масштабируется гораздо лучше, поскольку использует меньше ресурсов и может быть легче распараллелен.

Уменьшение количества взаимодействий с пользовательским интерфейсом при настройке

  • Проблема: настройка тестов с помощью пользовательского интерфейса является неоптимальной.
  • Решение: готовьте тест вне диапазона и обращайтесь к пользовательскому интерфейсу только для тестируемых элементов.

Поскольку UI-тесты требуют больше времени на выполнение и больше усилий на поддержку, нам нужно сделать их легковесными. Если вы хотите протестировать кнопку оформления заказа, тестируйте кнопку оформления заказа и ничего больше. Вам не нужен пользовательский интерфейс для заполнения корзины товарами. Вы можете сделать это за сценой, заполнив базу данных или напрямую вызвав API.

Конечно, вам все равно нужно проводить сквозное тестирование, и это более доступно при использовании BDD-фреймворка, такого как Cucumber или JBehave. Тестовые случаи могут быть записаны с использованием структурированного паттерна Gherkin Given-When-Then, что помогает сделать их более упорядоченными:

Feature: Checkout cart
  User must be able to buy their products

  Scenario: Successful purchase
    Given cart has products
    When I press the checkout button
    Then my order should be accepted
Вход в полноэкранный режим Выход из полноэкранного режима

Обман в тесте

В приведенном выше сценарии требуется наличие корзины с товарами. Это заданное условие, но тесту не важно, как была заполнена корзина. Мы можем «схитрить» и заполнить корзину непосредственно с помощью запроса к базе данных, что означает, что нам не нужно обращаться к пользовательскому интерфейсу. Суть в том, что не все должно проходить через пользовательский интерфейс.

@Given("cart has products")
public void cart_has_products() {
    stmt.executeUpdate("INSERT INTO Cart (user_id,product_id) VALUES (1,1000), (1,2000), (1, 3000)");
}
Вход в полноэкранный режим Выход из полноэкранного режима

Для сравнения, оба пункта When и Then должны иметь дело с пользовательским интерфейсом, поскольку они тестируют его, чтобы подтвердить опыт оформления заказа. Под капотом каждый пункт теста реализуется с помощью фреймворка, такого как Selenium:

public class StepDefinitions {

    private WebDriver driver = new ChromeDriver();

    @When("I press the checkout button")
    public void i_click_checkout() {
        driver.findElement(By.linkText("Checkout")).click();
    }

    @Then("my order should be accepted")
    public void i_should_get_order_number() {
        assertNotNull(driver.findElement(By.xpath("//*[matches(@id, 'Your order number is d+')]")));
    }

}
Вход в полноэкранный режим Выход из полноэкранного режима

У фреймворков для тестирования есть способы повторного использования сценариев и условий. Например, в Cucumber есть фоны. Время, потраченное на оптимизацию взаимодействия с пользовательским интерфейсом, обычно является хорошо потраченным временем.

Сосредоточьте тесты пользовательского интерфейса на счастливых путях

  • Проблема: попытка протестировать каждый уголок пользовательского интерфейса.
  • Решение: тщательно подходите к выбору тестов. Вам не нужно пробовать все возможные комбинации.

К этому моменту вы, наверное, уже поняли, что девиз для тестирования пользовательского интерфейса и сквозного тестирования — «будь коротким, будь ценным». Каждый тест должен приносить прибыль.

Слишком строгое отношение к пользовательскому интерфейсу — не лучшая идея, так как в этом случае легко нарушить тест, передвинув кнопку или изменив строку. Вы должны иметь возможность вносить косметические изменения и улучшения, не нарушая слой сквозного тестирования.

Это не означает, что тесты пользовательского интерфейса не ценны. Просто нужно быть разборчивым в выборе путей тестирования. Например, вы определенно хотите убедиться, что новые пользователи могут создать учетную запись или что существующие пользователи могут войти в систему. И наоборот, тестирование того, что происходит, когда пользователь вводит в поле поиска символ, не относящийся к юникоду, лучше проводить с помощью модульного теста.

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

Не пренебрегайте тестами

Важность отзывчивых тестов невозможно переоценить. Тесты — это код, и к ним следует относиться так же бережно. Необходимо выделять время на их поддержание, потому что скорость набора тестов напрямую зависит от того, как часто вы можете выпускать программное обеспечение. Набор тестов будет постепенно замедляться, если его не поддерживать должным образом, что приведет к снижению морального духа команды и заставит вашу организацию пропустить сроки. Не позволяйте этому случиться!

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *