MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

TypeScript端到端测试类型支持方案

2022-06-017.6k 阅读

一、TypeScript 端到端测试概述

在现代软件开发中,端到端(E2E)测试是确保应用程序从用户界面到后端服务整个流程功能正确性的关键环节。对于使用 TypeScript 进行开发的项目,在 E2E 测试中充分利用 TypeScript 的类型系统优势,可以显著提高测试代码的可靠性和可维护性。

TypeScript 作为 JavaScript 的超集,为代码带来了静态类型检查。在 E2E 测试场景下,这意味着我们可以在编写测试代码时就发现类型相关的错误,而不是等到运行时。例如,当我们调用某个函数传递了错误类型的参数,如果是在纯 JavaScript 测试代码中,可能要到运行测试时才会发现问题;但在 TypeScript 编写的测试代码中,编辑器或者构建工具就能提前提示错误。

二、常用的 E2E 测试框架与 TypeScript 集成

  1. Cypress 与 TypeScript
    • 集成步骤
      • 首先,确保项目已经安装了 Cypress。如果项目使用 npm,运行npm install cypress -D
      • 为了支持 TypeScript,安装@types/cypress,这是 Cypress 的类型定义包。运行npm install @types/cypress -D
      • 在 Cypress 的配置文件(通常是cypress.jsoncypress.config.js)中,指定测试文件的扩展名。例如,在cypress.config.js中:
module.exports = defineConfig({
  e2e: {
    specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}'
  }
});
  • 示例代码: 假设我们有一个简单的待办事项应用,页面上有一个输入框用于添加待办事项,一个按钮用于提交,以及一个列表展示待办事项。以下是使用 Cypress 和 TypeScript 编写的 E2E 测试:
describe('Todo App', () => {
  it('should add a new todo', () => {
    cy.visit('/todo');
    cy.get('input[type="text"]').type('Buy groceries');
    cy.get('button').click();
    cy.get('ul li').should('contain', 'Buy groceries');
  });
});

在这个例子中,TypeScript 的类型检查可以确保cy.get获取到的元素选择器是正确的类型,避免了在运行时因为选择器错误而导致测试失败却难以排查的问题。

  1. Puppeteer 与 TypeScript
    • 集成步骤
      • 安装 Puppeteer,运行npm install puppeteer -D
      • 安装@types/puppeteer来获取类型定义,运行npm install @types/puppeteer -D
      • 可以创建一个 TypeScript 配置文件tsconfig.json,配置如下:
{
  "compilerOptions": {
    "target": "ES6",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}
  • 示例代码
import puppeteer from 'puppeteer';

describe('Page title test', () => {
  let browser: puppeteer.Browser;
  let page: puppeteer.Page;

  beforeAll(async () => {
    browser = await puppeteer.launch();
    page = await browser.newPage();
  });

  afterAll(async () => {
    await browser.close();
  });

  it('should have correct page title', async () => {
    await page.goto('https://example.com');
    const title = await page.title();
    expect(title).toBe('Example Domain');
  });
});

这里,TypeScript 确保了puppeteer相关函数和对象的使用符合类型定义,比如launchnewPage等函数的调用参数和返回值类型都被严格检查。

三、自定义类型定义在 E2E 测试中的应用

  1. 针对应用特定元素的类型定义 在 E2E 测试中,我们经常需要操作应用程序中特定的 UI 元素。通过自定义类型定义,可以让测试代码更加清晰和健壮。 例如,在一个电商应用中,有一个商品详情页面,页面上有商品价格、名称、库存等元素。我们可以定义如下类型:
interface ProductDetails {
  price: number;
  name: string;
  stock: number;
}

const getProductDetails = (): ProductDetails => {
  const priceElement = document.querySelector('.product - price');
  const nameElement = document.querySelector('.product - name');
  const stockElement = document.querySelector('.product - stock');

  if (!priceElement ||!nameElement ||!stockElement) {
    throw new Error('Product details elements not found');
  }

  return {
    price: parseFloat(priceElement.textContent || '0'),
    name: nameElement.textContent || '',
    stock: parseInt(stockElement.textContent || '0')
  };
};

在 E2E 测试中使用这个类型定义:

describe('Product details page', () => {
  it('should display correct product details', () => {
    cy.visit('/product/123');
    cy.window().then((win) => {
      const details = win.getProductDetails();
      expect(details.price).toBeGreaterThan(0);
      expect(details.name).not.toBe('');
      expect(details.stock).toBeGreaterThanOrEqual(0);
    });
  });
});

这样,通过自定义ProductDetails类型,我们明确了商品详情数据的结构,在测试中可以更准确地验证数据。

  1. 针对 API 响应的类型定义 当 E2E 测试涉及到与后端 API 交互时,定义 API 响应的类型尤为重要。假设我们有一个获取用户信息的 API,响应数据如下:
{
  "id": 1,
  "name": "John Doe",
  "email": "johndoe@example.com"
}

我们可以定义如下类型:

interface User {
  id: number;
  name: string;
  email: string;
}

const getUser = async (id: number): Promise<User> => {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) {
    throw new Error('Failed to fetch user');
  }
  return response.json();
};

