2024年,重新探索前端单元测试之路,从入门到精通03-Vitest

# 2024年,重新探索前端单元测试之路,从入门到精通01-Vitest

# 2024年,重新探索前端单元测试之路,从入门到精通02-Vitest

十四、行为验证

这里主要讲的是对于函数的调用次数,以及函数参数等验证

这里举得例子是登录接口

import { phoneLogin } from "xxx/api";

const state = {
  tipString: "",
};

export function login(username: string, password: string) {
  phoneLogin(username, password);
}

export function loginV2(username: string, password: string) {
  const isLogin = phoneLogin(username, password);

  if (isLogin) {
    state.tipString = "登录成功拉";
  }
}

export function getTip() {
  return state.tipString;
}

接下来看看我们是怎么测试这个登录接口的

import { vi, it, expect, describe } from "vitest";
import { login, loginV2, getTip } from "./login";
import { phoneLogin } from "xxx/api";

// mock
vi.mock("cxr", () => {
  return {
    // phoneLogin: vi.fn().mockReturnValue(true),
    phoneLogin: vi.fn(()=> true)
  };
});

describe("login", () => {
  it("should called login function from cxr  ", async () => {
    login("phone", "jiubugaosuni");

    expect(cxrLogin).toBeCalled();
    // expect(cxrLogin).toBeCalledWith("phone", "jiubugaosuni");
    // expect(cxrLogin).toBeCalledTimes(1);
  });

  it("v2", () => {
    loginV2("phone", "jiubugaosuni");

    expect(phoneLogin).toBeCalled();
    expect(getTip()).toBe("登录成功拉");
  });
});

这里使用vi.mock去对接口重写,并且使用toBeCalledtoBeCalledWithtoBeCalledTimes等来验证该函数是否调用,调用参数是什么,一共调用了几次。

十五、不知道验证什么-完美主义、功能的目的、小步走-TDD思想

TDD(Test-Driven Development)是一种软件开发方法论,它强调在编写代码之前编写测试用例。其核心思想是通过编写测试用例来驱动代码的开发,以确保代码的正确性和可靠性。

我们只需要记住我们的目标是什么,然后小步走的思想,一个一个去完成,不要一开始就想把所有东西都弄好,这是不现实的。TDD的思想可以应用在我们生活之间

比如下面有一个函数,验证是否是http地址,我们需要如何测试呢

// url http
// url https
// url ""
// url "dslkfj"
// url "123"
// url 
export function isHttp(url: string): boolean {
  const pattern = /^http:\/\/www\./;
  return pattern.test(url);
}

我们只需要编写最小化的代码,因为不可能完美的去写出每个过程,所以,尽可能的写出我们想到的部分,其他的测试出来的问题,我们再去加就好了,不要被完美主义作祟。

import { describe, it, expect } from "vitest";
import { isHttp } from "./utils";

describe("isHttp", () => {
  it("should return true for the specific case: http://www.baidu.com", () => {
    const url = "http://www.baidu.com";
    expect(isHttp(url)).toBeTruthy();
  });

  it("should return false for non-http URLs", () => {
    const url = "https://www.google.com";
    expect(isHttp(url)).toBeFalsy();
  });
  it("should return false for non-http URLs", () => {
    const url = "";
    expect(isHttp(url)).toBeFalsy();
  });
});

十六、可预测性-随机数-日期date

随机数

这里如何对随机数进行验证呢?因为随机数一直是变化的

/**
 * 基于 Math.random 生成一个随机字符串
 * @param length 字符串长度
 * @returns 生成的随机字符串
 */
export function generateRandomString(length: number): string {
  let result = "";
  const characters = "abcdefghijklmnopqrstuvwxyz";
  for (let i = 0; i < length; i++) {
    const randomIndex = Math.floor(Math.random() * characters.length); // 生成 0 到字符串长度之间的随机整数
    result += characters.charAt(randomIndex); // 将指定位置上的字符添加到结果字符串中
  }
  return result;
}

验证

通过vi.spyOn去模拟Math.random方法并且固定返回它的值为0.1和0.2

import { vi, it, expect, describe } from "vitest";
import { generateRandomString } from "./random";

describe("Math.random", () => {
  it("should generate random string", () => {
    //     vi.spyOn(Math, "random").mockImplementation(() => {
    //       return 0.1;
    //     });
    vi.spyOn(Math, "random").mockImplementationOnce(() => {
      return 0.1;
    });
    vi.spyOn(Math, "random").mockImplementationOnce(() => {
      return 0.2;
    });

    const result = generateRandomString(2);

    expect(result).toBe("fc");
  });
});

日期函数

这里如何对日期进行验证呢?因为日期一直是变化的

/**
 * 检测今天是否为周五
 * @returns 如果今天是周五返回 "开心",否则返回 "不开心"
 */
