如何使用高级TypeScript模式构建可扩展的QA框架

TypeScript自动化QA(7部分系列)
🗺️ TypeScript第一步:自动化QA实用路线图
🗂️ 如何在TypeScript中使用数组和对象构建强大的QA自动化脚本
🔀 如何掌握TypeScript基础逻辑以构建更智能的自动化QA
🏷️ TypeScript自定义类型QA自动化实用指南
🏛️ 停止编写脆弱的测试:可扩展TypeScript POM蓝图
⚡️ 停止编写不稳定的测试:Playwright异步基础指南
🛡️ 如何使用高级TypeScript模式构建可扩展的QA框架
🤖 在我们的上一篇文章中,我们掌握了异步性,为我们的测试框架奠定了坚实的基础。但坚实的基础只是开始。要构建真正可扩展和可维护的自动化套件,我们需要一个强大的架构框架。这个框架就是高级的、表达性强的类型系统。
本文适合高级用户。我们将超越基础类型,向你展示如何利用TypeScript的高级模式来消除整类bug,甚至在你运行单个测试之前
你将学习构建世界级QA框架的五个基本模式:
枚举(Enums):管理固定常量集合并防止拼写错误
泛型(Generics):编写高度可重用、类型安全的代码,如API客户端
Zod & z.infer:在运行时验证API响应并消除手动类型定义
typeof:直接从运行时对象创建类型
工具类型(Utility Types):创建现有类型的灵活变体,无需重复代码
掌握这些概念将把你的框架从简单的测试集合转变为可扩展、自文档化和有弹性的资产。
前置要求:
TypeScript基础
基本TypeScript类型(string、number、boolean)
使用数组和对象构建数据
使用函数编写可重用代码
使用循环自动化操作(for、while)
使用条件语句做出决策(if/else)
联合类型和字面量类型
类型别名和接口
Playwright项目:你应该对如何编写和运行测试有基本了解
你理解页面对象模型(类)的目的
你理解并正确使用async/await、Promise.all和try/catch
🤔 问题:"简单"框架的隐藏成本
当框架很小时,保持清洁很容易。但随着它的增长,"简单"的解决方案会引入隐藏成本,使代码库变得脆弱且难以维护。
魔法字符串:使用原始字符串如'ADMIN''POST'作为角色或请求方法是定时炸弹。单个拼写错误('ADMNIN')会创建一个TypeScript无法捕获的bug。
重复逻辑:为每个API端点编写新的fetch函数(fetchUserfetchArticlefetchComment)会创建维护噩梦。认证逻辑的更改需要更新数十个文件。
类型漂移:你手动为API响应编写TypeScript interface。API发生变化——字段被重命名或删除。你的测试仍然编译,但在运行时失败,因为你的类型撒谎了。
负载混淆:你对创建新文章和更新标题都使用相同的大Article类型。这令人困惑且效率低下。
这些小问题会累积,导致一个难以重构且令人恐惧的框架。
🛠️ 解决方案,第1部分:枚举实现坚如磐石的常量
消除"魔法字符串"bug的最快方法是使用枚举。枚举是命名常量的受限集合。
脆弱的方式(魔法字符串)
想象一个分配角色的函数。字符串中的拼写错误会静默通过TypeScript。
  1. // 🚨 这是"之前" - 等待发生的bug 🚨
  2. function assignRole(username: string, role: string) {
  3. // 如果有人传递'admn'或'editorr'怎么办?
  4. console.log(`分配角色: ${role} 给 ${username}`);
  5. }

  6. // 简单的拼写错误意味着这段代码在逻辑上有缺陷,但TS不知道
  7. assignRole('idavidov', 'admnin'); // 糟糕!
typescript
健壮的修复(枚举)
通过定义UserRole枚举,我们强制开发者从有效选项列表中选择,给我们自动完成和编译时安全性。
  1. // ✅ 这是"之后" - 类型安全且清晰 ✅
  2. export enum UserRole {
  3. ADMIN = 'admin',
  4. EDITOR = 'editor',
  5. VIEWER = 'viewer',
  6. }

  7. function assignRole(username: string, role: UserRole) {
  8. console.log(`分配角色: ${role} 给 ${username}`);
  9. }

  10. // 1. 无拼写错误:如果UserRole.ADMNIN存在,TS会抛出错误
  11. // 2. 自动完成:你的编辑器会建议ADMIN、EDITOR或VIEWER
  12. assignRole('idavidov', UserRole.ADMIN);
