有四种可以自动化的软件测试的基本类型。
- 用实时数据(集成测试)对用户流的端到端测试
- 用模拟数据(隔离用户界面测试)对用户流的端到端测试
- 单个组件的孤立测试
- 单元测试
本文将重点介绍端到端测试,但首先是一些有关其他类型测试的注释。
剧作家对React,Vue和Svelte中的测试组件具有实验支持。我还没有能够将其集成到Redbit React项目之一,因为剧作家使用了与我们不同的捆绑器。 (剧作家在我们使用WebPack时使用Vite。)我们的许多组件都依赖于特定的WebPack配置和一些无法轻松在VITE中复制的自定义插件。虽然我确定这是一个可以克服的问题,但我不确定这是值得的。构建页面可能需要更少的时间来渲染与常规剧作家API一起测试的组件,并且也可以作为开发人员的参考库。
剧作家并不是首先是单位测试跑步者,因此我不会在这种情况下进行讨论。 Redbit在Web项目中使用开玩笑或单元测试。玩笑对剧作家使用类似的断言语法,这有助于减少开发人员的认知开销。
端到端用户流程测试
用户流的端到端测试旨在模拟用户在使用应用程序时执行的操作,并验证这些操作是否具有预期的结果。这是剧作家的主要目的。
自动化用户流
编写自动化的端到端测试通常涉及确定用户在应用程序中执行的操作顺序,将其手动转换为代码,然后添加断言以验证预期的操作实际执行。例如,您可以导航到应用程序中的某个页面,并验证浏览器的位置设置为预期的URL,然后单击链接,并验证浏览器的位置是否已更改为链接的URL。在途中,您可能想验证某些消息或其他组件在屏幕上可见。
剧作家提供了一种test generator,该test generator从长期用户流中的自动测试中汲取了很多繁琐的方法。它将在Chromium实例中启动您的应用程序,并在第二个过程中记录您在应用程序中执行的所有操作。您可以通过应用程序中的用户流进行导航,并且测试生成器将操作转换为代码。测试生成器还将添加一些基本断言,例如测试单击链接时浏览器位置更新为预期的URL。然后,您可以将测试代码复制到您的项目并手动添加其他断言。
测试生成器在我自动化的流量方面运行良好,除了它未能捕获浏览器返回按钮。除非修改以恢复缺失的导航操作,否则这将导致未能失败的测试。即使测试发电机的代码输出需要一些工作,我认为这仍然是胜利。修复测试所需的努力可能远远远远远远少于从头开始写这些测试所需的努力。
使用模拟数据测试
运行测试生成器时,它会启动您的应用程序,该应用可能会由API或其他一些数据源支持。数据源可能是生产环境(但希望不是),也可能是远程测试环境,或者是本地计算机上的开发环境。无论哪种方式,您都在使用实时数据进行测试。实时数据的问题在于,它通常会发生变化,当它更改时,您的测试可能会失败。考虑以下情况:
- 导航到呈现产品列表的页面
/products
。 - 单击要导航到
/products/<id>
的第一个项目,该项目显示有关单个产品的详细信息。在此示例中,<id>
表示分配给产品数据库记录的id
属性。 - 断言详细信息页面URL包含正确的
id
属性。
剧作家测试生成器将编写基于渲染数据执行这些操作的代码,这看起来更像是这样:
- 导航到
/products
。 - 单击包含文本“ Cuisinart食品处理器”的链接。
- 断言详细信息页面URL为
/products/34
。
test('Products list and detail navigation flow', async ({ page }) => {
// Navigate to the products page:
await page.goto('/products');
await expect(page).toHaveURL('/products');
// Click the "Cuisinart Food Processor" link:
await page.getByRole('link', { name: 'Cuisinart Food Processor' }).click();
await expect(page).toHaveURL('/products/34');
// Navigate back to the products page:
await page.goBack();
await expect(page).toHaveURL('/products');
});
只要列表中的第一个产品是“ Cuisinart食品加工机”,ID为34。如果产品具有不同的ID,则测试将失败。有两种解决方案。
嘲笑API响应
最简单的解决方案是通过模拟数据实现API请求。剧作家通过将请求拦截到特定路线来简单,干净地做到这一点:
test('Products list and detail navigation flow', async ({ page }) => {
// Fulfill the products list API request with mock data.
await page.route('/api/products', (route) => {
return route.fulfill({
status: 200,
body: JSON.stringify([
{ id: 34, name: 'Cuisinart Food Processor' },
{ id: 75, name: 'Vitamix Blender' },
]),
});
});
// Navigate to the products page:
await page.goto('/products');
await expect(page).toHaveURL('/products');
// Click the "Cuisinart Food Processor" link:
await page.getByRole('link', { name: 'Cuisinart Food Processor' }).click();
await expect(page).toHaveURL('/products/34');
// Navigate back to the products page:
await page.goBack();
await expect(page).toHaveURL('/products');
});
在此示例中,对/api/products
的请求将充满JSON序列化的测试数据。 (假定我们正在嘲笑发送JSON响应的API,但是您可以用适合您的应用程序替换测试数据。)
有了可靠的测试数据,您可以确保产品列表中的第一项永远不会更改,除非您更改它。只要应用程序的行为保持不变,您的测试就会始终通过。这并不是说您应该使用实时数据测试 。如果您正在运行集成测试,则可能需要验证一系列复杂的操作,在此过程中,必须将数据写入,从数据库中读取和删除。但是,在许多情况下,您只会关心测试系统的一个部分(例如用户界面),并且您应该能够孤立地进行测试。
基于结构的测试,而不是满足
返回我们之前看过的测试用例,我们可以看到它依赖于特定内容来在页面上找到第一个产品:
- 导航到
/products
。 - 单击包含文本“ Cuisinart食品处理器”的链接。
- 断言详细信息页面URL为
/products/34
。
test('Products list and detail navigation flow', async ({ page }) => {
// Navigate to the products page:
await page.goto('/products');
await expect(page).toHaveURL('/products');
// Click the "Cuisinart Food Processor" link:
await page.getByRole('link', { name: 'Cuisinart Food Processor' }).click();
await expect(page).toHaveURL('/products/34');
// Navigate back to the list:
await page.goBack();
await expect(page).toHaveURL('/products');
});
请注意,此测试不在乎“ Cuisinart食品加工机”链接的何处。我们期望它会在产品列表中,但测试并未验证这一点。它可以在页面上的任何地方。这对您来说可能很重要,但值得指出。
我们可以将此顺序重写以依赖页面结构,而是:
- 导航到
/products
。 - 从产品列表中的第一个链接中提取详细页面URL。
- 单击相同的链接。
- 断言详细信息页面URL与步骤2中的URL匹配。
以这种方式编写的测试将是含义的,而目标元素则精确:
test('Products list and detail navigation flow', async ({ page }) => {
// Navigate to the products page:
await page.goto('/products');
await expect(page).toHaveURL('/products');
// Get the first link in the products list and extract the detail page url:
const link = page.locator('.ul.products > li > a').nth(0);
const url = await link.evaluate((node) => node.getAttribute('href'));
// Navigate to the product detail page:
await link.click();
await expect(page).toHaveURL(url);
// Navigate back to the products list:
await page.goBack();
await expect(page).toHaveURL('/products');
});
交易是您需要应用程序的技术知识来基于结构编写测试。这种方法可能不可行,具体取决于您组织中的谁负责测试。它不是替代可靠的测试数据,而是提供了另一种使您的测试更准确和弹性的方法。
测试API请求
您可能有验证您的应用程序提出特定API请求的案例。例如,您可能需要测试当用户选择过滤器时,将提出新的API请求,并且请求使用他们选择的过滤器配置。剧作家允许您等待请求并获取有关它的信息。以下示例验证了是否为GET
请求使用某些分页和排序参数:
test('A request is made for the first page of products in descending order of creation', async ({ page }) => {
const request = await page.waitForRequest('/products**');
await page.waitForLoadState('networkidle');
// Verify that the request was configured correctly:
const url = new URL(request.url);
// Expect a GET request to /products?offset=0&limit=10&orderBy=createdAt&order=desc
await expect(request.method).toEqual('GET');
await expect(url.searchParams.get('offset')).toEqual('0');
await expect(url.searchParams.get('limit')).toEqual('10');
await expect(url.searchParams.get('orderBy')).toEqual('createdAt');
await expect(url.searchParams.get('order')).toEqual('desc');
});
如果请求是用任何其他方法或offset
,limit
,orderBy
和order
参数的任何其他值进行的,请求将失败。
注意:URL末尾的通配符(**
)告诉播放器不管查询参数如何,都匹配/products
的任何请求。没有它,只有在没有任何查询参数的情况下进行的请求才会匹配。
测试渲染精度
如果您具有可靠且稳定的测试数据来源(请参见嘲笑API响应),则可以测试您的数据是根据要求渲染的。该过程如下。
对于测试数据中的每个项目:
- 准备测试数据的属性,因为您希望它们已被渲染。例如,如果您有一个数字格式化器将数字(
25.00
)作为货币字符串('$25.00'
)呈现,请将其应用于数字。 (如果您的用户界面是本地化的,请确保您的测试使用与应用程序相同的语言环境。如果您要彻底,则可能需要对每个语言环境进行单独的测试。) - 找到与DOM树中每个属性相对应的元素并提取渲染值。
- 断言渲染值等于格式的值。
import { productsTestData } from './test-data';
import { formatCurrency } from './utilities/currency';
test('Products list renders as expected', async ({ page }) => {
await page.goto('/products');
await expect(page).toHaveURL('/products');
for (let i = 0; i < productsTestData.length; i++) {
const product = productsTestData[i];
// Format the expected values.
const expectedLink = `/products/${product.id}`;
const expectedName = product.name;
const expectedPrice = formatCurrency(product.price);
// Get the DOM node that contains the product.
const node = await page.locator('.ul.products > li').nth(i).evaluate((node) => node);
// Get the rendered link href.
const renderedLink = node.querySelector('a').getAttribute('href');
// Get the rendered product name and price.
// Trim the values to ignore any whitespace introducted during rendering.
const renderedName = node.querySelector('.product-name').textContent.trim();
const renderedPrice = node.querySelector('.product-price').textContent.trim();
// Assert that the rendered values equal the expected values.
expect(renderedLink).toEqual(expectedLink);
expect(renderedName).toEqual(expectedName);
expect(renderedPrice).toEqual(expectedPrice);
}
});
您应该测试多少?
您的端到端测试的复杂性或多或少会反映您的应用程序的复杂性。您渲染的信息越多,您必须测试的信息越多。上面的示例仅希望将链接,产品名称和产品价格作为特定字符串渲染。它不能测试布局和样式是否正确,甚至可以看到元素。可以编写更全面的测试,但是这样做需要更多的开发时间。您的测试可能会更频繁地无效,这将导致更多失败。有要考虑的费用和问题要问:
- 您的产品有多成熟?您的用户界面是否经常进行设计更改还是稳定?
- 您能负担得起更复杂的测试和更频繁的失败对您开发团队的速度的影响吗?
- 测试开发和维护的累积成本在什么时候超过了人类质量检查测试的成本?
- 增加的时间压力在什么时候导致开发人员放弃和删除失败的测试,而不是修复它们,使您的投资毫无意义?
这些问题没有正确的答案。您选择的测试策略应取决于组织的优先级,并且可能会随着时间的推移而发展。早期启动可能会优先考虑对用户流的高级测试,并将渲染测试限制为关键路径。随着组织的成熟及其能力的提高,他们可能会开始为其产品的其他部分增加测试,或者使现有测试更加全面,或两者兼而有之。
请记住,自动测试的目标是减少人类的时间和精力,并提高一致性。最高价值的自动化测试是必须经常运行的测试,并且需要对细节的关键路径上的复杂流进行最关注。首先确定减少这些领域的劳动力的机会,并逐渐从那里增加测试覆盖率。