上一篇介绍了 HPC Dashboard 自动化测试项目的整体情况,这一篇开始记录实际的测试开发过程。第一天,我下载了 Docker,把测试项目在本地跑了起来。
开发的第二天,我选择从 Upload 模块入手,使用 Playwright + TypeScript 编写 E2E 测试。
Upload 模块表面上只是上传文件、填写表单、点击提交,实际上还涉及 Job 创建、Recurring Job 管理,以及提交后在其他页面验证结果,因此我决定最先攻克它。
1. 从业务流程拆解测试场景
HPC Dashboard 主要用于管理和分析高性能计算任务。
用户首先需要上传任务相关的数据,系统才能创建 Job,并在其他页面展示任务信息和分析结果。
Upload 页面涉及两类文件:
- PBS Log:任务的运行记录。
- Metrics JSON:任务运行过程中的性能数据。
此外,用户还可以选择将 Job 关联到 Recurring Job:既可以创建新的 Recurring Job,也可以选择已有的 Recurring Job。

根据这些业务流程,我设计了五条测试:
| 用例 | 测试场景 | 预期结果 |
|---|---|---|
| Test1 | 只上传 Job Log,缺少 Metrics JSON | Submit 按钮禁用 |
| Test2 | 两份文件都已上传,但没有填写 Job Name | Submit 按钮禁用 |
| Test3 | 提交普通 Job | 提交成功,Jobs 页面能找到新 Job |
| Test4 | 提交 Job,同时创建新的 Recurring Job | 提交成功,Profile 页面能找到 Recurring Job |
| Test5 | 创建 Recurring Job,再将第二个 Job 关联到它 | Recurring Analysis 页面显示 2 runs |
这里既包含表单校验 ,也包含跨页面业务验证。
尤其是后三条,我不希望测试只停留在"点击 Submit 按钮"这一步,而是继续检查数据是否出现在对应的业务页面中。
2. 为什么这样设计测试用例?
设计这五条用例时,我主要考虑三个方面:
1. 正向与反向测试(Positive / Negative Testing)
Test1、Test2 验证缺少必填信息时无法提交;Test3--5 覆盖正常提交和 Recurring Job 业务流程。
2. 验证业务结果,而不只是点击成功
普通 Job 提交后,在 Jobs 页面检查任务是否出现;创建 Recurring Job 后,在 Profile 页面验证;关联第二个 Job 后,在 Recurring Analysis 页面检查是否显示 2 runs。
3. 优先覆盖核心流程(Risk-Based Testing)
先覆盖表单校验、普通 Job 创建和 Recurring Job 关联等主要场景,再逐步补充异常输入、错误处理和边界条件。
这五条用例是第一阶段的核心业务覆盖,并不代表完整的测试覆盖率。后续还需要完善测试稳定性、数据清理和更多异常场景。
2. 先封装重复的文件上传操作
五条测试中,多次需要上传同样的两份文件,因此我先把这个操作提取为 uploadFiles()。
ts
//nth(0):选择第一个上传控件,索引从 0 开始。
//nth(1):选择第二个上传控件。
async function uploadFiles(page: Page) {
const fileInputs = page.locator('input[type="file"]');
await fileInputs.nth(0).setInputFiles(
'test-data/demo_job_12.txt'
);
await fileInputs.nth(1).setInputFiles(
'test-data/demo_metrics_13.jsonl'
);
}
这里使用了 Playwright 的 setInputFiles() 方法。
页面存在两个文件上传控件,因此通过 nth(0) 和 nth(1) 分别指定文件。
这种写法比较直接,但依赖两个上传控件的排列顺序。一旦页面结构发生变化,后续可以考虑改用更明确的定位方式。
3. 验证必填项:不满足条件时禁止提交
前两条测试属于表单校验。
例如 Test1,只上传第一份文件,不上传 Metrics JSON:
ts
test('Test1: Submit is disabled without Metrics JSON', async ({ page }) => {
await openUploadPage(page);
const fileInputs = page.locator('input[type="file"]');
await fileInputs.nth(0).setInputFiles(
'test-data/demo_job_12.txt'
);
await expect(
page.getByRole('button', { name: 'Submit Job data' })
).toBeDisabled();
});
Test2 则上传两份文件,但不填写 Job Name:
ts
test('Test2: Submit disabled without Job Name', async ({ page }) => {
await openUploadPage(page);
await uploadFiles(page);
await expect(
page.getByRole('button', { name: 'Submit Job data' })
).toBeDisabled();
});
这两条测试的重点并不在于验证错误提示,而在于确认表单不完整时,提交按钮确实处于禁用状态。
4. 提交成功后,继续验证业务结果
对于正常提交,我封装了 submitJob(),统一处理 Job Name、Recurring Job 选项和提交操作。
其中一个关键断言是:
ts
const submitButton = page.getByRole('button', {
name: 'Submit Job data'
});
await expect(submitButton).toBeEnabled();
await submitButton.click();
await expect(
page.getByText('Success! Stored job successfully', {
exact: false
})
).toBeVisible({ timeout: 15000 });
这里验证了提交按钮可用,以及提交后出现成功消息。
但成功提示并不能完全代替后续的业务验证。
因此,在 Test3 中,我还会进入 Jobs 页面,确认刚创建的 Job 可以被找到:
ts
await page.getByText('Jobs', { exact: true }).click();
await expect(
page.getByText(jobName, { exact: true })
).toBeVisible({ timeout: 1000 });
为了避免测试数据名称重复,我使用时间戳生成 Job Name:
ts
const jobName = `playwright-job-${Date.now()}`;
Test4 采用类似思路,只不过提交时还会创建新的 Recurring Job,随后进入 Profile 页面检查对应名称是否出现。
5. Test5:验证两个 Job 关联到同一个 Recurring Job
五条测试中,Test5 的业务流程相对复杂。
它需要先创建一个新的 Recurring Job,然后再次上传文件,将第二个 Job 关联到刚创建的 Recurring Job。
调用公共方法时,通过 mode 区分新建和选择已有对象:
ts
await submitJob(page, jobName, {
name: recurringName,
mode: 'new'
});
// 第二次上传
await uploadFiles(page);
await submitJob(page, jobName2, {
name: recurringName,
mode: 'existing'
});
第二次提交后,测试进入 Recurring Analysis 页面,找到对应的 Recurring Job 卡片,并验证运行次数。
ts
await page.getByText('Recurring Analysis', {
exact: true
}).click();
const card = page.locator('article').filter({
has: page.getByRole('heading', {
name: recurringName,
exact: true
})
});
await expect(
card.getByText('2 runs', { exact: true })
).toBeVisible();
这里使用 filter({ has: ... }),先根据标题找到目标卡片,再在卡片内部验证 2 runs。
这样可以避免页面上存在多个 Recurring Job 时,错误地检查其他卡片中的运行次数。
相比只验证提交成功消息,这条测试进一步验证了两个 Job 是否被归入同一个 Recurring Job。
6. 第一批测试带来的问题
Day 2 结束时,五条 Upload 测试都在本地通过了。
但在开发过程中,也暴露出一些值得继续改进的问题:
- Locator 精确性:部分定位方式会匹配多个元素,需要限定范围。
- 等待方式 :代码中使用了
waitForTimeout(1000),后续需要考虑用具体的页面状态代替固定等待。 - 测试数据:每次提交都会生成 Job 或 Recurring Job,后续需要统一管理和清理。
- 执行稳定性 :早期测试执行不够稳定,因此暂时关闭了
fullyParallel,并继续排查原因。
这些问题当时没有一次性全部解决,而是在后续几天逐步完善。
小结
这次 Upload 模块自动化让我对 E2E 测试设计有了更具体的认识。
测试不能只证明操作被执行,还应该验证操作产生的业务结果。
从表单必填项,到 Job 提交,再到多个 Job 与同一个 Recurring Job 的关联,这五条用例逐步覆盖了 Upload 模块的主要业务流程。
下一篇,我会继续记录开发中遇到的 Locator 问题,以及如何在复杂页面中准确找到需要操作的元素。