在 E2E 测试中:

describe('User API', () => {
  it('should return correct user data', async () => {
    const user = await getUser(1);
    expect(user.id).toBe(1);
    expect(user.name).toBe('John Doe');
    expect(user.email).toBe('johndoe@example.com');
  });
});

通过定义User类型,我们可以在测试中确保 API 响应的数据结构符合预期,提高测试的可靠性。

四、类型安全的测试数据生成

  1. 使用 Faker.js 结合 TypeScript Faker.js 是一个用于生成假数据的库,在 E2E 测试中常用于填充表单、模拟用户输入等场景。结合 TypeScript 可以实现类型安全的数据生成。 首先,安装 Faker.js 和它的类型定义:npm install faker @types/faker -D。 假设我们有一个注册表单,需要生成用户名、邮箱和密码。可以这样做:
import faker from 'faker';

interface RegistrationData {
  username: string;
  email: string;
  password: string;
}

const generateRegistrationData = (): RegistrationData => {
  return {
    username: faker.internet.userName(),
    email: faker.internet.email(),
    password: faker.internet.password()
  };
};

在 E2E 测试中使用生成的数据:

describe('Registration form', () => {
  it('should register a new user', () => {
    const data = generateRegistrationData();
    cy.visit('/register');
    cy.get('input[name="username"]').type(data.username);
    cy.get('input[name="email"]').type(data.email);
    cy.get('input[name="password"]').type(data.password);
    cy.get('button[type="submit"]').click();
    // 后续验证注册成功的逻辑
  });
});

通过定义RegistrationData类型,确保了生成的数据结构符合注册表单的要求,避免了因数据类型不匹配导致的测试失败。

  1. 基于自定义规则的类型安全数据生成 有时候,Faker.js 生成的数据可能不完全符合我们的业务规则。我们可以基于自定义规则实现类型安全的数据生成。 例如,在一个游戏应用中,角色有等级、经验值等属性,等级范围是 1 - 100,经验值根据等级有一定的计算公式。
interface Character {
  level: number;
  experience: number;
}

const generateCharacter = (): Character => {
  const level = Math.floor(Math.random() * 100) + 1;
  const experience = level * 100; // 简单的经验值计算公式
  return {
    level,
    experience
  };
};

在 E2E 测试中:

describe('Character creation', () => {
  it('should create a valid character', () => {
    const character = generateCharacter();
    expect(character.level).toBeGreaterThan(0);
    expect(character.level).toBeLessThanOrEqual(100);
    expect(character.experience).toBe(character.level * 100);
    // 后续验证角色创建成功并符合属性要求的逻辑
  });
});

通过自定义Character类型和生成逻辑,保证了生成的角色数据符合游戏的业务规则,提高了 E2E 测试的准确性。

五、处理异步操作的类型安全

  1. Promise 和 async/await 的正确使用 在 E2E 测试中,很多操作是异步的,比如等待页面加载、API 调用等。正确使用Promiseasync/await结合 TypeScript 的类型系统至关重要。 例如,使用 Puppeteer 等待页面上某个元素出现:
import puppeteer from 'puppeteer';

const waitForElement = async (page: puppeteer.Page, selector: string): Promise<void> => {
  await page.waitForSelector(selector);
};

describe('Page element loading', () => {
  let browser: puppeteer.Browser;
  let page: puppeteer.Page;

  beforeAll(async () => {
    browser = await puppeteer.launch();
    page = await browser.newPage();
  });

  afterAll(async () => {
    await browser.close();
  });

  it('should wait for a specific element', async () => {
    await page.goto('https://example.com');
    await waitForElement(page, '.specific - element');
    // 验证元素出现后的逻辑
  });
});

