【问题标题】:What functions should I mock during unit testing在单元测试期间我应该模拟哪些功能
【发布时间】:2021-06-18 15:39:18
【问题描述】:

我一直在阅读一些文章,并在 Stack Overflow 上发布了关于何时应该模拟函数以及何时不应该模拟函数的帖子,但我有一个案例我不确定该怎么做。

我有一个 UserService 类,它使用依赖注入概念通过其构造函数接收依赖项。

class UserService {

 constructor(userRepository) {
    this.userRepository = userRepository;
 }

 async getUserByEmail(userEmail) {
    // would perform some validations to check if the value is an e-mail

    const user = await this.userRepository.findByEmail(email);

    return user;
 }

 async createUser(userData) {
    const isEmailInUse = await this.getUserByEmail(userData.email);

    if(isEmailInUse) {
        return "error";
    } 

    const user = await this.userRepository.create(userData);

    return user;
 }

} 

我想测试 createUser 方法是否正常工作,为了我的测试,我创建了一个假的 userRepository,它基本上是一个带有模拟方法的对象,我将在实例化 UserService 类时使用它

const UserService = require('./UserService.js');

describe("User Service tests", () => {

let userService;
let userRepository;

beforeEach(() => {
    userRepository = {
        findOne: jest.fn(),
        create: jest.fn(),
    }

    userService = new UserService(userRepository);
});

afterEach(() => {
    resetAllMocks();
});

describe("createUser", () => {

    it("should be able to create a new user", async () => {
        const newUserData = { name: 'User', email: 'user@test.com.br' }
        const user = { id: 1, name: 'User', email: 'user@test.com.br' }

        userRepository.create.mockResolvedValue(user);

        const result = await userService.createUser();

        expect(result).toStrictEqual(user);
    })
})

})

请注意,在 createUser 方法中,调用了 getUserByEmail 方法,该方法也是 UserService 类的方法,这就是我感到困惑的地方。

我是否应该模拟 getUserByEmail 方法,即使它是我正在测试的类的方法?如果这不是正确的方法,我该怎么办?

【问题讨论】:

  • 这个问题是相当基于意见的,但我想说只有模拟存储库才能让你的整个班级更容易测试。只有一件事要模拟,这样您可以检查您的类方法是否协同工作以提供预期的结果(当您尝试创建现有用户时返回 "error"
  • @blex 我担心的是,如果 getUserByEmail 方法有一些错误,即使问题不在函数本身,createUser 测试也可能失败
  • 总是视情况而定。您可以将它们视为不同的单元并单独测试,或者将它们视为一个并一起测试。由于您模拟了依赖项,因此这减少了移动部件的数量,因此即使问题不在函数本身,失败的风险也会降低。无论如何,您都可以监视某个方法以确保调用符合您的期望。

标签: javascript unit-testing jestjs mocking


【解决方案1】:

您不应该模拟任何这些函数,因为它会创建用户并从数据库中读取数据。如果你嘲笑他们,那么测试的意义何在。换句话说,您不会知道您的应用程序是否与数据库一起正常工作。无论如何,我会模拟功能,例如发送电子邮件的功能等。不要模拟作为应用程序核心的函数。您应该有一个用于测试的数据库和另一个用于生产的数据库。

【讨论】:

  • 这就是系统测试(或集成测试)的用途。这里,OP 说的是单元测试
【解决方案2】:

您应该几乎总是希望 来模拟您应该测试的部分内容,在这种情况下为 UserService。为了说明原因,请考虑以下两个测试:

  1. 在 repo 对象上为 findByEmail 提供测试双重实现:

    it("throws an error if the user already exists", async () => {
        const email = "foo@bar.baz";
        const user = { email, name: "Foo Barrington" };
        const service = new UserService({
            findByEmail: (_email) => Promise.resolve(_email === email ? user : null),
        });
    
        await expect(service.createUser(user)).rejects.toThrow("User already exists");
    });
    
  2. 存根服务自己的getUserByEmail 方法:

    it("throws an error if the user already exists", async () => {
        const email = "foo@bar.baz";
        const user = { email, name: "Foo Barrington" };
        const service = new UserService({});
        service.getUserByEmail = (_email) => Promise.resolve(_email === email ? user : null);
    
        await expect(service.createUser(user)).rejects.toThrow("User already exists");
    });
    

对于您当前的实现,两者都可以通过。但是让我们想想事情可能会发生怎样的变化。


假设我们需要丰富getUserByEmail 在某个时候提供的用户模型:

async getUserByEmail(userEmail) {
    const user = await this.userRepository.findByEmail(userEmail);
    user.moreStuff = await.this.userRepository.getSomething(user.id);
    return user;
}

显然我们不需要这些额外的数据来知道用户是否存在,所以我们将基本的用户对象检索分解:

async getUserByEmail(userEmail) {
    const user = await this._getUser(userEmail);
    user.moreStuff = await.this.userRepository.getSomething(user.id);
    return user;
}

async createUser(userData) {
    if (await this._getUser(userData.email)) {
        throw new Error("User already exists");
    }
    return this.userRepository.create(userData);
}

async _getUser(userEmail) {
    return this.userRepository.findByEmail(userEmail);
}

如果我们使用测试 1,我们根本不必更改它 - 我们仍然在 repo 上使用 findByEmail,内部实现已更改的事实是不透明的到我们的测试。但是对于测试 2,即使代码仍然执行相同的操作,它现在也失败了。这是一个假阴性;功能有效,但测试失败。

事实上,您可以应用该重构,提取_getUser,在新功能明确需求之前; createUser 使用 getUserByEmail 的事实直接反映了this.userRepository.findByEmail(email)意外重复 - 它们有不同的更改理由。


或者想象我们做了一些改变打破getUserByEmail。让我们模拟一个富集问题,例如:

async getUserByEmail(userEmail) {
    const user = await this.userRepository.findByEmail(userEmail);
    throw new Error("lol whoops!");
    return user;
}

如果我们使用测试 1,我们对 createUser 的测试也会失败,但这是正确的结果!实现已损坏,无法创建用户。对于测试 2,我们有 误报;测试通过,但功能不起作用。

在这种情况下,您可以说最好看到 only getUserByEmail 失败,因为这就是问题所在,但我认为当您查看时会非常混乱代码:"createUser 也调用了那个方法,但是测试说没问题...".

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-10-13
    • 2016-01-27
    • 2021-11-22
    • 1970-01-01
    • 1970-01-01
    • 2012-03-30
    • 2018-06-16
    相关资源
    最近更新 更多