export function checkFriday(): string {
  const today = new Date();
  console.log(today.getDay());
  if (today.getDay() === 5) {
    return "happy";
  } else {
    return "sad";
  }
}

验证

这里使用的是模拟定时器,使用vi.useFakeTimers(),注意:这里在测试结束后需要使用vi.useRealTimers();移除掉,然后我们使用vi.setSystemTime(new Date(2023, 3, 21));去设置系统时间

import { beforeEach, afterEach, vi, it, expect, describe } from "vitest";
import { checkFriday } from "./date";

describe("date", () => {
  beforeEach(() => {
    vi.useFakeTimers();
  });

  afterEach(() => {
    vi.useRealTimers();
  });
  it("should be happy when it's Friday", () => {
    vi.setSystemTime(new Date(2023, 3, 21));

    const result = checkFriday();

    expect(result).toBe("happy");
  });
  it("should be sad when it's not Friday", () => {
    vi.setSystemTime(new Date(2023, 3, 22));

    const result = checkFriday();

    expect(result).toBe("sad");
  });
  it("third", () => {
    checkFriday();
  });
});

十七、快速反馈-处理异步代码time、promise

定时器与延时器

export function sayHi() {
  setTimeout(() => {
    setInterval(() => {
      console.log("hi");
    }, 100);
  }, 1000);
}
export class User {
  id: string;

  constructor(id: string) {
    this.id = id;
  }

  fetchData(callback: (data: string) => void, delay: number): void {
    setTimeout(() => {
      const data = `Data for user with id: ${this.id}`;
      callback(data);
    }, delay);
  }

  fetchDataV2(callback: (data: string) => void): void {
    setTimeout(() => {
      const data = `Data for user with id: ${this.id}`;
      callback(data);
    }, 2000);
  }
}

验证

这里我们也是使用vi.useFakeTimers();去模拟定时器,这里我们有两种方式去进行验证,第一种就是使用vi.advanceTimersToNextTimer()vi.advanceTimersByTime(1100);这个是设置定时器的时间,第二种就是使用vi.spyOn(console, "log");去模拟打印方法,这种是比较推荐的

import { vi, it, expect, describe } from "vitest";
import { sayHi } from "./setInterval";

describe("setInterval", () => {
  it("should call one", () => {
    vi.useFakeTimers();
    vi.spyOn(console, "log");
    sayHi();
    //     vi.advanceTimersToNextTimer()
    //     vi.advanceTimersToNextTimer()
	//     vi.advanceTimersByTime(1100);

    expect(console.log).toBeCalledWith("hi");
  });
});
import { vi, it, expect, describe } from "vitest";
import { User } from "./setTimeout";

describe("setTimeout", () => {
  it("should fetch User data", () => {
    vi.useFakeTimers();
    const user = new User("1");

    const callback = vi.fn();
    user.fetchDataV2(callback);
    // vi.advanceTimersByTime(1000)
    // vi.advanceTimersToNextTimer();

    const userA = new User("1");

    const callbackA = vi.fn();
    userA.fetchDataV2(callbackA);
    // vi.advanceTimersToNextTimer();

    vi.runAllTimers();

    expect(callback).toBeCalledWith("Data for user with id: 1");
    expect(callbackA).toBeCalledWith("Data for user with id: 1");
  });
});

promise

export class View {
  count: number = 1;
  render() {
    Promise.resolve()
      .then(() => {
        this.count = 2;
      })
      .then(() => {
        this.count = 3;
      });
  }
}

export function fetchUserData() {
  return new Promise((resolve, reject) => {
    resolve("1");
  });
}

export function delay(time: number) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("ok");
    }, time);
  });
}

验证

import { vi, it, expect, describe } from "vitest";
import { delay, fetchUserData } from "./index";

describe("Promise", () => {
  it("normal", async () => {
    const result = await fetchUserData();

    expect(result).toBe("1");
  });

  it("delay", async () => {
    vi.useFakeTimers();
    //     vi.advanceTimersToNextTimer()
    //     const result = await delay(1000);
    const result = delay(100);
    vi.advanceTimersToNextTimer();

    expect(result).resolves.toBe("ok");
  });
});

第二种是结合flush-promises这个库来使用

import { it, expect, describe } from "vitest";
import { View } from "./view";
import flushPromises from "flush-promises";

describe("View", () => {
  it("should change count", async () => {
    const view = new View();

    view.render();
    await flushPromises();

    expect(view.count).toBe(3);
  });
});

十八、API的多种测试方案

1、直接mock axios

直接上代码,通过直接对axios进行mock来定义返回结果---------不推荐使用

import { test, expect, vi } from "vitest";
import { useTodoStore } from "./todo";
import { setActivePinia, createPinia } from "pinia";
import axios from "axios";

