如何使用高级TypeScript模式构建可扩展的QA框架
liuian 2025-09-04 11:56 5 浏览
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函数(fetchUser、fetchArticle、fetchComment)会创建维护噩梦。认证逻辑的更改需要更新数十个文件。
- 类型漂移:你手动为API响应编写TypeScript interface。API发生变化——字段被重命名或删除。你的测试仍然编译,但在运行时失败,因为你的类型撒谎了。
- 负载混淆:你对创建新文章和更新标题都使用相同的大Article类型。这令人困惑且效率低下。
这些小问题会累积,导致一个难以重构且令人恐惧的框架。
解决方案,第1部分:枚举实现坚如磐石的常量
消除"魔法字符串"bug的最快方法是使用枚举。枚举是命名常量的受限集合。
脆弱的方式(魔法字符串)
想象一个分配角色的函数。字符串中的拼写错误会静默通过TypeScript。
// 这是"之前" - 等待发生的bug
function assignRole(username: string, role: string) {
// 如果有人传递'admn'或'editorr'怎么办?
console.log(`分配角色: ${role} 给 ${username}`);
}
// 简单的拼写错误意味着这段代码在逻辑上有缺陷,但TS不知道
assignRole('idavidov', 'admnin'); // 糟糕!
健壮的修复(枚举)
通过定义UserRole枚举,我们强制开发者从有效选项列表中选择,给我们自动完成和编译时安全性。
// 这是"之后" - 类型安全且清晰
export enum UserRole {
ADMIN = 'admin',
EDITOR = 'editor',
VIEWER = 'viewer',
}
function assignRole(username: string, role: UserRole) {
console.log(`分配角色: ${role} 给 ${username}`);
}
// 1. 无拼写错误:如果UserRole.ADMNIN存在,TS会抛出错误
// 2. 自动完成:你的编辑器会建议ADMIN、EDITOR或VIEWER
assignRole('idavidov', UserRole.ADMIN);
经验法则:如果你有一组固定的相关字符串,使用枚举。
解决方案,第2部分:泛型实现最大可重用性
泛型可以说是编写可扩展代码最强大的功能。它们允许你编写可以与任何类型一起工作的函数,而不牺牲类型安全性。完美的用例是可重用的API请求函数。
重复的方式(无泛型)
没有泛型,你最终会为每个API端点编写几乎相同的函数。
// "之前" - 大量重复代码
async function fetchArticle(id: string): Promise<ArticleResponse> {
const res = await request.get(`/api/articles/${id}`);
return await res.json();
}
async function fetchUser(id: string): Promise<UserResponse> {
const res = await request.get(`/api/users/${id}`);
return await res.json();
}
可扩展的修复(泛型函数)
我们可以编写一个函数apiRequest,它可以获取任何资源并返回强类型响应。魔法是<T>占位符。
// "之后" - 单个、可重用、类型安全的函数
// 我们定义一个接受泛型类型`T`的函数
// 它返回一个Promise,其主体将是类型`T`
async function apiRequest<T = unknown>({
method,
url,
}: // ... 其他参数
ApiRequestParams): Promise<ApiRequestResponse<T>> {
const response = await apiRequestOriginal({
/* ... 实现细节 ... */
});
return {
status: response.status,
body: response.body as T, // 我们告诉TS在这里信任我们
};
}
当我们调用这个函数时,我们指定T应该是什么。
// T变成ArticleResponse。'body'常量现在完全类型化了!
const { body } = await apiRequest<ArticleResponse>({
method: 'GET',
url: 'api/articles/my-article',
});
// 我们现在可以使用完整的自动完成访问body.article.title
console.log(body.article.title);
解决方案,第3部分:Zod & z.infer实现端到端安全性
我们已经解决了代码重复。现在让我们解决"类型漂移"。你数据的最终真相来源是API本身。Zod是一个TypeScript库,它让我们创建一个在运行时验证真实API响应的模式,而z.infer让我们从同一个模式创建编译时TypeScript类型。
一个模式。两个好处。零漂移。
首先,为你的API响应定义一个模式。这是一个描述数据形状的实际JavaScript对象。
// 1. 用Zod定义运行时模式
export const ArticleResponseSchema = z.object({
article: z.object({
slug: z.string(),
title: z.string(),
description: z.string(),
body: z.string(),
author: z.object({
username: z.string(),
// ... 其他作者字段
}),
}),
});
接下来,使用z.infer的魔法从模式创建TypeScript类型,无需任何额外工作。
// 2. 直接从模式推断TypeScript类型
export type ArticleResponse = z.infer<typeof ArticleResponseSchema>;
// 无需手动编写这个!
// interface ArticleResponse {
// article: {
// slug: string;
// title: string;
// ...
// }
// }
现在,在你的测试中,你两者都使用。Zod模式验证实时数据,推断的类型给你自动完成和静态分析。
// 3. 在测试中使用两者以获得100%的信心
await test.step('验证创建文章', async () => {
const { status, body } = await apiRequest<ArticleResponse>({
/* ... 请求参数 ... */
});
// 运行时检查:API响应是否匹配我们的模式?
// 如果API发生变化,这将失败,立即捕获bug
expect(ArticleResponseSchema.parse(body)).toBeTruthy();
// 编译时安全性:我们现在可以自信地使用'body'
const articleId = body.article.slug;
expect(status).toBe(201);
});
解决方案,第4部分:typeof和工具类型实现灵活性
有时你需要简单、临时对象的类型,或者你需要为API更新负载等事情创建现有类型的轻微变体。
typeof:从运行时对象创建类型
如果你的代码中有一个常量对象,你可以使用typeof创建一个完美匹配其形状的类型。
// 定义默认负载的运行时对象
const defaultArticlePayload = {
article: {
title: '我的默认标题',
description: '一篇很棒的文章',
body: '内容...',
tagList: ['testing', 'playwright'],
},
};
// 创建一个完全匹配对象形状的类型
type ArticlePayload = typeof defaultArticlePayload;
// 这个函数现在只接受具有该确切形状的对象
function createArticle(payload: ArticlePayload) {
// ...
}
Partial和Pick:从其他类型创建类型
使用我们从Zod的ArticleResponse类型,如果我们想更新文章怎么办?我们可能只需要发送几个字段,而不是全部。工具类型让我们可以即时创建这些变体。
- Partial<T>:使T中的所有字段可选
- Pick<T, K>:通过从T中选择几个键K创建新类型
// 我们的原始类型,其中所有字段都是必需的
// type Article = { slug: string; title: string; body: string; ... }
type Article = z.infer<typeof ArticleResponseSchema>['article'];
// 场景1:更新负载,其中任何字段都是可选的
// 这创建了一个类型如:{ title?: string; body?: string; ... }
type UpdateArticlePayload = Partial<Article>;
// 场景2:表示唯一标识符的类型
// 这创建了类型:{ slug: string; }
type ArticleLocator = Pick<Article, 'slug'>;
5个模式总结
你的使命:构建一个牢不可破的框架
你现在已经装备了在最健壮、企业级测试自动化框架中使用的模式。回到你自己的项目,寻找升级的机会:
- 寻找魔法字符串:找到任何硬编码字符串('admin'、'success'、'POST')并用枚举替换它们
- 模式化你的端点:选择你最关键的API端点,为它编写Zod模式,并使用z.infer生成类型。在测试中应用它
- 用泛型重构:识别两个或更多重复函数(如API调用)并将它们重构为单个、可重用的泛型函数
- 创建智能负载:寻找POST或PATCH请求,使用Partial或Pick等工具类型创建精确、最小的负载
采用这些高级模式是将你的测试框架从简单工具转变为强大、可扩展和真正可靠的工程资产的最后一步。
实际应用示例
1. 完整的API客户端实现
// 定义API请求参数类型
interface ApiRequestParams {
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
url: string;
body?: unknown;
headers?: Record<string, string>;
}
// 定义API响应类型
interface ApiRequestResponse<T> {
status: number;
body: T;
headers: Record<string, string>;
}
// 泛型API请求函数
async function apiRequest<T = unknown>({
method,
url,
body,
headers = {},
}: ApiRequestParams): Promise<ApiRequestResponse<T>> {
const response = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
...headers,
},
body: body ? JSON.stringify(body) : undefined,
});
const responseBody = await response.json();
return {
status: response.status,
body: responseBody as T,
headers: Object.fromEntries(response.headers.entries()),
};
}
2. 完整的Zod模式示例
import { z } from 'zod';
// 用户模式
export const UserSchema = z.object({
id: z.number(),
username: z.string().min(3),
email: z.string().email(),
role: z.enum(['admin', 'editor', 'viewer']),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
});
// 文章模式
export const ArticleSchema = z.object({
id: z.number(),
slug: z.string(),
title: z.string().min(1),
description: z.string(),
body: z.string(),
author: UserSchema,
tagList: z.array(z.string()),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
});
// 文章响应模式
export const ArticleResponseSchema = z.object({
article: ArticleSchema,
});
// 文章列表响应模式
export const ArticlesResponseSchema = z.object({
articles: z.array(ArticleSchema),
articlesCount: z.number(),
});
// 推断类型
export type User = z.infer<typeof UserSchema>;
export type Article = z.infer<typeof ArticleSchema>;
export type ArticleResponse = z.infer<typeof ArticleResponseSchema>;
export type ArticlesResponse = z.infer<typeof ArticlesResponseSchema>;
3. 完整的测试示例
import { test, expect } from '@playwright/test';
import { apiRequest } from '../utils/api-client';
import {
ArticleResponseSchema,
ArticleResponse,
UpdateArticlePayload
} from '../schemas/article';
test.describe('文章API测试', () => {
test('应该创建新文章', async () => {
const newArticle = {
article: {
title: '测试文章',
description: '这是一个测试文章',
body: '文章内容...',
tagList: ['testing', 'automation'],
},
};
const { status, body } = await apiRequest<ArticleResponse>({
method: 'POST',
url: '/api/articles',
body: newArticle,
});
// 运行时验证
expect(ArticleResponseSchema.parse(body)).toBeTruthy();
// 编译时类型安全
expect(status).toBe(201);
expect(body.article.title).toBe('测试文章');
expect(body.article.author.username).toBeDefined();
});
test('应该更新文章', async () => {
const updatePayload: UpdateArticlePayload = {
title: '更新的标题',
description: '更新的描述',
};
const { status, body } = await apiRequest<ArticleResponse>({
method: 'PUT',
url: '/api/articles/test-article',
body: { article: updatePayload },
});
expect(status).toBe(200);
expect(body.article.title).toBe('更新的标题');
});
});
4. 工具类型的高级用法
// 从现有类型创建新类型
type CreateArticlePayload = Pick<Article, 'title' | 'description' | 'body' | 'tagList'>;
type ArticleSummary = Pick<Article, 'id' | 'title' | 'description' | 'author'>;
type ArticleUpdatePayload = Partial<CreateArticlePayload>;
// 组合工具类型
type RequiredArticleFields = Required<Pick<Article, 'title' | 'body'>>;
type OptionalArticleFields = Partial<Omit<Article, 'id' | 'createdAt' | 'updatedAt'>>;
// 条件类型
type ApiResponse<T> = {
data: T;
status: 'success' | 'error';
message?: string;
};
type SuccessResponse<T> = ApiResponse<T> & { status: 'success' };
type ErrorResponse = ApiResponse<never> & { status: 'error'; message: string };
相关推荐
- C语言学习从内存堆栈视角,给这段枚举代码做个 "内存透视"
-
从内存堆栈视角,给这段枚举代码做个"内存透视"#include<stdio.h>enumDAY{MON=1,TUE,WED,THU,FR...
- Python基础:枚举,都有哪些特点和使用场景呢?
-
在Python编程语言中,枚举(Enumeration)是一种特殊的类,用于为一组常量创建一个名称空间。枚举类在Python3.4中被引入,提供了一种更加直观和方便的方式来处理一组相关的常量。枚举类...
- Java枚举你真的会用吗_java枚举怎么使用
-
概述Java中枚举,大家在项目中经常使用吧,主要用来定义一些固定值,在一个有限的集合内,比如在表示一周的某一天,一年中的四季等。那你了解枚举的本质吗?了解枚举的一些常见用法吗?枚举介绍和使用枚举主要用...
- 反射、枚举以及Lambda表达式_反射getmethod
-
一、反射1.定义Java的反射(reflection)机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法(即使是私有的);对于任意一个对象,都能够调用它的任意方法和属性,那么,我们就...
- 一个高效使用cursor开发项目的方法,怎么让 AI 写的代码不跑题?
-
最近又用cursor做了一个小应用,番茄时钟,用来管理自己的时间,提高效率。然后使用cursor开发的过程中。有了一些新的感悟。找到了一条可以让Curosr不跑题的办法。生成一份详细的项目资...
- 前端铜九铁十面试必备八股文——工程化
-
常用的git命令gitclone:克隆远程仓库到本地。gitinit:在当前目录初始化一个新的Git仓库。gitadd:将文件添加到暂存区,准备提交。gitcommit-m"co...
- IntelliJ IDEA 2025.2 的主要更新亮点
-
以下是该版本的一些关键改进与功能更新:AI增强体验离线Java代码补全:AI助手现在支持在离线模式下进行Java代码块建议,并允许用户选择本地代码模型使用。AIAssistant新增...
- 一行命令,AI 直接写代码!OpenAI 正式发布 Codex CLI
-
【一句话速读】OpenAI把2021年的Codex品牌复活,推出全新CodexCLI——一个本地运行的轻量级编码代理。只需npmi-g@openai/codex,它就能在终端里帮...
- 如何使用高级TypeScript模式构建可扩展的QA框架
-
TypeScript自动化QA(7部分系列)TypeScript第一步:自动化QA实用路线图如何在TypeScript中使用数组和对象构建强大的QA自动化脚本如何掌握TypeScript基础...
- Bun JS工具包新增MySQL驱动和密钥管理功能
-
Bun团队发布了其JavaScript打包器和运行时的1.2.21版本,该工具使用Zig语言编写,新增了包括MySQL和SQLite内置驱动、YAML解析器以及用于工具和本地开发的密钥管理器等功能。新...
- 编码 10000 个小时后,开发者悟了:“不要急于发布!”
-
【CSDN编者按】在软件开发的道路上,时间是最好的老师。根据“一万小时定律”,要成为某个领域的专家,通常需要大约一万小时的刻意练习。本文作者身为一名程序员,也经历了一万小时的编程,最终悟出了一个道理...
- 一文说明,TypeScript 的装饰器_typescript logo
-
●装饰器(Decorators)●注意:装饰器目前是一项实验性特性,在未来的版本中可能会发生改变●装饰器一般使用在以下几个地方○类○类属性○类方法○类方法的参数○通过这些我们也能看得出来,...
- 前端小哥哥:如何使用typescript开发实战项目?
-
前言笔者上一篇文章:主要写了typescript的用法和核心知识点总结,这篇文章将通过一个实际的前端案例来教大家如何在项目中使用typescript.你将收获如何使用umi快速搭建一个基于React...
- 一篇文章搞懂TypeScript_typescript implements
-
TypeScript是JavaScript的超集,一方面给动态类型的js增加了类型校验,另一方面扩展了js的各种功能。原始数据类型字符串数值布尔nullundefinedSymbolBi...
- TypeScript的any和unknown,用错一个就是线上Bug
-
在TypeScript开发中,类型系统是我们抵御运行时错误的第一道防线。但两个特殊类型——any和unknown,却常常被误用,成为线上故障的隐形推手。本文通过真实案例解析,告诉你为什么unknown...
- 一周热门
-
-
【验证码逆向专栏】vaptcha 手势验证码逆向分析
-
Python实现人事自动打卡,再也不会被批评
-
Psutil + Flask + Pyecharts + Bootstrap 开发动态可视化系统监控
-
一个解决支持HTML/CSS/JS网页转PDF(高质量)的终极解决方案
-
再见Swagger UI 国人开源了一款超好用的 API 文档生成框架,真香
-
网页转成pdf文件的经验分享 网页转成pdf文件的经验分享怎么弄
-
C++ std::vector 简介
-
飞牛OS入门安装遇到问题,如何解决?
-
系统C盘清理:微信PC端文件清理,扩大C盘可用空间步骤
-
10款高性能NAS丨双十一必看,轻松搞定虚拟机、Docker、软路由
-
- 最近发表
- 标签列表
-
- python判断字典是否为空 (50)
- crontab每周一执行 (48)
- aes和des区别 (43)
- bash脚本和shell脚本的区别 (35)
- canvas库 (33)
- dataframe筛选满足条件的行 (35)
- gitlab日志 (33)
- lua xpcall (36)
- blob转json (33)
- python判断是否在列表中 (34)
- python html转pdf (36)
- 安装指定版本npm (37)
- idea搜索jar包内容 (33)
- css鼠标悬停出现隐藏的文字 (34)
- linux nacos启动命令 (33)
- gitlab 日志 (36)
- adb pull (37)
- python判断元素在不在列表里 (34)
- python 字典删除元素 (34)
- vscode切换git分支 (35)
- python bytes转16进制 (35)
- grep前后几行 (34)
- hashmap转list (35)
- c++ 字符串查找 (35)
- mysql刷新权限 (34)