Тестирование

Переопределяем внешний контент для тестов playwright

При написании автотестов очень важная скорость их работы, а также воспроизводимость — не должно быть косвенных факторов, из-за которых может свалиться тест. Тест должен проверять именно функциональность нашего приложения, выдавать надёжный и чёткий результат, работает оно или нет. Если работа теста завязана на каких-то внешних сервисах, то получается, что мы тестируем уже не чисто наше приложение, а приложение+сервис. Если какой-то внешний сервис внезапно стал недоступен, то тест свалится, а мы будем искать ошибку в нашем приложении, которой на самом деле нет.

В этой статей поговорим о том, как в тестах, написанных на фреймворке playwright, избавиться от зависимости от CDN.

Как работает загрузка с CDN

В современных приложениях часто такие ресурсы, как шрифты, иконки, изображения, иногда скрипты загружаются с внешнего CDN-сервера. В этом случае CDN и становится таким внешним сервисом, который делает наши автотесты менее надёжными, а также замедляет из работу, поскольку для каждого запуска(ведь playwright очищает кеш в начале каждого теста) шрифты будут грузиться с серверов Google, и стоит хоть раз оборваться соединению, как вся серия тестов будет помечена как неудачная, и нам потребуется потратить время, чтобы выяснить, в чём дело.

Google Fonts

Например, для использования шрифтов часто используется сервис Google Fonts. В этом случае вебмастер не скачивает себе никакие шрифты, а просто находит на сайт Google Fonts, выбирает нужный шрифт, выбирает, какие степени жирности будет использоваться. После этого он получает фрагмент когда, который нужно вставить себе на сайт в секцию <head>. Например, вот такой:

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" rel="stylesheet">

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

Загружаем файлы себе

В первую очередь нам необходимо точно выяснить, какие именно файлы скачиваются с CDN. Проще всего для этого использовать инспектор Chrome — открыть новую вкладку браузера, затем открыть в инспекторе вкладку Network, затем открыть приложение. После этого нужно найти те файлы, которые загружаются не с localhost (или другого адреса, по которому работает ваше приложение). Эти файлы нужно скачать себе, а также запомнить или записать адреса, по которым они скачиваются.

Добавляем файлы в проект

Создаём директорию tests/files/fonts, сохраняем туда загруженные файлы. При этом желательно, чтобы структура директорий повторяла пути к файлам. Например, сохраним файлы следующим образом.

https://fonts.googleapis.com/css2?family=Roboto&display=swap googleapis/roboto.css → tests/fonts/googleapis/Roboto.css
https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu5mxKOzY.woff2 → tests/fonts/gstatic/roboto/v30/KFOmCnqEu92Fr1Mu5mxKOzY.woff2
https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu4mxK.woff2 → tests/fonts/gstatic/roboto/v30/KFOmCnqEu92Fr1Mu4mxK.woff2

Перехватываем запросы

Для того, чтобы запросы на загрузку шрифтом реально не выполнялись, в playwright предусмотрен метод page.route(url, handler). Например, мы можем полностью блокировать загрузку данных с домена fonts.gstatic.com:

await context.route('https://fonts.googleapis.com/**', (route, request) => {
  return route.abort()
})

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

Для этого используем метод request.fulfill({ body: 'подставной ответ', contentType: 'text/html' })

Загружаем содержимое локального файла

Для того, чтобы подставить в качестве ответа содержимое локальных файлов, можно использовать стандартный в поставке Node.JS модуль fs, а имя файла будем получать исходя из адреса перехваченного запроса.

Начнём с файлов шрифтов, с ними чуть проще. Адрес запроса имеет вид https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu4mxK.woff2. Мы хотим на основании него сформировать путь к файлу tests/fonts/gstatic/roboto/v30/KFOmCnqEu92Fr1Mu4mxK.woff2. При этом будем иметь ввиду, что в будущем, возможно, нам и понадобятся другие аналогичные файлы. В связи с этим постараемся написать универсальный код, чтобы для добавления нового файла его достаточно было сохранить в нужной директории, не меняя самого кода. Это можно сделать, обрезая имя домена и постоянную часть пути:

const fontName = request.url().replace('https://fonts.gstatic.com/s/', '')
const fileName = `tests/files/gstatic/${fontName}`

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

await context.route('https://fonts.gstatic.com/s/**', (route, request) => {
  const fontName = request.url().replace('https://fonts.gstatic.com/s/', '')
  const fileName = `tests/files/gstatic/${fontName}`
  fs.readFile(fileName, (err, body) => {
    if (err) {
      console.log(`${fileName} not found`)
      route.abort()
    } else {
      route.fulfill({ body, contentType: 'font/woff2' })
    }
  })
})

Используем регулярные выражения для обработки перехватываемого адреса

Чуть сложнее будет с css-файлами, потому что тут не получится так просто обрезать имя домена, потому что путь здесь не похож на имя файла.

Нам нужно путь вида https://fonts.googleapis.com/css2?family=Roboto&display=swap преобразовать в имя файла tests/fonts/googleapis/Roboto.css.

Для этого нам придётся использовать регулярное выражение.

Для получения слова Roboto (которое будет идентифицировать наш css-файл), напишем следующий код:

request.url().match(/family=(\w+)/)[1]

Тогда весь код для перехвата запроса будет выглядеть так:

await context.route('https://fonts.googleapis.com/css2?*', (route, request) => {
  const fontName = request.url().match(/family=(\w+)/)
  if (fontName && fontName[1]) {
    const fileName = `tests/files/googleapis/${fontName[1]}.css`
    fs.readFile(fileName, (err: any, body: any) => {
      if (err) {
        console.log(`${fileName} not found`)
        route.abort()
      } else {
        route.fulfill({ body, contentType: 'text/css; charset=utf-8' })
      }
    })
  } else {
    route.abort()
  }
})

Проблемы такого подхода

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

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

Более универсальным решением может быть изначально не загружать данные с внешних CDN, а вместо этого загружать данные локально. Например, те же шрифты можно установить через npm. В этом случае у нас будет только одна точка отказа, а также не будет всей этой проблемы с тестами.