В создании автоматизированных e2e-тестов для сложных систем есть много трудностей. Одна из них — “идеальное” окружение. Это окружение должно быть полностью под вашим контролем и включать многие (если не все) характеристики окружения разработки или продакшн-окружения.
На практике это часто не так. Окружение требует регулярного контроля, сброса данных и настройки требований для каждого теста “на лету”.
В случае онлайн-приложений, основанных на REST-архитектуре и запросах API, мы можем обойти эту проблему и приводить систему в необходимое состояние перед каждым тестом или набором тестов.
В этом примере я воспользуюсь Playwright с модулем запросов, который он предоставляет для отправки вызовов API.
Допустим, приложение, которое мы тестируем, представляет собой простой магазин с товарами. Для авторизации используется технология JWT (веб-токен в формате JSON). Следовательно, каждый последующий вызов после входа в систему должен включать данный токен.
Чтобы некоторые тесты сработали, на складе должен быть определенный продукт. Следовательно, пользователь должен сначала пройти аутентификацию, получить токен и воспользоваться им для пополнения запасов этого определенного продукта.
Подход в стиле “дешево и сердито”
Очевидным кажется такой способ действий: отправка вызовов внутри блока beforeAll
, который выполняется перед всеми тестами в тестовом файле или наборе тестов. Вот как это сделать:
test.beforeAll(async (request) => {
let response = await request.post(`https://someapp.com/api/login`, {
headers: this.standardHeaders,
data: {
username: "USERNAME",
password: "PASSWORD",
},
})
let { accessToken, refreshToken } = JSON.parse(await response.text())
let setupCall = await request.post(`https://someapp.com/product/00001`, {
headers: {
authorization: `Bearer ${accessToken}`,
},
data: {
stock: 10,
},
})
if (setupCall.status() == 200) {
console.log("Product is now setup correctly")
}
})
test("Test that requires product with id 00001 in stock", async (request) => {
// ЗДЕСЬ ОТРАБАТЫВАЕТ САМ ТЕСТ
})
У этой стратегии есть несколько недостатков. Наиболее очевидный из них — неудобочитаемость, поскольку тест наполнен ненужным кодом, который имеет мало общего с его основной задачей. Вторая проблема — избыточность кода, поскольку функциональность добавления продукта потребует повторять данный код для каждого теста. Для проведения десяти тестов пользователь должен войти в систему десять раз, что приводит к потере времени и ресурсов и вызывает проблемы с производительностью.
Классический ООП-подход и проблемы с асинхронностью
Как и многие другие архитектурные проблемы, эту можно решить с помощью концепций ООП. Класс, который предоставляет данные для теста, обеспечивает стандартизированный, повторяемый и масштабируемый способ доступа к API.
Класс будет выглядеть примерно так:
class SetupCalls {
constructor(request) {
this.baseUrl = "https://someapp.com"
this.accessToken = null
this.refreshToken = null
}
async getToken(username, password) {
this.request = await request.newContext()
let response = await this.request.post(`${this.baseUrl}/api/login`, {
headers: this.standardHeaders,
data: {
username: username,
password: password,
},
})
let { accessToken, refreshToken } = JSON.parse(await response.text())
this.accessToken = accessToken
this.refreshToken = refreshToken
}
async setStock(productId) {
let setupCall = await this.request.post(`${this.baseUrl}/product/${productId}`, {
headers: {
authorization: `Bearer ${this.accessToken}`,
},
data: {
stock: 10,
},
})
if (setupCall.status() == 200) {
console.log("Product is now setup correctly")
}
}
}
Теперь, когда функции эффективно инкапсулированы, довольно просто понять, что делает каждая из них. Несколько вызовов можно выполнить с использованием одного и того же токена, поскольку он является общим для всего класса, что экономит время входа в систему. Посмотрим, как теперь выглядит тест:
test.beforeAll(async () => {
let setupCalls = new SetupCalls()
await setupCalls.getToken('John', 'john123')
await setupCalls.setStock("00001")
await setupCalls.setStock("00002")
await setupCalls.setStock("00003")
})
test("Test that requires product with id 00001 in stock", async (request) => {
// ЗДЕСЬ БУДЕТ ТЕСТ
})
Несмотря на то что реализация кажется превосходной, остаются некоторые проблемы. Пользователь должен понимать специфику работы класса setup
, прежде чем использовать его. Если метод getToken
не будет вызван раньше всех остальных, тесты завершатся неудачей, поскольку токен не будет заполнен.
Вызов токена getToken
в идеале должен выполняться в конструкторе, однако конструктор не может быть асинхронной функцией. Таким образом, решением скорее всего может стать использование Promises.
constructor() {
this.baseUrl = "https://someapp.com"
let {accessToken, refreshToken} = Promise.resolve(getToken()).then(res => {
// сделать что-то с токеном
});
}j
Однако это изменяет нормальное поведение конструктора и требует тщательного сохранения контекста. Такой подход подрывает цель использования async-await, которая заключается в том, чтобы код был читабельным и простым.
Решение — ООП с фабричными функциями
Вместо традиционного конструктора мы можем инициализировать объект с помощью стандартной статической функции (метода) и использовать асинхронные методы внутри этой функции, не теряя ни одного из преимуществ последней реализации.
Статические методы вызываются не для экземпляров класса, а для самого класса. Превращение конструктора в приватный гарантирует, что пользователь инициализирует объект с помощью init
, а не конструктора по умолчанию. С помощью метода init
классу предоставляется только необходимая информация, такая как токен доступа, и с этой информацией создается новый экземпляр класса.
Взглянем на код:
class SetupCalls {
/**
* @private
*/
constructor(config) {
this.baseUrl = "https://someapp.com"
this.accessToken = config.accessToken
this.refreshToken = config.refreshToken
}
static async init(username, password) {
let config = await this.getToken(username, password)
return new SetupCalls(config)
}
async getToken(username, password) {
this.request = await request.newContext()
let response = await this.request.post(`https://someapp.com/api/login`, {
data: {
username: username,
password: password,
},
})
return JSON.parse(await response.text())
}
async setStock(productId) {....}
}
Теперь тест значительно лучше оптимизирован и использует асинхронность в любой ситуации:
test.beforeAll(async () => {
let setupCalls = await SetupCalls.init("John", "John1213!")
await setupCalls.setStock("00001")
})
test("Test that requires product with id 00001 in stock", async (request) => {
// TEST ITSELF BEGINS
})
Последние штрихи и улучшения
Часто можно настроить данные еще до запуска тестов. Флаг globalSetup
в playwright.config.js
позволяет указать местоположение для файла, который обрабатывает глобальную настройку:
const config = {
globalSetup: require.resolve("./globalSetup"),
testDir: "./tests",
timeout: 80 * 1000,
expect: {
timeout: 5000,
},
...
}
Теперь, когда мы создали класс, файл может вызвать его и настроить весь набор данных в одной функции. Мы даже можем настроить параллельный запуск всех вызовов, чтобы ускорить тесты, поскольку вызовы для продуктов не связаны между собой. Вот как будет выглядеть файл globalConfig.js
:
module.exports = async (config) => {
let setupCalls = await SetupCalls.init(config.username, config.password)
let dataset = ["00001", "00002", "00003", "00004"]
const responses = await Promise.all(
dataset.map(async (id) => {
const res = await setupCalls.setStock(id)
})
)
console.log(responses)
}
Заключение
Если вы проводите тестирование API или просто используете API для подготовки данных или другой автоматизации, всегда полезно организовать вызовы таким образом, чтобы обеспечить гибкость при сохранении удобочитаемости и структуры.
Из-за своей асинхронной природы JavaScript иногда может сбивать с толку. Таким образом, простота выполнения при сохранении производительности может означать разницу между просто хорошим и отличным кодом.
Читайте также:
- Какой метод глубокого клонирования в JavaScript наиболее эффективный — исследование
- 2 инструмента для автоматизации тестирования производительности на стороне клиента
- Мониторинг сайта: просто, но эффективно
Читайте нас в Telegram, VK и Дзен
Перевод статьи Nikola Dimic: How To Structure API Calls for Automation Tests in Playwright and JavaScript