Skip to main content

行为验证

什么是行为验证

行为验证是指,验证对象之间的交互是否按照预期执行。

对象间的交互是指,调用函数方法和调用对象的方法。

行为验证的背后逻辑是:

  • 状态的改变是由特定行为导致的。
  • 所以如果我们所有的行为都是正确的,那就可以推断出状态也是正确的。

如何获取交互信息

行为验证需要知道一些交互信息,比如

  • 函数是否被调用
  • 函数的参数是什么
  • 函数被调用了几次
  • ...

我们需要通过 mock 来获取交互信息。

行为测试代码示例

测试注册用户模块

// database.ts

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

export class Database {
private dataStore: User[] = [];

addUser(user: User): void {
this.dataStore.push(user);
}

getUser(id: number): User | undefined {
return this.dataStore.find((user) => user.id === id);
}
}
// UserService.ts

import { Database, User } from "./database";

export class UserService {
constructor(private database: Database) {}

createUser(name: string): User {
const id = Math.floor(Math.random() * 1000);
const newUser: User = { id, name };

this.database.addUser(newUser);

return newUser;
}
}

那如何用行为验证的方式来测试上述代码呢?

it("should create a new user", () => {
const database = new Database();

vi.spyOn(database, "addUser"); // 第二个参数是方法名
console.log(database.addUser); // 会发现 addUser 上被 Vitest 挂载了很多方法

const userService = new UserService(database);

userService.createUser("nansen");

expect(database.addUser).toBeCalled();
});

当然处理方式不至上面这一种。

测试调用第三方库进行登录的功能

比如,我们通过调用第三方库实现了一个登录逻辑。

这样我们就拿不到最终的状态,只能去验证其行为。

vi.mock("moduleName", () => {
return {
login: vi.fn(),
};
});

describe("login", () => {
it("login function should be called from moduleName", async () => {
login("username", "password");

// 验证被调用
expect(login).toBeCalled();

// 验证参数
expect(login).toBeCalledWith("username", "password");

// 验证被调用几次
expect(login).toBeCalledTimes(1);
});
});

一般在验证完行为后,仍然要进行验证状态。

比如在该例子中,我们应该接着验证登录状态 loginState

行为验证的缺点

  1. 破坏了封装性

    行为验证是白盒测试,暴露了内部代码的实现细节。

    如果之后重构了代码,单元测试中的行为验证也需要维护。

  2. 丧失了测试的有效性

    由于我们只是假定了执行完某个交互后,状态是正确的。

    比如在上面例子中,就是假定了 login 函数被执行后,就登录成功了。

    这样即使 login 函数的内部实现出现了错误,这个行为测试也仍然会通过,这样就丧失了测试的有效性。

由于使用了 mock,有时会出现测试通过了,但实际功能有问题的情况。

这就是为什么尽量不要使用行为验证的原因。

但也有不得不使用行为验证的场景。

何时使用行为验证

只有在找不到状态来验证,或者状态非常难获取的时候,才采用行为测试。

比如,

  1. 调用了一个后端 API,但该 API 不会返回任何值。

  2. 要验证数据是否被存入数据库的话,采用状态验证,就需要去访问数据库,为了节省反复访问数据库带来的时间成本,可以改成用行为验证。

    在该例子中,就可以看是否调用了数据库的保存操作。