vi.mock("axios");

test("add todo", async () => {
  // 准备数据
  vi.mocked(axios.post).mockImplementation((path, { title }: any) => {
    return Promise.resolve({
      data: { data: { todo: { id: 1, title } }, state: 1 },
    });
  });
  setActivePinia(createPinia());
  const todoStore = useTodoStore();
  const title = "吃饭";

  // 调用
  await todoStore.addTodo(title);

  // 验证
  expect(todoStore.todos[0].title).toBe(title);
});

test("should not add todo when title is empty string", async () => {
  setActivePinia(createPinia());
  const todoStore = useTodoStore();
  const title = "";

  // 调用
  await todoStore.addTodo(title);

  // 验证
  expect(todoStore.todos.length).toBe(0);
});

test("remove todo", async () => {
  vi.mocked(axios.post).mockImplementationOnce((path, { title }: any) => {
    return Promise.resolve({
      data: { data: { todo: { id: 1, title } }, state: 1 },
    });
  });
  vi.mocked(axios.post).mockImplementationOnce((path, { id }: any) => {
    return Promise.resolve({
      data: { data: { id }, state: 1 },
    });
  });

  setActivePinia(createPinia());
  const todoStore = useTodoStore();
  const todo = await todoStore.addTodo("吃饭"); // round-trip

  // 调用
  await todoStore.removeTodo(todo!.id);

  // 验证
  expect(todoStore.todos.length).toBe(0);
});

test("should throw error when removed id does not exist ", async () => {
  // 准备数据
  vi.mocked(axios.post).mockImplementationOnce((path, { id }: any) => {
    return Promise.resolve({
      data: { data: null, state: 0 },
    });
  });

  setActivePinia(createPinia());
  const todoStore = useTodoStore();

  expect(async () => {
    // 调用
    await todoStore.removeTodo(2);
    // 抛出一个错误
  }).rejects.toThrowError("id:2 不存在");
});

test("update todo list", async () => {
  const todoList = [{ id: 1, title: "写代码" }];
  vi.mocked(axios.get).mockResolvedValue({ data: { data: { todoList } } });

  setActivePinia(createPinia());
  const todoStore = useTodoStore();
  await todoStore.updateTodoList();

  expect(todoStore.todos[0].title).toBe("写代码");
});

2、mock中间层

这个的意思指的是,只对行为进行验证,行为产生的结果进行mock-----------推荐使用

import { test, expect, vi } from "vitest";
import { useTodoStore } from "./todo";
import { setActivePinia, createPinia } from "pinia";
import { fetchAddTodo, fetchRemoveTodo, fetchTodoList } from "../api";

vi.mock("../api");

// SUT  create list
// create list
// add todo to todos , todos' length is 1
test("should add todo to the list when successful", async () => {
  // 准备数据
  vi.mocked(fetchAddTodo).mockImplementation((title) => {
    return Promise.resolve({
      data: { todo: { id: 1, title } },
      state: 1,
    });
  });
  setActivePinia(createPinia());
  const todoStore = useTodoStore();
  const title = "吃饭";

  // 调用
  await todoStore.addTodo(title);

  // 验证
  expect(todoStore.todos[0].title).toBe(title);


});

test("should not be added todo when network is error", async () => {
  // 准备数据
  vi.mocked(fetchAddTodo).mockImplementation((title) => {
    return Promise.reject("network error");
  });
  setActivePinia(createPinia());
  const todoStore = useTodoStore();
  const title = "吃饭";

  // 调用
  expect(async () => {
    await todoStore.addTodo(title);
  }).rejects.toThrowError("network error");
});

test("should not add a todo when title is empty", async () => {
  // 准备数据
  setActivePinia(createPinia());
  const todoStore = useTodoStore();
  const title = "";

  // 调用
  await todoStore.addTodo(title);

  // 验证
  expect(todoStore.todos.length).toBe(0);
});

test("remove todo when todo is", async () => {
  // 准备数据
  vi.mocked(fetchAddTodo).mockImplementation((title) => {
    return Promise.resolve({
      data: { todo: { id: 1, title } },
      state: 1,
    });
  });
  vi.mocked(fetchRemoveTodo).mockImplementationOnce((id) => {
    return Promise.resolve({
      data: { id },
      state: 1,
    });
  });

  setActivePinia(createPinia());
  const todoStore = useTodoStore();
  const todo = await todoStore.addTodo("吃饭"); // round-trip

  // 调用
  await todoStore.removeTodo(todo!.id);

  // 验证
  expect(todoStore.todos.length).toBe(0);
});