typescript
经验法则:如果你有一组固定的相关字符串,使用枚举。
🚀 解决方案,第2部分:泛型实现最大可重用性
泛型可以说是编写可扩展代码最强大的功能。它们允许你编写可以与任何类型一起工作的函数,而不牺牲类型安全性。完美的用例是可重用的API请求函数。
重复的方式(无泛型)
没有泛型,你最终会为每个API端点编写几乎相同的函数。
  1. // 🚨 "之前" - 大量重复代码 🚨
  2. async function fetchArticle(id: string): Promise<ArticleResponse> {
  3. const res = await request.get(`/api/articles/${id}`);
  4. return await res.json();
  5. }

  6. async function fetchUser(id: string): Promise<UserResponse> {
  7. const res = await request.get(`/api/users/${id}`);
  8. return await res.json();
  9. }
typescript
可扩展的修复(泛型函数)
我们可以编写一个函数apiRequest,它可以获取任何资源并返回强类型响应。魔法是`<T>占位符。
  1. // ✅ "之后" - 单个、可重用、类型安全的函数 ✅

  2. // 我们定义一个接受泛型类型`T`的函数
  3. // 它返回一个Promise,其主体将是类型`T`
  4. async function apiRequest<T = unknown>({
  5. method,
  6. url,
  7. }: // ... 其他参数
  8. ApiRequestParams): Promise<ApiRequestResponse<T>> {
  9. const response = await apiRequestOriginal({
  10. /* ... 实现细节 ... */
  11. });
  12. return {
  13. status: response.status,
  14. body: response.body as T, // 我们告诉TS在这里信任我们
  15. };
  16. }
typescript
当我们调用这个函数时,我们指定T应该是什么。
  1. // T变成ArticleResponse。'body'常量现在完全类型化了!
  2. const { body } = await apiRequest<ArticleResponse>({
  3. method: 'GET',
  4. url: 'api/articles/my-article',
  5. });

  6. // 我们现在可以使用完整的自动完成访问body.article.title
  7. console.log(body.article.title);
typescript
🛡️ 解决方案,第3部分:Zod & z.infer实现端到端安全性
我们已经解决了代码重复。现在让我们解决"类型漂移"。你数据的最终真相来源是API本身。Zod是一个TypeScript库,它让我们创建一个在运行时验证真实API响应的模式,而z.infer让我们从同一个模式创建编译时TypeScript类型。
一个模式。两个好处。零漂移。
首先,为你的API响应定义一个模式。这是一个描述数据形状的实际JavaScript对象。
  1. // 1. 用Zod定义运行时模式
  2. export const ArticleResponseSchema = z.object({
  3. article: z.object({
  4. slug: z.string(),
  5. title: z.string(),
  6. description: z.string(),
  7. body: z.string(),
  8. author: z.object({
  9. username: z.string(),
  10. // ... 其他作者字段
  11. }),
  12. }),
  13. });
typescript
接下来,使用z.infer的魔法从模式创建TypeScript类型,无需任何额外工作。
  1. // 2. 直接从模式推断TypeScript类型
  2. export type ArticleResponse = z.infer<typeof ArticleResponseSchema>;

  3. // 无需手动编写这个!
  4. // interface ArticleResponse {
  5. // article: {
  6. // slug: string;
  7. // title: string;
  8. // ...
  9. // }
  10. // }
typescript
现在,在你的测试中,你两者都使用。Zod模式验证实时数据,推断的类型给你自动完成和静态分析。
  1. // 3. 在测试中使用两者以获得100%的信心
  2. await test.step('验证创建文章', async () => {
  3. const { status, body } = await apiRequest<ArticleResponse>({
  4. /* ... 请求参数 ... */
  5. });

  6. // 运行时检查:API响应是否匹配我们的模式?
  7. // 如果API发生变化,这将失败,立即捕获bug
  8. expect(ArticleResponseSchema.parse(body)).toBeTruthy();

  9. // 编译时安全性:我们现在可以自信地使用'body'
  10. const articleId = body.article.slug;
  11. expect(status).toBe(201);
  12. });
typescript
🛠️ 解决方案,第4部分:typeof和工具类型实现灵活性
有时你需要简单、临时对象的类型,或者你需要为API更新负载等事情创建现有类型的轻微变体。
typeof:从运行时对象创建类型
如果你的代码中有一个常量对象,你可以使用typeof创建一个完美匹配其形状的类型。
  1. // 定义默认负载的运行时对象
  2. const defaultArticlePayload = {
  3. article: {
  4. title: '我的默认标题',
  5. description: '一篇很棒的文章',
  6. body: '内容...',
  7. tagList: ['testing', 'playwright'],
  8. },
  9. };

  10. // 创建一个完全匹配对象形状的类型
  11. type ArticlePayload = typeof defaultArticlePayload;

  12. // 这个函数现在只接受具有该确切形状的对象
  13. function createArticle(payload: ArticlePayload) {
  14. // ...
  15. }
typescript
PartialPick:从其他类型创建类型
使用我们从Zod的ArticleResponse类型,如果我们想更新文章怎么办?我们可能只需要发送几个字段,而不是全部。工具类型让我们可以即时创建这些变体。
Partial<T>:使T中的所有字段可选
Pick<T, K>:通过从T中选择几个键K创建新类型
  1. // 我们的原始类型,其中所有字段都是必需的
  2. // type Article = { slug: string; title: string; body: string; ... }
  3. type Article = z.infer<typeof ArticleResponseSchema>['article'];

  4. // ✅ 场景1:更新负载,其中任何字段都是可选的
  5. // 这创建了一个类型如:{ title?: string; body?: string; ... }
  6. type UpdateArticlePayload = Partial<Article>;

  7. // ✅ 场景2:表示唯一标识符的类型
  8. // 这创建了类型:{ slug: string; }
  9. type ArticleLocator = Pick<Article, 'slug'>;
typescript
5个模式总结
🚀 你的使命:构建一个牢不可破的框架
你现在已经装备了在最健壮、企业级测试自动化框架中使用的模式。回到你自己的项目,寻找升级的机会:
寻找魔法字符串:找到任何硬编码字符串('admin''success''POST')并用枚举替换它们
模式化你的端点:选择你最关键的API端点,为它编写Zod模式,并使用z.infer生成类型。在测试中应用它
用泛型重构:识别两个或更多重复函数(如API调用)并将它们重构为单个、可重用的泛型函数
创建智能负载:寻找POSTPATCH请求,使用PartialPick等工具类型创建精确、最小的负载
采用这些高级模式是将你的测试框架从简单工具转变为强大、可扩展和真正可靠的工程资产的最后一步。
实际应用示例
1. 完整的API客户端实现
  1. // 定义API请求参数类型
  2. interface ApiRequestParams {
  3. method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
  4. url: string;
  5. body?: unknown;
  6. headers?: Record<string, string>;
  7. }

  8. // 定义API响应类型
  9. interface ApiRequestResponse<T> {
  10. status: number;
  11. body: T;
  12. headers: Record<string, string>;
  13. }

  14. // 泛型API请求函数
  15. async function apiRequest<T = unknown>({
  16. method,
  17. url,
  18. body,
  19. headers = {},
  20. }: ApiRequestParams): Promise<ApiRequestResponse<T>> {
  21. const response = await fetch(url, {
  22. method,
  23. headers: {
  24. 'Content-Type': 'application/json',
  25. ...headers,
  26. },
  27. body: body ? JSON.stringify(body) : undefined,
  28. });

  29. const responseBody = await response.json();

  30. return {
  31. status: response.status,
  32. body: responseBody as T,
  33. headers: Object.fromEntries(response.headers.entries()),
  34. };
  35. }
typescript
2. 完整的Zod模式示例
  1. import { z } from 'zod';

  2. // 用户模式
  3. export const UserSchema = z.object({
  4. id: z.number(),
  5. username: z.string().min(3),
  6. email: z.string().email(),
  7. role: z.enum(['admin', 'editor', 'viewer']),
  8. createdAt: z.string().datetime(),
  9. updatedAt: z.string().datetime(),
  10. });

  11. // 文章模式
  12. export const ArticleSchema = z.object({
  13. id: z.number(),
  14. slug: z.string(),
  15. title: z.string().min(1),
  16. description: z.string(),
  17. body: z.string(),
  18. author: UserSchema,
  19. tagList: z.array(z.string()),
  20. createdAt: z.string().datetime(),
  21. updatedAt: z.string().datetime(),
  22. });

  23. // 文章响应模式
  24. export const ArticleResponseSchema = z.object({
  25. article: ArticleSchema,
  26. });

  27. // 文章列表响应模式
  28. export const ArticlesResponseSchema = z.object({
  29. articles: z.array(ArticleSchema),
  30. articlesCount: z.number(),
  31. });

  32. // 推断类型
  33. export type User = z.infer<typeof UserSchema>;
  34. export type Article = z.infer<typeof ArticleSchema>;
  35. export type ArticleResponse = z.infer<typeof ArticleResponseSchema>;
  36. export type ArticlesResponse = z.infer<typeof ArticlesResponseSchema>;
typescript
3. 完整的测试示例
  1. import { test, expect } from '@playwright/test';
  2. import { apiRequest } from '../utils/api-client';
  3. import {
  4. ArticleResponseSchema,
  5. ArticleResponse,
  6. UpdateArticlePayload
  7. } from '../schemas/article';

  8. test.describe('文章API测试', () => {
  9. test('应该创建新文章', async () => {
  10. const newArticle = {
  11. article: {
  12. title: '测试文章',
  13. description: '这是一个测试文章',
  14. body: '文章内容...',
  15. tagList: ['testing', 'automation'],
  16. },
  17. };

  18. const { status, body } = await apiRequest<ArticleResponse>({
  19. method: 'POST',
  20. url: '/api/articles',
  21. body: newArticle,
  22. });

  23. // 运行时验证
  24. expect(ArticleResponseSchema.parse(body)).toBeTruthy();
  25. // 编译时类型安全
  26. expect(status).toBe(201);
  27. expect(body.article.title).toBe('测试文章');
  28. expect(body.article.author.username).toBeDefined();
  29. });

  30. test('应该更新文章', async () => {
  31. const updatePayload: UpdateArticlePayload = {
  32. title: '更新的标题',
  33. description: '更新的描述',
  34. };

  35. const { status, body } = await apiRequest<ArticleResponse>({
  36. method: 'PUT',
  37. url: '/api/articles/test-article',
  38. body: { article: updatePayload },
  39. });

  40. expect(status).toBe(200);
  41. expect(body.article.title).toBe('更新的标题');
  42. });
  43. });
typescript
4. 工具类型的高级用法
  1. // 从现有类型创建新类型
  2. type CreateArticlePayload = Pick<Article, 'title' | 'description' | 'body' | 'tagList'>;
  3. type ArticleSummary = Pick<Article, 'id' | 'title' | 'description' | 'author'>;
  4. type ArticleUpdatePayload = Partial<CreateArticlePayload>;

  5. // 组合工具类型
  6. type RequiredArticleFields = Required<Pick<Article, 'title' | 'body'>>;
  7. type OptionalArticleFields = Partial<Omit<Article, 'id' | 'createdAt' | 'updatedAt'>>;

  8. // 条件类型
  9. type ApiResponse<T> = {
  10. data: T;
  11. status: 'success' | 'error';
  12. message?: string;
  13. };

  14. type SuccessResponse<T> = ApiResponse<T> & { status: 'success' };
  15. type ErrorResponse = ApiResponse<never> & { status: 'error'; message: string };
typescript
🙏🏻 感谢阅读! 构建健壮、可扩展的自动化框架是一个最好一起进行的旅程。如果你觉得这篇文章有帮助,考虑加入一个不断增长的QA专业人士社区🚀,他们热衷于掌握现代测试。
通过注册通讯加入社区并获取最新文章和技巧。