Быстрое выполнение сквозных тестов


Reflow v4.14.0

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

Сегодня мы выпустили Reflow v4.14.0, который сократил среднее время выполнения последовательности сквозных тестов на ~70%. Мы пишем эту статью, чтобы объяснить, как это сделать. Reflow — это инструмент с низким кодом, который помогает вашей команде разрабатывать и поддерживать отказоустойчивые сквозные тесты. Мы используем playwright под капотом, поэтому все, что мы сделали, может быть применено независимо от того, используете вы Reflow или нет.

Невероятно параллельное тестирование

Ваш набор сквозных тестов должен выполнять шаги пользователя в потоке синхронно, но должен выполнять несколько независимых потоков параллельно.

В Reflow мы поддерживаем это с помощью нашей распределенной архитектуры. Композиция тестов с конвейерами позволяет создать N серверов для обработки N пользовательских потоков: вам просто нужно спроектировать свои наборы тестов так, чтобы они могли работать параллельно.

Несмотря на это, синхронный код может легко закрасться в систему.

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

for (const action of toUpdate) {
  await queryAll<ActionInContextModel, MutationUpdateActionInContextArgs>(client, {
    mutation: shallowUpdateActionInContext,
    variables: {
      input: action,
    },
  });
}
Вход в полноэкранный режим Выход из полноэкранного режима

Это можно тривиально переписать для параллельного выполнения.

await Promise.all(toUpdate.map((action) =>
  queryAll<ActionInContextModel, MutationUpdateActionInContextArgs>(client, {
    mutation: shallowUpdateActionInContext,
    variables: {
      input: action,
    },
  }))
);
Вход в полноэкранный режим Выход из полноэкранного режима

Reflow использует AppSync Resolvers с бессерверной DynamoDB для работы наших API. Это масштабируется вверх/вниз по мере необходимости, поэтому мы не видим абсолютно никакого негативного влияния на параллельную работу. Мы исправили это в версии 4.12.1, а в версии 4.14.0 мы улучшаем это еще больше, внедряя дополнительный слой кэширования, чтобы уменьшить количество S3-объектов для скриншотов.

Условная стабильность

Reflow узнает о вашем приложении и использует эти знания для поддержания стабильности записанных тестов. Однако поддержание стабильности записанных тестов часто требует времени; начиная с версии 4.14.0 Reflow динамически сокращает количество методов поддержания стабильности, внедряя дополнительную логику для определения их необходимости.

До версии 4.14.0 этот участок кода всегда ожидал трех событий после каждого действия: события load, события networkidle и события screenshotstable.

private async pageStable(baselineAction): Promise<void> {
  try {
    await this.page.waitForLoadState('load', { timeout: 30000 });
  } catch (e) {
    logger.verbose(this.test.id, "waitForLoadState('load')", e);
  }
  try {
    await this.page.waitForLoadState('networkidle', { timeout: 5000 });
  } catch (e) {
    logger.verbose(this.test.id, "waitForLoadState('networkidle')", e);
  }

   await this.screenshotStable(baselineAction?.preStepScreenshot?.image);
}
Вход в полноэкранный режим Выход из полноэкранного режима

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

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

Reflow динамически узнает о вашем приложении, подключаясь к событиям DOM и Network. Если вы хотите тратить меньше усилий на поддержку сквозной работы, попробуйте его.

Больше никаких явных ожиданий

Во многих компаниях при возникновении проблем со сквозной стабильностью распространенной стратегией является добавление await wait(1000) или эквивалента, чтобы просто задержать выполнение тестовых действий.

Это гораздо больший грех, чем произвольное использование методов стабильности. Методы стабильности завершатся раньше, как только сработает их событие стабильности. Если вы чувствуете себя ленивым, по крайней мере, попробуйте сначала использовать waitForLoadState, а не wait.

Если вы готовы написать немного больше кода, попробуйте написать пункт waitUntil, который явно ожидает установки некоторого состояния страницы, когда страница будет стабильной. Хотя reflow поддерживает действие wait, мы советуем нашим клиентам всегда использовать вместо него визуальное утверждение. Оно будет ждать (до произвольно настраиваемого максимума), пока элемент страницы не будет выглядеть как зафиксированная базовая линия. В противном случае можно настроить reflow на продолжение или провал теста.

Несколько утверждений wait(X) со временем прокрались в кодовую базу reflow. Возникает соблазн добавить их в новые функции, чтобы лениво повысить стабильность, достаточную для отправки функции. В версии 4.14.0 мы безжалостно удалили все явные вызовы wait из горячих путей кода, заменив их динамическим временем ожидания, настроенным на основе тестируемого приложения.

Перенос вычислений за пределы «горячих» путей

Reflow использует алгоритм визуального сравнения (SSIM weighted Pixel Diff) для вычисления визуальных изменений. Он захватывает веб-страницы во весь рост, затем сравнивает их с базовыми изображениями для расчета стабильности и информирования пользователя об изменениях страницы.