test("should throw a error when removed id does not exist", async () => {
  // 准备数据
  vi.mocked(fetchRemoveTodo).mockImplementationOnce(() => {
    return Promise.resolve({
      data: null,
      state: 0,
    });
  });

  setActivePinia(createPinia());
  const todoStore = useTodoStore();

  // 调用
  expect(async () => {
    await todoStore.removeTodo(2);
  }).rejects.toThrowError("id:2 does not exist");
});

test("update todo list", async () => {
  const todoList = [{ id: 1, title: "写代码" }];
  vi.mocked(fetchTodoList).mockResolvedValue({ data: { todoList } });

  setActivePinia(createPinia());
  const todoStore = useTodoStore();
  await todoStore.updateTodoList();

  expect(todoStore.todos[0].title).toBe("写代码");
});

3、使用mock server worker

这里指的是使用msw这个库来mock返回结果,相当于利用中间件去测试

缺点:就是需要去学习新的库,不太推荐使用

import { beforeAll, afterEach, afterAll, test, expect} from "vitest";
import { useTodoStore } from "./todo";
import { setActivePinia, createPinia } from "pinia";
import { server } from "../mocks/server";
import { mockAddTodo, mockRemoveTodo, mockTodoList } from "../mocks/handlers";


test.todo("sad path")

test("add todo", async () => {
  // koa express
  server.use(mockAddTodo());

  setActivePinia(createPinia());
  const todoStore = useTodoStore();
  const title = "吃饭";

  // 调用
  await todoStore.addTodo(title);

  // 验证
  expect(todoStore.todos[0].title).toBe(title);
});

test("remove todo", async () => {
  server.use(mockAddTodo(), mockRemoveTodo());

  setActivePinia(createPinia());
  const todoStore = useTodoStore();
  const todo = await todoStore.addTodo("吃饭"); // round-trip

  // 调用
  await todoStore.removeTodo(todo!.id);

  // 验证
  expect(todoStore.todos.length).toBe(0);
});

test("update todo list", async () => {
  const todoList = [{ id: 1, title: "写代码" }];
  server.use(mockTodoList(todoList));

  setActivePinia(createPinia());
  const todoStore = useTodoStore();
  await todoStore.updateTodoList();

  expect(todoStore.todos[0].title).toBe("写代码");
});

十九、参数化验证

提供在多个测试case中复用相同的测试逻辑的方法

比如我们想去验证一个正则表达式

export function emailValidator(email: string): boolean {
  const regex = /^[\w-]+(\.[\w-]+)*@([\w-]+\.)+[a-zA-Z]{2,7}$/;
  return regex.test(email);
}

可能我们的测试case非常多,可以看到非常多重复的case

import { emailValidator } from "./emailValidator"
import { it, expect, describe } from "vitest"

describe("emailValidator", () => {
  it("should return true for valid email", () => {
    const email = "valid-email@example.com"
    expect(emailValidator(email)).toBe(true)
  })

  it("should return false for invalid email without domain extension", () => {
    const email = "invalid.email@example"
    expect(emailValidator(email)).toBe(false)
  })

  it("should return false for invalid email with extra dot at the end", () => {
    const email = "another.invalid.email@example."
    expect(emailValidator(email)).toBe(false)
  })

  it("should return false for invalid email with missing '@'", () => {
    const email = "yet.another.invalid.email.example.com"
    expect(emailValidator(email)).toBe(false)
  })
})

解决办法:我们可以利用工具提供的方法

import { emailValidator } from "./emailValidator"
import { it, expect, describe } from "vitest"

describe("emailValidator", () => {
  it.each([
    ["valid-email@example.com", true],
    ["invalid.email@example", false],
    ["another.invalid.email@example.", false],
    ["yet.another.invalid.email.example.com", false],
  ])("should return %s when email is %s", (email, expected) => {
    expect(emailValidator(email)).toBe(expected)
  })

  it.each([{ email: "valid-email@example.com", expected: true }])(
    "should return $email when email is $expected",
    ({ email, expected }) => {
      console.log(email, expected)
      expect(emailValidator(email)).toBe(expected)
    }
  )

  it.each`
    email                        | expected
    ${"valid-email@example.com"} | ${true}
    ${"invalid.email@example"}   | ${false}
  `("should return $email when email is $expected", ({ email, expected }) => {
    console.log(email, expected)
    expect(emailValidator(email)).toBe(expected)
  })

  it.each`
    email             | expected
    ${{ a: "aaaaa" }} | ${true}
    ${[]}             | ${true}
    ${false}          | ${true}
  `("should return $email.a when email is $expected", ({ email, expected }) => {
    console.log(email, expected)
    expect(false).toBe(true)
    //     expect(emailValidator(email)).toBe(expected);
  })
})

我们使用it.each,并且使用模版字符串的语法,在参数中,我们还可以使用$email.a去获取参数,其中第三种和第四种方法是最佳实践

第一种方法因为在执行过程中,不太好查看错误信息,所以不推荐使用—
theme: z-blue
highlight: atom-one-light

