如何编写依赖服务器时间的E2E测试?(Angular+Spring Boot+Playwright)
解决方案:基于Playwright的时间依赖型接口E2E测试实现
核心思路
要验证依赖后端时间的接口行为,关键是隔离后端的系统时间依赖,同时确保测试专属代码不会侵入生产环境。我们通过Spring的抽象时钟(Clock)替换硬编码的系统时间获取,配合仅测试环境生效的时间控制接口,再用Playwright完成端到端流程验证。
第一步:后端改造(Spring Boot)
1. 用Clock抽象替换直接时间获取
将业务代码中的硬编码时间判断改为依赖Clock,这样测试时可以注入自定义时钟:
@Service public class OrderService { private final Clock clock; // 构造注入Clock,生产环境自动使用系统默认时钟 public OrderService(Clock clock) { this.clock = clock; } public String cancelOrder(int id) { LocalTime now = LocalTime.now(clock); LocalTime twoPM = LocalTime.of(14, 0); LocalTime threePM = LocalTime.of(15, 0); if (now.isAfter(twoPM) && now.isBefore(threePM)) { return "Canceled"; } else { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot cancel order outside 2-3 PM"); } } }
2. 环境隔离的时钟配置
- 生产环境配置:默认使用系统时钟,仅在非test环境生效
@Configuration @Profile("!test") public class ProductionClockConfig { @Bean public Clock systemClock() { return Clock.systemDefaultZone(); } }
- 测试环境配置:提供可修改的可变时钟,仅在test环境生效
@Configuration @Profile("test") public class TestClockConfig { @Bean public MutableClock mutableClock() { return new MutableClock(Clock.systemDefaultZone()); } // 自定义可变时钟类,支持动态设置时间 public static class MutableClock extends Clock { private Clock delegate; public MutableClock(Clock delegate) { this.delegate = delegate; } public void setClock(Clock clock) { this.delegate = clock; } @Override public ZoneId getZone() { return delegate.getZone(); } @Override public Clock withZone(ZoneId zone) { return delegate.withZone(zone); } @Override public Instant instant() { return delegate.instant(); } } }
3. 测试专属时间控制接口
添加仅在test环境暴露的接口,用于Playwright测试时修改后端时间:
@RestController @Profile("test") @RequestMapping("/test/clock") public class TestClockController { private final TestClockConfig.MutableClock mutableClock; public TestClockController(TestClockConfig.MutableClock mutableClock) { this.mutableClock = mutableClock; } @PostMapping("/set-time") public void setTime(@RequestParam LocalTime time) { // 设置为当天的指定时间 LocalDate today = LocalDate.now(); Instant targetInstant = LocalDateTime.of(today, time) .atZone(ZoneId.systemDefault()) .toInstant(); mutableClock.setClock(Clock.fixed(targetInstant, ZoneId.systemDefault())); } @PostMapping("/reset") public void resetClock() { mutableClock.setClock(Clock.systemDefaultZone()); } }
第二步:Playwright E2E测试编写(TypeScript示例)
通过调用测试接口设置后端时间,再执行前端操作验证结果:
import { test, expect } from '@playwright/test'; test.describe('Order Cancel Functionality', () => { // 每个测试前后重置后端时钟,避免测试污染 test.beforeEach(async ({ request }) => { await request.post('/test/clock/reset'); }); test.afterEach(async ({ request }) => { await request.post('/test/clock/reset'); }); test('shouldCancelOrder', async ({ page, request }) => { // 设置后端时间为14:30(允许取消的时间段) await request.post('/test/clock/set-time', { params: { time: '14:30:00' } }); // 前端操作流程:打开订单页 → 点击取消按钮 await page.goto('/orders'); await page.getByRole('button', { name: 'Cancel Order' }).click(); // 验证取消成功提示 await expect(page.getByText('Canceled')).toBeVisible(); }); test('shouldNotCancelOrder', async ({ page, request }) => { // 设置后端时间为15:30(不允许取消的时间段) await request.post('/test/clock/set-time', { params: { time: '15:30:00' } }); // 前端操作流程:打开订单页 → 点击取消按钮 await page.goto('/orders'); await page.getByRole('button', { name: 'Cancel Order' }).click(); // 验证错误提示 await expect(page.getByText('Cannot cancel order outside 2-3 PM')).toBeVisible(); }); });
确保测试代码不进入生产环境的关键措施
- Spring Profile隔离:所有测试专属类(
TestClockConfig、TestClockController)都标记@Profile("test"),生产环境不会加载这些类。 - 构建阶段排除:Maven/Gradle的生产打包配置中,排除test相关的代码和依赖。
- 网关/安全拦截:生产环境的API网关或Spring Security配置,直接拦截所有
/test/前缀的请求,双重保险。
内容的提问来源于stack exchange,提问作者Tristate
相关产品推荐
相关产品推荐