Эта операция полностью выполняется на центральном процессоре и поэтому является относительно медленной: для страницы высотой 1080 x 10000px мы обнаружили, что она может заблокировать основной поток на 2-3 секунды. Это было основным узким местом в производительности больших приложений.

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

private async screenshotStable(baselineScreenshot: S3ObjectInput | undefined): Promise<{
  diff: Promise<ComparisonModel | undefined>;
  current?: { image: S3ObjectInput };
}> {
/* ... */
}
Вход в полноэкранный режим Выход из полноэкранного режима

Перенос вычислений в рабочие потоки

По умолчанию все вычисления в Node.JS однопоточны. Обычно создание многопоточных приложений не стоит усилий: большинство процессов node.js могут справиться с работой, распределяя ее между процессами, вместо того, чтобы иметь дело с потоками.

В Reflow, поскольку сравнение изображений требует больших вычислений, мы перенесли его в рабочий поток. Это гарантирует, что приложение не будет остановлено во время визуального сравнения: все остальные асинхронные процессы (например, загрузка результатов тестирования в реальном времени) могут выполняться параллельно со сравнением изображений.

Мы сделали это с помощью npm threads wrapper и esbuild. Сначала мы перенесли весь наш вычислительный код в новый файл с минимальным количеством импортов, названный imageCompare.worker.js. Затем мы добавили шаг предварительной компиляции с помощью esbuild, чтобы скомпилировать этот файл в пакет. Затем мы порождаем рабочего, используя этот сгенерированный файл в качестве блоба, и взаимодействуем с ним через интерфейс обещания threads.

import fs from 'fs';
import { expose } from 'threads/worker';
import { isMainThread } from 'worker_threads';

/* ... */

const workerExports = {
  configureWorker,
  compareFiles,
  compareScreenshots,
};

if (!isMainThread) {
  expose(workerExports);
}
export type ImageCompareWorkerExports = typeof workerExports;
Вход в полноэкранный режим Выход из полноэкранного режима
import { spawn, BlobWorker } from 'threads';

import type { ImageCompareWorkerExports } from './imageCompare.worker';
import { configureWorker, comparePNG as workerComparePng, cropImage as workerCropImage } from './imageCompare.worker';
import { source as workerBlob } from '../../generated/imageCompare.workerSource';
import logger, { getLevel } from '../logger';

let worker: ImageCompareWorkerExports;

export async function bootImageCompareWorker() {
  try {
    worker = await spawn<ImageCompareWorkerExports>(BlobWorker.fromText(workerBlob));
    configureWorker(getLevel(process?.env?.LOG_LEVEL));
    return worker.configureWorker(getLevel(process?.env?.LOG_LEVEL));
  } catch (e) {
    logger.fatal('Error starting worker', e);
  }
}

export async function compareFiles(imageA: string, imageB: string, outFile: string): Promise<void> {
  return worker.compareFiles(imageA, imageB, outFile);
}

export async function compareScreenshots(preData: Buffer, postData: Buffer, options): Promise<ScreenshotCompareOutput> {
  return worker.compareScreenshots(preData, postData, options);
}
Вход в полноэкранный режим Выход из полноэкранного режима

Отслеживайте свои релизы, проводите сквозные тесты раз в релиз

Ваши сквозные тесты должны быть направлены на выявление регрессий, а не багов: если они прошли один раз в релизе, не стоит выполнять их снова.

В версии 4.14.0 мы сделали первый шаг к тому, чтобы помочь QA-командам сделать это: отслеживание релизов с использованием карты исходных текстов. Теперь Reflow хэширует и загружает все карты исходников, связанные с данным релизом (условно с регулярным выражением для фильтрации кода приложения), чтобы создать идентификатор версии. Это означает, что он может отслеживать, когда ваше приложение было выпущено, и, следовательно, позволит вам определить, была ли сквозная последовательность уже успешно выполнена на этом релизе.

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

TL;DR

  1. Откажитесь от всех операторов sleep: всегда ждите определенных событий. Вводите методы обеспечения стабильности только в случае необходимости.
  2. Выполняйте тесты параллельно
  3. Разумно кэшируйте тесты; не стоит выполнять их чаще, чем один раз за релиз.

Если вы чувствуете себя авантюристом, попробуйте reflow: SaaS для автоматизации записи/воспроизведения тестов с низким кодом, который попытается сделать все это за вас, быстро.

Небольшое предостережение: для максимальной скорости используйте reflow на собственном хостинге. Мы используем AWS Fargate для создания эфемерных экземпляров браузера для каждого пользователя, время холодного запуска которых составляет ~1 минуту при первом использовании устройства записи тестов.

Оцените статью
Procodings.ru
Добавить комментарий