# 2024年,重新探索前端单元测试之路,从入门到精通01-Vitest

# 2024年,重新探索前端单元测试之路,从入门到精通02-Vitest

十四、行为验证

这里主要讲的是对于函数的调用次数,以及函数参数等验证

这里举得例子是登录接口

import { phoneLogin } from "xxx/api";

const state = {
  tipString: "",
};

export function login(username: string, password: string) {
  phoneLogin(username, password);
}

export function loginV2(username: string, password: string) {
  const isLogin = phoneLogin(username, password);

  if (isLogin) {
    state.tipString = "登录成功拉";
  }
}

export function getTip() {
  return state.tipString;
}

接下来看看我们是怎么测试这个登录接口的

import { vi, it, expect, describe } from "vitest";
import { login, loginV2, getTip } from "./login";
import { phoneLogin } from "xxx/api";

// mock
vi.mock("cxr", () => {
  return {
    // phoneLogin: vi.fn().mockReturnValue(true),
    phoneLogin: vi.fn(()=> true)
  };
});

describe("login", () => {
  it("should called login function from cxr  ", async () => {
    login("phone", "jiubugaosuni");

    expect(cxrLogin).toBeCalled();
    // expect(cxrLogin).toBeCalledWith("phone", "jiubugaosuni");
    // expect(cxrLogin).toBeCalledTimes(1);
  });

  it("v2", () => {
    loginV2("phone", "jiubugaosuni");

    expect(phoneLogin).toBeCalled();
    expect(getTip()).toBe("登录成功拉");
  });
});

这里使用vi.mock去对接口重写,并且使用toBeCalledtoBeCalledWithtoBeCalledTimes等来验证该函数是否调用,调用参数是什么,一共调用了几次。

十五、不知道验证什么-完美主义、功能的目的、小步走-TDD思想

TDD(Test-Driven Development)是一种软件开发方法论,它强调在编写代码之前编写测试用例。其核心思想是通过编写测试用例来驱动代码的开发,以确保代码的正确性和可靠性。

我们只需要记住我们的目标是什么,然后小步走的思想,一个一个去完成,不要一开始就想把所有东西都弄好,这是不现实的。TDD的思想可以应用在我们生活之间

比如下面有一个函数,验证是否是http地址,我们需要如何测试呢

// url http
// url https
// url ""
// url "dslkfj"
// url "123"
// url 
export function isHttp(url: string): boolean {
  const pattern = /^http:\/\/www\./;
  return pattern.test(url);
}

我们只需要编写最小化的代码,因为不可能完美的去写出每个过程,所以,尽可能的写出我们想到的部分,其他的测试出来的问题,我们再去加就好了,不要被完美主义作祟。

import { describe, it, expect } from "vitest";
import { isHttp } from "./utils";

describe("isHttp", () => {
  it("should return true for the specific case: http://www.baidu.com", () => {
    const url = "http://www.baidu.com";
    expect(isHttp(url)).toBeTruthy();
  });

  it("should return false for non-http URLs", () => {
    const url = "https://www.google.com";
    expect(isHttp(url)).toBeFalsy();
  });
  it("should return false for non-http URLs", () => {
    const url = "";
    expect(isHttp(url)).toBeFalsy();
  });
});

十六、可预测性-随机数-日期date

随机数

这里如何对随机数进行验证呢?因为随机数一直是变化的

/**
 * 基于 Math.random 生成一个随机字符串
 * @param length 字符串长度
 * @returns 生成的随机字符串
 */
export function generateRandomString(length: number): string {
  let result = "";
  const characters = "abcdefghijklmnopqrstuvwxyz";
  for (let i = 0; i < length; i++) {
    const randomIndex = Math.floor(Math.random() * characters.length); // 生成 0 到字符串长度之间的随机整数
    result += characters.charAt(randomIndex); // 将指定位置上的字符添加到结果字符串中
  }
  return result;
}

验证

通过vi.spyOn去模拟Math.random方法并且固定返回它的值为0.1和0.2

import { vi, it, expect, describe } from "vitest";
import { generateRandomString } from "./random";

describe("Math.random", () => {
  it("should generate random string", () => {
    //     vi.spyOn(Math, "random").mockImplementation(() => {
    //       return 0.1;
    //     });
    vi.spyOn(Math, "random").mockImplementationOnce(() => {
      return 0.1;
    });
    vi.spyOn(Math, "random").mockImplementationOnce(() => {
      return 0.2;
    });

    const result = generateRandomString(2);

    expect(result).toBe("fc");
  });
});

日期函数

这里如何对日期进行验证呢?因为日期一直是变化的

/**
 * 检测今天是否为周五
 * @returns 如果今天是周五返回 "开心",否则返回 "不开心"
 */
