Как отслеживать и перехватывать сетевые запросы и ответы
Обзор
Данный рецепт работает только при использовании Chrome DevTools Protocol (CDP).
Читайте подробности в разделе «Как использовать CDP в Testplane»
В CDP имеются домены Fetch и Network, с помощью которых можно получить полный доступ ко всем сетевым запросам и ответам. При использовании Webdriver-протокола нам бы пришлось писать отдельный proxy-сервер и весь трафик направлять через него.
В webdriverio для работы с сетевыми запросами существует метод mock, который использует API домена Fetch.
С помощью этого метода мы можем:
- замокать запрос к ресурсу и вернуть свои данные;
- отменить запрос, вернув необходимую ошибку;
- модифицировать ответ от ресурса;
- перенаправить с запрошенного ресурса на какой-то другой ресурс;
- замокать ресурс и, например, собрать информацию о том, сколько раз этот ресурс вызывался и какой ответ он возвращал.
Давайте попробуем написать тесты с использованием этого API и покрыть разные кейсы. Сразу уточним, что все графические изображения с процессом выполнения тестов замедлены в 2 раза, так как тесты локально выполняются очень быстро и что-то разглядеть довольно проблематично.
Пример 1: мокаем запрос к google.com и возвращаем свой ответ
it("should mock google.com", async function () {
// Мокаем поход на google.com
const mock = await this.browser.mock("https://google.com");
// Возвращаем строку "Hello, world!" вместо ответа от сайта.
// Опция "fetchResponse" отвечает за то, нужно ли делать запрос
// на замоканный ресурс, по умолчанию - true
mock.respond("Hello, world!", { fetchResponse: false });
await this.browser.url("https://google.com");
});
По графическому изображению видно, что мы вернули свой текст, при этом в строке браузера отображается как будто мы выполнили переход на google.com. Также видно, что мы не замокали фавиконку и она приехала снаружи. Этот же самый пример мы можем написать с использованием API puppeteer'а, для этого в webdriverio реализована команда getPuppeteer():
it("should mock google.com using puppeteer api", async function () {
// Получаем инстанс puppeteer'а
const puppeteer = await this.browser.getPuppeteer();
// Получаем первую открытую страницу (считаем, что она активная в данный момент)
const [page] = await puppeteer.pages();
// Активируем перехват всех запросов
await page.setRequestInterception(true);
page.on("request", async request => {
if (request.url() !== "https://google.com/") {
// Если урл запроса не матчится на https://google.com/,
// то выполняем запрос (т. е. не перехватываем его)
return request.continue();
}
// отвечаем своими данными
return request.respond({ body: "Hello, world!" });
});
// Здесь можно было бы вызвать и "page.goto('https://google.com')", но лучше вызывать "url",
// так как в большинстве плагинов есть обертки команды "url", добавляющие дополнительную
// логику. Например, в testplane добавляется урл в мету.
await this.browser.url("https://google.com");
});
Хардкорный вариант с использованием CDP напрямую
А теперь представим, что в puppeteer еще нет API для мока запросов, но это уже реализовано в домене Fetch CDP. В этом случае воспользуемся методом этого домена через общение с CDP-сессией напрямую. Для этого в puppeteer есть метод CDPSession.send():
it("should mock google.com using cdp fetch domain", async function () {
// Получаем инстанс puppeteer'а
const puppeteer = await this.browser.getPuppeteer();
// Получаем первую открытую страницу (считаем, что она активная в данный момент)
const [page] = await puppeteer.pages();
// Создаем CDP-сессию
const client = await page.target().createCDPSession();
// Включаем возможность перехватить запрос с помощью подписки на событие "requestPaused"
await client.send("Fetch.enable");
client.on("Fetch.requestPaused", event => {
const {
request: { url },
requestId,
responseHeaders,
} = event;
if (url !== "https://google.com/") {
// Если урл запроса не матчится на https://google.com/,
// то выполняем запрос (т. е. не перехватываем его)
return client.send("Fetch.continueRequest", { requestId });
}
// Подменяем ответ на свой и упаковываем его в base64
return client.send("Fetch.fulfillRequest", {
requestId,
responseCode: 200,
responseHeaders,
body: Buffer.from("Hello, world!", "utf8").toString("base64"),
});
});
await this.browser.url("https://google.com");
});
Очевидно, что при использовании API webdriverio для мока запросов код получается сильно короче, но API webdriverio сильно ограничен и для более сложных кейсов необходимо использовать API puppeteer'а. При этом в самом puppeteer'е тоже может не быть API для каких-то новых методов или доменов CDP. Поэтому в редких случаях может пригодиться общение по CDP напрямую с помощью CDPSession.send().
Пример 2: отменяем запрос за логотипом гугла
it("should abort request to logo on google.com", async function () {
// В качестве урла можно использовать маску
const mock = await this.browser.mock("https://www.google.com/images/**/*.png");
// Кидаем ошибку "ERR_FAILED" при загрузке ресурса, сматчившегося на маску мока
mock.abort("Failed");
await this.browser.url("https://google.com");
});
По графическому изображению видно, что логотип не отобразился и в логе присутствует ошибка net::ERR_FAILED
. Такое решение может быть удобно для отключения каких-то скриптов, которые мешают быстрому выполнению теста. Например, можно отключить сбор аналитики.
Пример 3: при загрузке google.com берем ответ из фикстуры
it("should mock google.com and return answer from fixture", async function () {
// Мокаем поход на google.com
const mock = await this.browser.mock("https://google.com");
// Указываем путь, откуда нужно взять фикстуру, а с помощью
// "fetchResponse: false" говорим, что выполнять реальный поход не нужно
mock.respond("./fixtures/my-google.com.html", { fetchResponse: false });
await this.browser.url("https://google.com");
});
По графическому изображению видно, что вместо выдачи google.com отобразились данные из нашей фикстуры.
Пример 4: перенаправляем запрос с google.com на yandex.ru
it("should redirect from google.com to yandex.ru", async function () {
// Мокаем поход на google.com
const mock = await this.browser.mock("https://google.com");
// Для редиректа необходимо просто указать урл
mock.respond("https://yandex.ru");
await this.browser.url("https://google.com");
});
Пример 5: модифицируем ответ от google.com в реальном времени
В puppeteer все еще не реализован API для удобного изменения ответа. Про это есть issue#1191. Но такая возможность уже поддержана в CDP. Webdriverio использует CDP напрямую через [puppeteer][puppeteer] и, соответственно, в webdriverio это работает.
Заменим в ответе от google.com все строки содержащие Google
на Yandex
:
it("should modify response from google.com", async function () {
// Тут нужно мокать именно с www, так как переход на google.com
// возвращает ответ 301 без body и перенаправляет нас на www
const mock = await this.browser.mock("https://www.google.com");
mock.respond(req => {
// С помощью регулярки заменяем "Google" на "Yandex"
return req.body.replace(/Google/g, "Yandex");
});
await this.browser.url("https://google.com");
});