这里,waitForElement函数返回Promise<void>,明确了它是一个异步操作且不返回具体值。async/await的使用使得代码看起来更同步,同时 TypeScript 确保了page.waitForSelector等异步函数的正确调用。

  1. 处理异步回调的类型安全 在一些 E2E 测试场景中,可能会遇到使用异步回调的情况。例如,Cypress 的cy.then方法。
describe('Asynchronous callback handling', () => {
  it('should handle async callback correctly', () => {
    cy.visit('/async - page');
    cy.get('.async - element').then(($element) => {
      const text = $element.text();
      expect(text).not.toBe('');
    });
  });
});

在这个例子中,cy.then的回调函数参数$element的类型是由 Cypress 的类型定义确定的。TypeScript 确保了我们在回调函数中对$element的操作是类型安全的,比如调用text方法。

六、与持续集成(CI)结合确保类型一致性

  1. 在 CI 环境中运行 TypeScript E2E 测试 将 TypeScript 编写的 E2E 测试集成到持续集成流程中,可以确保每次代码提交时,测试代码的类型一致性和正确性。 以 GitHub Actions 为例,假设项目使用 Cypress 进行 E2E 测试。首先,在项目根目录创建.github/workflows/e2e - test.yml文件:
name: E2E Tests
on:
  push:
    branches:
      - main
jobs:
  e2e - test:
    runs - on: ubuntu - latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2
      - name: Set up Node.js
        uses: actions/setup - node@v2
        with:
          node - version: '14'
      - name: Install dependencies
        run: npm install
      - name: Run E2E tests
        run: npx cypress run

在这个流程中,首先拉取代码,然后安装 Node.js 和项目依赖,最后运行 Cypress 的 E2E 测试。如果测试代码中有类型错误,在运行测试时就会报错,阻止代码合并到主分支。

  1. 利用 CI 进行类型检查优化 除了运行测试,还可以在 CI 环境中单独进行 TypeScript 的类型检查。可以在package.json中添加一个脚本:
{
  "scripts": {
    "type - check": "tsc --noEmit"
  }
}

然后在 GitHub Actions 中添加步骤:

name: E2E Tests
on:
  push:
    branches:
      - main
jobs:
  e2e - test:
    runs - on: ubuntu - latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2
      - name: Set up Node.js
        uses: actions/setup - node@v2
        with:
          node - version: '14'
      - name: Install dependencies
        run: npm install
      - name: Type check
        run: npm run type - check
      - name: Run E2E tests
        run: npx cypress run

这样,在运行 E2E 测试之前,先进行类型检查。如果类型检查不通过,就不会执行 E2E 测试,提高了整个 CI 流程的效率和可靠性。

七、类型迁移与升级策略

  1. 从 JavaScript 测试迁移到 TypeScript 测试 如果项目之前使用 JavaScript 编写 E2E 测试,要迁移到 TypeScript 测试,可以逐步进行。 首先,安装 TypeScript 和相关类型定义,如前面提到的针对测试框架的类型定义(@types/cypress@types/puppeteer等)。 然后,将测试文件的扩展名从.js改为.ts。在编辑器中,TypeScript 会开始提示类型错误。可以从简单的函数和变量开始添加类型注解。 例如,在一个 JavaScript 的 Cypress 测试中:
describe('Simple test', () => {
  it('should pass', () => {
    const num = 10;
    expect(num).toBe(10);
  });
});

迁移到 TypeScript 后:

describe('Simple test', () => {
  it('should pass', () => {
    const num: number = 10;
    expect(num).toBe(10);
  });
});

随着逐步添加类型注解,不断修复类型错误,最终完成整个测试代码库的迁移。

  1. TypeScript 版本升级与类型兼容性 当升级 TypeScript 版本时,可能会遇到类型兼容性问题。例如,新的 TypeScript 版本可能对某些类型的推断更加严格,或者废弃了一些旧的类型语法。 在升级之前,建议在项目的测试环境中进行预演。可以先修改package.json中的 TypeScript 版本,然后运行测试和类型检查。 如果遇到类型错误,查看 TypeScript 的官方文档了解版本升级的变化。例如,在 TypeScript 3.7 中引入了Optional ChainingNullish Coalescing运算符,这可能会影响到代码中类型的使用方式。如果之前在代码中手动处理nullundefined,可能需要调整为使用新的运算符,同时确保类型注解仍然正确。

通过遵循这些类型迁移与升级策略,可以在保证 E2E 测试功能的前提下,充分利用 TypeScript 不断演进的特性,提高测试代码的质量和可维护性。