export function checkFriday(): string {
  const today = new Date();
  console.log(today.getDay());
  if (today.getDay() === 5) {
    return "happy";
  } else {
    return "sad";
  }
}

验证

这里使用的是模拟定时器,使用vi.useFakeTimers(),注意:这里在测试结束后需要使用vi.useRealTimers();移除掉,然后我们使用vi.setSystemTime(new Date(2023, 3, 21));去设置系统时间

import { beforeEach, afterEach, vi, it, expect, describe } from "vitest";
import { checkFriday } from "./date";

describe("date", () => {
  beforeEach(() => {
    vi.useFakeTimers();
  });

  afterEach(() => {
    vi.useRealTimers();
  });
  it("should be happy when it's Friday", () => {
    vi.setSystemTime(new Date(2023, 3, 21));

    const result = checkFriday();

    expect(result).toBe("happy");
  });
  it("should be sad when it's not Friday", () => {
    vi.setSystemTime(new Date(2023, 3, 22));

    const result = checkFriday();

    expect(result).toBe("sad");
  });
  it("third", () => {
    checkFriday();
  });
});

十七、快速反馈-处理异步代码time、promise

定时器与延时器

export function sayHi() {
  setTimeout(() => {
    setInterval(() => {
      console.log("hi");
    }, 100);
  }, 1000);
}
export class User {
  id: string;

  constructor(id: string) {
    this.id = id;
  }

  fetchData(callback: (data: string) => void, delay: number): void {
    setTimeout(() => {
      const data = `Data for user with id: ${this.id}`;
      callback(data);
    }, delay);
  }

  fetchDataV2(callback: (data: string) => void): void {
    setTimeout(() => {
      const data = `Data for user with id: ${this.id}`;
      callback(data);
    }, 2000);
  }
}

验证

这里我们也是使用vi.useFakeTimers();去模拟定时器,这里我们有两种方式去进行验证,第一种就是使用vi.advanceTimersToNextTimer()vi.advanceTimersByTime(1100);这个是设置定时器的时间,第二种就是使用vi.spyOn(console, "log");去模拟打印方法,这种是比较推荐的

import { vi, it, expect, describe } from "vitest";
import { sayHi } from "./setInterval";

describe("setInterval", () => {
  it("should call one", () => {
    vi.useFakeTimers();
    vi.spyOn(console, "log");
    sayHi();
    //     vi.advanceTimersToNextTimer()
    //     vi.advanceTimersToNextTimer()
	//     vi.advanceTimersByTime(1100);

    expect(console.log).toBeCalledWith("hi");
  });
});
import { vi, it, expect, describe } from "vitest";
import { User } from "./setTimeout";

describe("setTimeout", () => {
  it("should fetch User data", () => {
    vi.useFakeTimers();
    const user = new User("1");

    const callback = vi.fn();
    user.fetchDataV2(callback);
    // vi.advanceTimersByTime(1000)
    // vi.advanceTimersToNextTimer();

    const userA = new User("1");

    const callbackA = vi.fn();
    userA.fetchDataV2(callbackA);
    // vi.advanceTimersToNextTimer();

    vi.runAllTimers();

    expect(callback).toBeCalledWith("Data for user with id: 1");
    expect(callbackA).toBeCalledWith("Data for user with id: 1");
  });
});

promise

export class View {
  count: number = 1;
  render() {
    Promise.resolve()
      .then(() => {
        this.count = 2;
      })
      .then(() => {
        this.count = 3;
      });
  }
}

export function fetchUserData() {
  return new Promise((resolve, reject) => {
    resolve("1");
  });
}

export function delay(time: number) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("ok");
    }, time);
  });
}

验证

import { vi, it, expect, describe } from "vitest";
import { delay, fetchUserData } from "./index";

describe("Promise", () => {
  it("normal", async () => {
    const result = await fetchUserData();

    expect(result).toBe("1");
  });

  it("delay", async () => {
    vi.useFakeTimers();
    //     vi.advanceTimersToNextTimer()
    //     const result = await delay(1000);
    const result = delay(100);
    vi.advanceTimersToNextTimer();

    expect(result).resolves.toBe("ok");
  });
});

第二种是结合flush-promises这个库来使用

import { it, expect, describe } from "vitest";
import { View } from "./view";
import flushPromises from "flush-promises";

describe("View", () => {
  it("should change count", async () => {
    const view = new View();

    view.render();
    await flushPromises();

    expect(view.count).toBe(3);
  });
});

十八、API的多种测试方案

1、直接mock axios

直接上代码,通过直接对axios进行mock来定义返回结果---------不推荐使用

import { test, expect, vi } from "vitest";
import { useTodoStore } from "./todo";
import { setActivePinia, createPinia } from "pinia";
import axios from "axios";

