首页 > 解决方案 > 在单元测试期间我应该模拟哪些功能

问题描述

我一直在阅读一些文章,并在 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 方法,即使它是我正在测试的类的方法?如果这不是正确的方法,我该怎么办?

标签: javascriptunit-testingjestjsmocking

解决方案


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

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

    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,我们根本不需要更改它- 我们仍在使用findByEmailrepo,内部实现已更改的事实对我们的测试是不透明的。但是对于测试 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也调用该方法,但测试说没关系……”


推荐阅读