百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT知识 > 正文

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

liuian 2025-09-04 11:56 23 浏览

TypeScript自动化QA(7部分系列)

  1. TypeScript第一步:自动化QA实用路线图
  2. 如何在TypeScript中使用数组和对象构建强大的QA自动化脚本
  3. 如何掌握TypeScript基础逻辑以构建更智能的自动化QA
  4. TypeScript自定义类型QA自动化实用指南
  5. 停止编写脆弱的测试:可扩展TypeScript POM蓝图
  6. 停止编写不稳定的测试:Playwright异步基础指南
  7. 如何使用高级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。

//  这是"之前" - 等待发生的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个模式总结

你的使命:构建一个牢不可破的框架

你现在已经装备了在最健壮、企业级测试自动化框架中使用的模式。回到你自己的项目,寻找升级的机会:

  1. 寻找魔法字符串:找到任何硬编码字符串('admin''success''POST')并用枚举替换它们
  2. 模式化你的端点:选择你最关键的API端点,为它编写Zod模式,并使用z.infer生成类型。在测试中应用它
  3. 用泛型重构:识别两个或更多重复函数(如API调用)并将它们重构为单个、可重用的泛型函数
  4. 创建智能负载:寻找POSTPATCH请求,使用PartialPick等工具类型创建精确、最小的负载

采用这些高级模式是将你的测试框架从简单工具转变为强大、可扩展和真正可靠的工程资产的最后一步。


实际应用示例

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 };

相关推荐

电脑中病毒的原因(电脑中病毒正常吗)

电脑中毒的原因有以下几方面:1.网页被挂病毒。2.电脑裸奔,无防病毒软件。3.执行一些不安全的程序。4.U盘等不安全介质。5.电脑漏洞不及时补,被后台种毒。为了电脑不中病毒要注意以下几方面:1.更新系...

手机psd转换成jpg最简单方式

可以使用photoshop工具,方法如下:1、首先打开PS软件,然后选择自己需要的JPG格式的图片,在PS中打开。2、接下来先按快捷键“Ctrl+j”将图片复制出来,防止后面操作对原图片有损...

qq好友回复恢复官网(官方qq好友恢复)
  • qq好友回复恢复官网(官方qq好友恢复)
  • qq好友回复恢复官网(官方qq好友恢复)
  • qq好友回复恢复官网(官方qq好友恢复)
  • qq好友回复恢复官网(官方qq好友恢复)
win7提示激活码过期怎么办(win7激活已过期)

以win7为例,出现这样的问题原因分析:电脑的win7系统激活过又重新提示要激活的原因是因为微软对网络上的秘钥进行封杀所以导致我们激活无效。具体的解决方法:1、我们打开dos命令窗口,在创立中输入“s...

联想笔记本光驱驱动下载(联想电脑光驱驱动器在哪)

开机时进入BIOS,具体按什么牌子不同,按键也不同,开机有提示的,选择启动项,把光驱启动的顺序放到第一.按F10保存,重新启动就是光驱启动啦不需要设置光驱驱动,笔记本自带光驱驱动光驱是电脑的硬件设备,...

win10装机必备实用软件(win10电脑装机必备软件)

1、office大部分的版本如office2007、office2000、office2011、office2013、office2016、office365等都支持win10。2、需要注意...

迅雷无法下载的链接用什么下载

1.可以使用其他下载工具代替迅雷。2.迅雷可能无法下载的原因有很多,比如网络问题、软件故障等。其他下载工具可以提供类似的功能,但可能具有更好的稳定性和兼容性。3.一些常见的替代迅雷的下载工具包括...

apple官方网站(apple官方网站旗舰店)

1、首先打开浏览器,输入https://www.apple.com/;2、即可浏览苹果官网。 苹果公司(AppleInc.)是美国一家高科技公司。由史蒂夫·乔布斯、斯蒂夫·沃兹尼亚克和罗·韦恩(R...

哪些手机用鸿蒙系统(都什么手机能用鸿蒙系统)

截至目前,国内有以下几款手机品牌可以装鸿蒙系统:1.华为:华为Mate40系列、P40系列、Mate30系列、MatePadPro系列等。2.荣耀:荣耀V40、荣耀30系列、荣耀X10系列等...

手机u盘读不出来了怎么修复(手机u盘读取不出来)

1、手机不支持OTG功能,所以将U盘连接到手机后,手机无法识别U盘的内容,因此显示不了;这种情况只能换台支持OTG功能的手机来连接U盘才行。2、手机支持OTG功能,但是使用的OTG线质量有问题导致无法...

笔记本散热器买哪种好(笔记本散热器买哪种好贴吧)

散热器有十大品牌:九州风神、超频三,酷冷至尊Tt、AVC、思民、捷冷、安钛克Antec、安耐美Enermax、海盗船Corsair。能位列十大品牌,每一种的质量和功能都有保障。、目前网上销量最高的是九...

打印机驱动一直安装失败(打印机驱动一直安装失败怎么办)
打印机驱动一直安装失败(打印机驱动一直安装失败怎么办)

打印机驱动程序安装失败需要对电脑进行其他设置,详细步骤如下:1,在电脑桌面上找到【计算机】并用鼠标右击。2,右击后在出现的选项中找到【管理】选项并点击打开。3,接下里会进入到计算机控制台界面,在这里要根据自己的电脑选择64位或者32位,选择...

2026-01-14 12:55 liuian

ctrl加谁是截图(ctrl和什么键可以截图)

第一种:Ctrl+PrScrn使用这个组合键截屏,获得的是整个屏幕的图片第二种:Alt+PrScrn这个组合键截屏,获得的结果是当前窗口的图片第三种:打开qq,使用快捷键Ctrl+...

技嘉主板bios设置启动顺序(技嘉主板bios设置启动顺序怎么设置)

启动顺序设置方法如下:1、重启电脑连续按[DEL]键进入BIOS设置,按DEL进入BIOS设置。2、按键盘方向键右键切换到BOOT选项,将windows10功能设置为"其它操作系统"...

目前台式电脑主机怎么选(台式主机选择)
目前台式电脑主机怎么选(台式主机选择)

每个人对电脑的性需要不同,因此根据自己家的家庭需要,选择合适的电脑即可。以下简单说明:1,双核处理器+2G内存+集成显卡+机械硬盘。性能满足上网、看电影、聊天、办公、玩象棋之类的小游戏。价格在2000以内可以买到;2,四核处理器+4G内存+...

2026-01-14 12:05 liuian