vi.mock("axios");

test("add todo", async () => {
  // 准备数据
  vi.mocked(axios.post).mockImplementation((path, { title }: any) => {
    return Promise.resolve({
      data: { data: { todo: { id: 1, title } }, state: 1 },
    });
  });
  setActivePinia(createPinia());
  const todoStore = useTodoStore();
  const title = "吃饭";

  // 调用
  await todoStore.addTodo(title);

  // 验证
  expect(todoStore.todos[0].title).toBe(title);
});

test("should not add todo when title is empty string", async () => {
  setActivePinia(createPinia());
  const todoStore = useTodoStore();
  const title = "";

  // 调用
  await todoStore.addTodo(title);

  // 验证
  expect(todoStore.todos.length).toBe(0);
});

test("remove todo", async () => {
  vi.mocked(axios.post).mockImplementationOnce((path, { title }: any) => {
    return Promise.resolve({
      data: { data: { todo: { id: 1, title } }, state: 1 },
    });
  });
  vi.mocked(axios.post).mockImplementationOnce((path, { id }: any) => {
    return Promise.resolve({
      data: { data: { id }, state: 1 },
    });
  });

  setActivePinia(createPinia());
  const todoStore = useTodoStore();
  const todo = await todoStore.addTodo("吃饭"); // round-trip

  // 调用
  await todoStore.removeTodo(todo!.id);

  // 验证
  expect(todoStore.todos.length).toBe(0);
});

test("should throw error when removed id does not exist ", async () => {
  // 准备数据
  vi.mocked(axios.post).mockImplementationOnce((path, { id }: any) => {
    return Promise.resolve({
      data: { data: null, state: 0 },
    });
  });

  setActivePinia(createPinia());
  const todoStore = useTodoStore();

  expect(async () => {
    // 调用
    await todoStore.removeTodo(2);
    // 抛出一个错误
  }).rejects.toThrowError("id:2 不存在");
});

test("update todo list", async () => {
  const todoList = [{ id: 1, title: "写代码" }];
  vi.mocked(axios.get).mockResolvedValue({ data: { data: { todoList } } });

  setActivePinia(createPinia());
  const todoStore = useTodoStore();
  await todoStore.updateTodoList();

  expect(todoStore.todos[0].title).toBe("写代码");
});

2、mock中间层

这个的意思指的是,只对行为进行验证,行为产生的结果进行mock-----------推荐使用

import { test, expect, vi } from "vitest";
import { useTodoStore } from "./todo";
import { setActivePinia, createPinia } from "pinia";
import { fetchAddTodo, fetchRemoveTodo, fetchTodoList } from "../api";

vi.mock("../api");

// SUT  create list
// create list
// add todo to todos , todos' length is 1
test("should add todo to the list when successful", async () => {
  // 准备数据
  vi.mocked(fetchAddTodo).mockImplementation((title) => {
    return Promise.resolve({
      data: { todo: { id: 1, title } },
      state: 1,
    });
  });
  setActivePinia(createPinia());
  const todoStore = useTodoStore();
  const title = "吃饭";

  // 调用
  await todoStore.addTodo(title);

  // 验证
  expect(todoStore.todos[0].title).toBe(title);


});

test("should not be added todo when network is error", async () => {
  // 准备数据
  vi.mocked(fetchAddTodo).mockImplementation((title) => {
    return Promise.reject("network error");
  });
  setActivePinia(createPinia());
  const todoStore = useTodoStore();
  const title = "吃饭";

  // 调用
  expect(async () => {
    await todoStore.addTodo(title);
  }).rejects.toThrowError("network error");
});

test("should not add a todo when title is empty", async () => {
  // 准备数据
  setActivePinia(createPinia());
  const todoStore = useTodoStore();
  const title = "";

  // 调用
  await todoStore.addTodo(title);

  // 验证
  expect(todoStore.todos.length).toBe(0);
});

test("remove todo when todo is", async () => {
  // 准备数据
  vi.mocked(fetchAddTodo).mockImplementation((title) => {
    return Promise.resolve({
      data: { todo: { id: 1, title } },
      state: 1,
    });
  });
  vi.mocked(fetchRemoveTodo).mockImplementationOnce((id) => {
    return Promise.resolve({
      data: { id },
      state: 1,
    });
  });

  setActivePinia(createPinia());
  const todoStore = useTodoStore();
  const todo = await todoStore.addTodo("吃饭"); // round-trip

  // 调用
  await todoStore.removeTodo(todo!.id);

  // 验证
  expect(todoStore.todos.length).toBe(0);
});

