在单元测试期间我应该模拟哪些功能 [英] What functions should I mock during unit testing

查看:36
本文介绍了在单元测试期间我应该模拟哪些功能的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我一直在阅读一些文章,并在 Stack Overflow 上发帖讨论什么时候应该模拟一个函数,什么时候不应该,但我有一个案例,我不确定该怎么做.

I've been reading some articles, and posts here on Stack Overflow about when I should mock a function and when I shouldn't, but I have a case where I'm not sure about what to do.

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

I have a UserService class which uses dependency injection concept to receive dependencies through its constructor.

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 类时使用该对象

I want to test if the createUser method works properly, and for my tests, I created a fake userRepository which is basically a object with mocked methods that I will use while instantiating UserService Class

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类的方法,这就是我感到困惑的地方.

Note that in the createUser method, there is a call to the getUserByEmail method which is also a method of UserService class, and that is where I got confused.

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

Should I mock the getUserByEmail method even it is a method of the class I'm testing? If it is not the correct approach, what should I do?

推荐答案

你应该几乎总是喜欢而不是来模拟你应该测试的部分,在这种情况下 用户服务.为了说明原因,请考虑以下两个测试:

You should almost always prefer not to mock parts of the thing you're supposed to be testing, in this case UserService. To illustrate why, consider these two tests:

  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");
});

  • 存根服务自己的 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");
    });
    

  • 对于您当前的实现,两者都通过得很好.但让我们想想事情可能会发生什么变化.

    For your current implementation, both pass just fine. But let's think about how things might change.

    想象一下,我们需要在某个时候丰富getUserByEmail提供的用户模型:

    Imagine we need to enrich the user model getUserByEmail provides at some point:

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

    显然我们不需要这些额外的数据只是为了知道用户是否存在,所以我们考虑了基本的用户对象检索:

    Obviously we don't need this extra data just to know whether or not the user exists, so we factor out the basic user object retrieval:

    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,即使代码仍然执行相同的操作,它现在也失败了.这是一个假阴性;功能正常,但测试失败.

    If we were using test 1, we wouldn't have to change it at all - we're still consuming findByEmail on the repo, the fact that the internal implementation has changed is opaque to our test. But with test 2, that's now failing even though the code still does the same things. This is a false negative; the functionality works but the test fails.

    事实上,您可以应用该重构,提取 _getUser,然后在新功能使需求变得如此清晰之前;createUser 使用 getUserByEmail 的事实直接反映了 偶然 this.userRepository.findByEmail(email) 的重复 - 他们有改变的不同原因.

    In fact you could have applied that refactor, extracting _getUser, prior to a new feature making the need so clear; the fact that createUser uses getUserByEmail directly reflects accidental duplication of this.userRepository.findByEmail(email) - they have different reasons to change.

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

    Or imagine we make some change that breaks getUserByEmail. Let's simulate a problem with the enrichment, for example:

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

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

    If we're using test 1, our test for createUser fails too, but that's the correct outcome! The implementation is broken, a user cannot be created. With test 2 we have a false positive; the test passes but the functionality doesn't work.

    在这种情况下,您可以说最好看到 only getUserByEmail 失败,因为这就是问题所在,但我认为那会很好当您查看代码时会感到困惑:"createUser 也调用了该方法,但测试表明它很好...".

    In this case, you could say that it's better to see that only getUserByEmail is failing because that's where the problem is, but I'd contend that would be pretty confusing when you looked at the code: "createUser calls that method too, but the tests say it's fine...".

    这篇关于在单元测试期间我应该模拟哪些功能的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

    查看全文
    登录 关闭
    扫码关注1秒登录
    发送“验证码”获取 | 15天全站免登陆