剧作家是新的柏树
多年来, 柏树 是用于端到端测试的行业标准工具,尤其是在JavaScript世界中。我喜欢该工具的出色API和惊人的文档。交互式UI很棒,使调试测试变得有趣而容易。但是街区上有一个新的很酷的孩子-Playwright。它比柏树有很多好处,但是由于已经有很多文章,因此我们不会在本文中介绍它。相反,我们将涵盖不那么流行的模拟技术 - 录制和答复请求。
标准API模拟
标准API模拟通常会截获真实的API调用,并提供传递给route.fulfill()
的模拟JSON。您可以使用以下代码轻松地使用剧作家进行操作:
await page.route('https://testapi.com/api/football-players', async route => {
const json = {
[
'Kylian Mbappe',
'Erling Haaland',
'Arturo Vidal',
'Kamil Grosicki'
]
};
await route.fulfill({ json });
});
此方法非常好,对于大多数情况下,这将是最佳选择。但是在某些情况下,我们可能想根据此记录请求并运行测试。
我为什么要录制请求?
让我们想象以下情况。我们有一个电子商务网站,上面有许多过滤器。有些过滤器是孤立的,有些过滤器取决于其他过滤器。有效负载可能包括200个查询参数。您将如何用单个嘲笑的JSON对其进行测试?以这种方式测试数百种组合是不可能的。这就是为什么运行测试,记录所有API请求,存储有关请求的全部信息,然后在将来使用该数据运行测试的整个信息可能是个好主意。为什么不使用真正的API而不是嘲笑?好吧,执行邮政,put,路径或删除之类的请求是很愚蠢的,这些请求本质上是在实际DB上仅仅用于运行测试的。那简单的查询呢?有时,API具有一定的限制,或者其定价基于许多请求。我们可能希望避免在测试中使用真实API的原因有很多。让我们快速了解如何使用剧作家实现它。
示例应用程序
我已经构建了一个简单的VUE应用程序,它使用sunsetsunrise API获取有关日落和日出的数据。
这是代码:
<template>
<div class="sun-hours">
<h1 data-testid="sun-hours">Sun hours</h1>
<input type="text" placeholder="latitude" v-model="lat">
<input type="text" placeholder="longitude" v-model="lng">
<button @click="getSunHours" data-testid="search-btn">Search</button>
<div v-if="result">
<h2 data-testid="results-header">Results:</h2>
<div>
<span>Sunrise: </span>
<span>{{result.sunrise}}</span>
</div>
<div>
<span>Sunset: </span>
<span>{{result.sunset}}</span>
</div>
<div>
<span>Golden hour: </span>
<span>{{result.golden_hour}}</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {onMounted, ref} from "vue";
const lat = ref('38.907192');
const lng = ref('-77.036873');
const result = ref<SunHoursResponse | null>(null);
interface SunHoursResponse {
sunrise: string,
sunset: string,
golden_hour: string,
}
async function getSunHours() {
const response = await fetch(`https://api.sunrisesunset.io/json?lat=${lat.value}&lng=${lng.value}&timezone=UTC&date=today`);
result.value = (await response.json()).results;
}
</script>
<style scoped>
.sun-hours {
display: flex;
flex-direction: column;
gap: 20px;
}
</style>
简单测试
测试只是用纬度和经度填充表格,然后单击按钮以获取API的数据。之后,我们执行一些检查,仅此而已:
//sunhours.spec.ts
import { test, expect } from '@playwright/test';
test('checks if API returns result', async ({browser }) => {
await page.goto('http://localhost:5173/');
await expect(page.getByTestId('sun-hours')).toHaveText('Sun hours');
await page.getByTestId('search-btn').click();
await expect(page.getByTestId('results-header')).toHaveText('Results:');
});
录制请求
让我们假设此API是付费产品,并且有更多类似的测试来检查参数和边缘案例的不同组合。让我们配置我们的测试以通过在测试开始时添加以下几行记录初始请求:
const context = await browser.newContext({
recordHar: { path: 'requests.har', mode: 'full', urlFilter: '**/api.sunrisesunset.io/**' }
});
const page = await context.newPage();
并在测试文件末尾关闭上下文:
await context.close();
我们正在创建新的浏览器上下文,以记录请求并将其存储在HAR文件中。我们将选项对象传递给使用称为recordHar
的选项。
- 路径 - 存储所有记录的文件的路径
- 模式 - 可以将其设置为“完整”或“最小”。 “完整”选项将存储有关请求的每个细节,而“最小”更简洁。
- urlfilter-我们可以指定应在其录制的模式
整个测试文件看起来像这样:
import { test, expect } from '@playwright/test';
test('checks if API returns result', async ({browser }) => {
const context = await browser.newContext({
recordHar: { path: 'requests.har', mode: 'full', urlFilter: '**/api.sunrisesunset.io/**' }
});
const page = await context.newPage();
await page.goto('http://localhost:5173/');
await expect(page.getByTestId('sun-hours')).toHaveText('Sun hours');
await page.getByTestId('search-btn').click();
await expect(page.getByTestId('results-header')).toHaveText('Results:');
await context.close();
});
通过以下命令运行测试后
npx playwright test sunhours.spec.ts
我们可以看到创建了一个新的文件requests.har
。这是这个文件的外观:
{
"log": {
"version": "1.2",
"creator": {
"name": "Playwright",
"version": "1.31.1"
},
"browser": {
"name": "firefox",
"version": "109.0"
},
"pages": [
{
"startedDateTime": "2023-03-05T21:38:57.390Z",
"id": "page@e9d0544dbdcdaf2e7ef5e43c0e00e4eb",
"title": "Vite + Vue + TS",
"pageTimings": {
"onContentLoad": 164,
"onLoad": 176
}
}
],
"entries": [
{
"startedDateTime": "2023-03-05T21:38:57.654Z",
"time": 182.55100000000002,
"request": {
"method": "GET",
"url": "https://api.sunrisesunset.io/json?lat=38.907192&lng=-77.036873&timezone=UTC&date=today",
"httpVersion": "HTTP/2.0",
"cookies": [],
"headers": [
{ "name": "Host", "value": "api.sunrisesunset.io" },
{ "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/109.0" },
{ "name": "Accept", "value": "*/*" },
{ "name": "Accept-Language", "value": "en-US" },
{ "name": "Accept-Encoding", "value": "gzip, deflate, br" },
{ "name": "Referer", "value": "http://localhost:5173/" },
{ "name": "Origin", "value": "http://localhost:5173" },
{ "name": "Connection", "value": "keep-alive" },
{ "name": "Sec-Fetch-Dest", "value": "empty" },
{ "name": "Sec-Fetch-Mode", "value": "cors" },
{ "name": "Sec-Fetch-Site", "value": "cross-site" }
],
"queryString": [
{
"name": "lat",
"value": "38.907192"
},
{
"name": "lng",
"value": "-77.036873"
},
{
"name": "timezone",
"value": "UTC"
},
{
"name": "date",
"value": "today"
}
],
"headersSize": 376,
"bodySize": 0
},
"response": {
"status": 200,
"statusText": "OK",
"httpVersion": "HTTP/2.0",
"cookies": [],
"headers": [
{ "name": "date", "value": "Sun" },
{ "name": "date", "value": "05 Mar 2023 21:38:57 GMT" },
{ "name": "content-type", "value": "application/json" },
{ "name": "cf-ray", "value": "7a358247496cb90f-AMS" },
{ "name": "access-control-allow-origin", "value": "*" },
{ "name": "age", "value": "6553" },
{ "name": "cache-control", "value": "s-max-age=1320" },
{ "name": "cache-control", "value": "max-age=1320" },
{ "name": "last-modified", "value": "Sun" },
{ "name": "last-modified", "value": "05 Mar 2023 17:59:18 GMT" },
{ "name": "link", "value": "<https://sunrisesunset.io/wp-json/>; rel=\"https://api.w.org/\"" },
{ "name": "strict-transport-security", "value": "max-age=15552000" },
{ "name": "vary", "value": "Accept-Encoding" },
{ "name": "cf-cache-status", "value": "HIT" },
{ "name": "fastcgi-cache", "value": "BYPASS" },
{ "name": "x-content-type-options", "value": "nosniff" },
{ "name": "x-frame-options", "value": "SAMEORIGIN" },
{ "name": "x-robots-tag", "value": "noindex" },
{ "name": "x-ua-compatible", "value": "IE=edge" },
{ "name": "x-xss-protection", "value": "1; mode=block" },
{ "name": "report-to", "value": "{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v3?s=vnbhiEqRfzhoBgtaOEfESVjShLW%2B3OMVpAeGJ%2FxwC7RLPvxSvjPNaaFdQTtgAtl3Jw6E%2FkXC43DvmB8oyCxrok7%2BRa2M6JYR0l2Ri9el79wOg3DQEtwC%2BwKjpRR7iP4CrZDM64Pr3g%3D%3D\"}]" },
{ "name": "report-to", "value": "\"group\":\"cf-nel\"" },
{ "name": "report-to", "value": "\"max_age\":604800}" },
{ "name": "nel", "value": "{\"success_fraction\":0" },
{ "name": "nel", "value": "\"report_to\":\"cf-nel\"" },
{ "name": "nel", "value": "\"max_age\":604800}" },
{ "name": "server", "value": "cloudflare" },
{ "name": "content-encoding", "value": "br" },
{ "name": "alt-svc", "value": "h3=\":443\"; ma=86400" },
{ "name": "alt-svc", "value": "h3-29=\":443\"; ma=86400" },
{ "name": "X-Firefox-Spdy", "value": "h2" }
],
"content": {
"size": 266,
"mimeType": "application/json",
"compression": 80,
"text": "{\"results\":{\"sunrise\":\"11:38:10 AM\",\"sunset\":\"11:04:37 PM\",\"first_light\":\"10:09:42 AM\",\"last_light\":\"12:33:05 AM\",\"dawn\":\"11:11:29 AM\",\"dusk\":\"11:31:19 PM\",\"solar_noon\":\"5:21:24 PM\",\"golden_hour\":\"10:28:53 PM\",\"day_length\":\"11:26:27\",\"timezone\":\"UTC\"},\"status\":\"OK\"}"
},
"headersSize": 1114,
"bodySize": 186,
"redirectURL": "",
"_transferSize": 1214
},
"cache": {},
"timings": { "dns": 0.207, "connect": 76.133, "ssl": 46.679, "send": 0, "wait": 59.464, "receive": 0.068 },
"pageref": "page@e9d0544dbdcdaf2e7ef5e43c0e00e4eb",
"serverIPAddress": "188.114.97.13",
"_serverPort": 443,
"_securityDetails": {
"protocol": "TLS 1.3",
"subjectName": "*.sunrisesunset.io",
"issuer": "GTS CA 1P5",
"validFrom": 1677028638,
"validTo": 1684804637
}
}
]
}
}
它存储有关请求和响应的所有信息。如果测试执行100个不同的API调用,则将100个请求记录到HAR文件中。
重播请求
如果我们要在下次运行测试时使用记录的请求,我们要做的就是添加以下行:
await page.routeFromHAR('requests.har');
此代码行将检查所有可能的API调用,并且params匹配了记录在requests.har
文件中的请求之一,则将由我们的har
文件的请求代替了对真实API的请求。
完整代码:
import { test, expect } from '@playwright/test';
test('checks if API returns result', async ({page, browser }) => {
await page.goto('http://localhost:5173/');
await page.routeFromHAR('requests.har');
await expect(page.getByTestId('sun-hours')).toHaveText('Sun hours');
await page.getByTestId('search-btn').click();
await expect(page.getByTestId('results-header')).toHaveText('Results:');
});
概括
此技术是标准模拟的一种更灵活的方式,尤其是在我们想基于不同参数测试许多响应的情况下。剧作家提供了一种使用HAR文件记录和重播请求的方便方法。有关更多详细信息,您可以检查专用文档部分here。