test("should throw a error when removed id does not exist", async () => {
  // 准备数据
  vi.mocked(fetchRemoveTodo).mockImplementationOnce(() => {
    return Promise.resolve({
      data: null,
      state: 0,
    });
  });

  setActivePinia(createPinia());
  const todoStore = useTodoStore();

  // 调用
  expect(async () => {
    await todoStore.removeTodo(2);
  }).rejects.toThrowError("id:2 does not exist");
});

test("update todo list", async () => {
  const todoList = [{ id: 1, title: "写代码" }];
  vi.mocked(fetchTodoList).mockResolvedValue({ data: { todoList } });

  setActivePinia(createPinia());
  const todoStore = useTodoStore();
  await todoStore.updateTodoList();

  expect(todoStore.todos[0].title).toBe("写代码");
});

3、使用mock server worker

这里指的是使用msw这个库来mock返回结果,相当于利用中间件去测试

缺点:就是需要去学习新的库,不太推荐使用

import { beforeAll, afterEach, afterAll, test, expect} from "vitest";
import { useTodoStore } from "./todo";
import { setActivePinia, createPinia } from "pinia";
import { server } from "../mocks/server";
import { mockAddTodo, mockRemoveTodo, mockTodoList } from "../mocks/handlers";


test.todo("sad path")

test("add todo", async () => {
  // koa express
  server.use(mockAddTodo());

  setActivePinia(createPinia());
  const todoStore = useTodoStore();
  const title = "吃饭";

  // 调用
  await todoStore.addTodo(title);

  // 验证
  expect(todoStore.todos[0].title).toBe(title);
});

test("remove todo", async () => {
  server.use(mockAddTodo(), mockRemoveTodo());

  setActivePinia(createPinia());
  const todoStore = useTodoStore();
  const todo = await todoStore.addTodo("吃饭"); // round-trip

  // 调用
  await todoStore.removeTodo(todo!.id);

  // 验证
  expect(todoStore.todos.length).toBe(0);
});

test("update todo list", async () => {
  const todoList = [{ id: 1, title: "写代码" }];
  server.use(mockTodoList(todoList));

  setActivePinia(createPinia());
  const todoStore = useTodoStore();
  await todoStore.updateTodoList();

  expect(todoStore.todos[0].title).toBe("写代码");
});

十九、参数化验证

提供在多个测试case中复用相同的测试逻辑的方法

比如我们想去验证一个正则表达式

export function emailValidator(email: string): boolean {
  const regex = /^[\w-]+(\.[\w-]+)*@([\w-]+\.)+[a-zA-Z]{2,7}$/;
  return regex.test(email);
}

可能我们的测试case非常多,可以看到非常多重复的case

import { emailValidator } from "./emailValidator"
import { it, expect, describe } from "vitest"

describe("emailValidator", () => {
  it("should return true for valid email", () => {
    const email = "valid-email@example.com"
    expect(emailValidator(email)).toBe(true)
  })

  it("should return false for invalid email without domain extension", () => {
    const email = "invalid.email@example"
    expect(emailValidator(email)).toBe(false)
  })

  it("should return false for invalid email with extra dot at the end", () => {
    const email = "another.invalid.email@example."
    expect(emailValidator(email)).toBe(false)
  })

  it("should return false for invalid email with missing '@'", () => {
    const email = "yet.another.invalid.email.example.com"
    expect(emailValidator(email)).toBe(false)
  })
})

解决办法:我们可以利用工具提供的方法

import { emailValidator } from "./emailValidator"
import { it, expect, describe } from "vitest"

describe("emailValidator", () => {
  it.each([
    ["valid-email@example.com", true],
    ["invalid.email@example", false],
    ["another.invalid.email@example.", false],
    ["yet.another.invalid.email.example.com", false],
  ])("should return %s when email is %s", (email, expected) => {
    expect(emailValidator(email)).toBe(expected)
  })

  it.each([{ email: "valid-email@example.com", expected: true }])(
    "should return $email when email is $expected",
    ({ email, expected }) => {
      console.log(email, expected)
      expect(emailValidator(email)).toBe(expected)
    }
  )

  it.each`
    email                        | expected
    ${"valid-email@example.com"} | ${true}
    ${"invalid.email@example"}   | ${false}
  `("should return $email when email is $expected", ({ email, expected }) => {
    console.log(email, expected)
    expect(emailValidator(email)).toBe(expected)
  })

  it.each`
    email             | expected
    ${{ a: "aaaaa" }} | ${true}
    ${[]}             | ${true}
    ${false}          | ${true}
  `("should return $email.a when email is $expected", ({ email, expected }) => {
    console.log(email, expected)
    expect(false).toBe(true)
    //     expect(emailValidator(email)).toBe(expected);
  })
})

我们使用it.each,并且使用模版字符串的语法,在参数中,我们还可以使用$email.a去获取参数,其中第三种和第四种方法是最佳实践

第一种方法因为在执行过程中,不太好查看错误信息,所以不推荐使用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值