2018年在模块中测试/模拟功能的最新状态是什么? [英] What is the state of the art for testing/mocking functions within a module in 2018?

查看:117
本文介绍了2018年在模块中测试/模拟功能的最新状态是什么?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个用于学习测试的模块,看起来像这样:

I have a module, for the purposes of learning testing, that looks like this:

api.js

import axios from "axios";

const BASE_URL = "https://jsonplaceholder.typicode.com/";
const URI_USERS = 'users/';

export async function makeApiCall(uri) {
    try {
        const response = await axios(BASE_URL + uri);
        return response.data;
    } catch (err) {
        throw err.message;
    }
}

export async function fetchUsers() {
    return makeApiCall(URI_USERS);
}

export async function fetchUser(id) {
    return makeApiCall(URI_USERS + id);
}

export async function fetchUserStrings(...ids) {
    const users = await Promise.all(ids.map(id => fetchUser(id)));
    return users.map(user => parseUser(user));
}

export function parseUser(user) {
    return `${user.name}:${user.username}`;
}

相当简单的东西.

现在,我想测试该fetchUserStrings方法,并为此执行我想在fetchUserparseUser上进行模拟/间谍的操作.同时-我不希望parseUser的行为受到嘲笑-因为我实际上正在测试它.

Now I want to test that fetchUserStrings method, and to do that I want to mock/spy on both fetchUser and parseUser. At the same time - I don't want the behaviour of parseUser to stay mocked - for when I'm actually testing that.

我遇到了一个问题,即似乎不可能对同一模块中的函数进行模拟/监视.

I run in the problem that it seems that it is not possible to mock/spy on functions within the same module.

以下是我已阅读的资源:

Here are the resources I've read about it:

如何模拟特定的模块功能?开玩笑github问题.(100多个大拇指).

How to mock a specific module function? Jest github issue. (100+ thumbs up).

我们被告知的地方:

在JavaScript中无法通过在需要模块之后通过模拟函数来支持上述功能–几乎没有办法检索foo所引用的绑定并对其进行修改.

Supporting the above by mocking a function after requiring a module is impossible in JavaScript – there is (almost) no way to retrieve the binding that foo refers to and modify it.

jest-mock的工作方式是隔离运行模块代码,然后检索模块的元数据并创建模拟函数.同样,在这种情况下,它将无法修改foo的本地绑定.

The way that jest-mock works is that it runs the module code in isolation and then retrieves the metadata of a module and creates mock functions. Again, in this case it won't have any way to modify the local binding of foo.

通过对象引用功能

他提出的解决方案是ES5-但此博客文章中描述了现代的等效方法:

Refer to the functions via an object

The solution he proposes is ES5 - but the modern equivalent is described in this blog post:

https://luetkemj.github.io/170421/mocking-笑话模块/

在这里,不是通过直接调用我的函数,而是通过类似这样的对象来引用它们:

Where, instead of calling my functions directly, I refer to them via an object like:

api.js

async function makeApiCall(uri) {
    try {
        const response = await axios(BASE_URL + uri);
        return response.data;
    } catch (err) {
        throw err.message;
    }
}

async function fetchUsers() {
    return lib.makeApiCall(URI_USERS);
}

async function fetchUser(id) {
    return lib.makeApiCall(URI_USERS + id);
}

async function fetchUserStrings(...ids) {
    const users = await Promise.all(ids.map(id => lib.fetchUser(id)));
    return users.map(user => lib.parseUser(user));
}

function parseUser(user) {
    return `${user.name}:${user.username}`;
}

const lib = {
    makeApiCall, 
    fetchUsers, 
    fetchUser, 
    fetchUserStrings, 
    parseUser
}; 

export default lib; 

建议该解决方案的其他帖子:

Other posts that suggest this solution:

https://groups.google.com/forum/#!topic /sinonjs/bPZYl6jjMdg https://stackoverflow.com/a/45288360/1068446

这似乎是同一想法的一种变体: https://stackoverflow.com/a/47976589/1068446

And this one seems to be a variant of the same idea: https://stackoverflow.com/a/47976589/1068446

另一种选择是,我将模块分解,这样我就不会在彼此之间直接调用函数.

An alternative, is that I would break my module up, such that I'm never calling functions directly within each other.

例如

api.js

import axios from "axios";

const BASE_URL = "https://jsonplaceholder.typicode.com/";

export async function makeApiCall(uri) {
    try {
        const response = await axios(BASE_URL + uri);
        return response.data;
    } catch (err) {
        throw err.message;
    }
}

user-api.js

import {makeApiCall} from "./api"; 

export async function fetchUsers() {
    return makeApiCall(URI_USERS);
}

export async function fetchUser(id) {
    return makeApiCall(URI_USERS + id);
}

user-service.js

import {fetchUser} from "./user-api.js"; 
import {parseUser} from "./user-parser.js"; 

export async function fetchUserStrings(...ids) {
    const users = await Promise.all(ids.map(id => lib.fetchUser(id)));
    return ids.map(user => lib.parseUser(user));
}

user-parser.js

export function parseUser(user) {
    return `${user.name}:${user.username}`;
}

这样,我可以在测试依赖模块时模拟依赖模块,不用担心.

And that way I can mock the dependency modules when I'm testing the dependant module, no worries.

但是我不确定拆分这样的模块是否可行-我想在某些情况下您可能需要循环依赖.

But I'm not sure that breaking up the modules like this is even feasible - I imagine that there might be a circumstance where you have circular dependencies.

还有一些替代方法:

https://stackoverflow.com/a/47804180/1068446

这个看起来丑陋,imo.

This one looks ugly as though, imo.

https://stackoverflow.com/a/52725067/1068446

我必须承认-我没看那么多.

I have to admit - I haven't looked at this much.

现在正在调查这个.

我的问题:这都是一种令人沮丧而又古怪的测试方式-是否存在人们在2018年编写单元测试的标准,简便易行的方式来专门解决此问题?

推荐答案

您已经发现尝试直接测试ES6模块非常痛苦.在您的情况下,听起来好像是在编译ES6模块而不是直接对其进行测试,这可能会生成看起来像这样的代码:

As you've already discovered attempting to directly test an ES6 module is extremely painful. In your situation it sounds like you are transpiling the ES6 module rather than testing it directly, which would likely generate code that looks something like this:

async function makeApiCall(uri) {
    ...
}

module.exports.makeApiCall = makeApiCall;

由于其他方法是直接调用makeApiCall,而不是导出,因此即使您尝试模拟导出也不会发生任何事情.从目前的角度来看,ES6模块的导出是一成不变的,因此,即使您没有移植模块,您仍然可能会遇到问题.

Since the other methods are calling makeApiCall directly, rather than the export, even if you tried to mock the export nothing would happen. As it stands ES6 module exports are immutable, so even if you did not transpile the module you would likely still have issues.

将所有内容附加到"lib"对象可能是最简单的方法,但感觉就像是破解,而不是解决方案.另外,使用可以重新连接模块的库是一个潜在的解决方案,但它的功能非常强大,我认为它闻起来很香.通常,当您遇到这种类型的代码错误时,就会遇到设计问题.

Attaching everything to a "lib" object is probably the easiest way to get going, but it feels like a hack, not a solution. Alternatively using a library that can rewire the module is a potential solution, but its extremely hokey and in my opinion it smells. Usually when you're running into this type of code smell you have a design problem.

将模块拆分为小块感觉就像是穷人的依赖注入,正如您所说的那样,您可能会很快遇到问题.真正的依赖注入可能是最可靠的解决方案,但这是您需要从头开始构建的,不是您可以将其插入现有项目并期望立即进行工作的东西.

Splitting up the modules into tiny pieces feels like a poor mans dependency injection, and as you've stated you'll likely run into issues quickly. Real dependency injection is probably the most robust solution, but it's something you need to build from the ground up, it's not something that you can just plug into an existing project and expect to have things working immediately.

我的建议?创建类并将其用于测试,然后仅使模块成为该类实例的瘦包装.由于您使用的是类,因此将始终使用集中式对象(this对象)来引用方法调用,这将使您可以模拟所需的东西.使用类也将使您有机会在构造类时注入数据,从而在测试中提供极其精细的控制.

My suggestion? Create classes and use them for testing instead, then just make the module a thin wrapper over an instance of the class. Since you're using a class you'll always be referencing your method calls using a centralized object (the this object) which will allow you to mock out the things you need. Using a class will also give you an opportunity to inject data when you construct the class, giving you extremely fine grained control in your tests.

让我们重构您的api模块以使用一个类:

Let's refactor your api module to use a class:

import axios from 'axios';

export class ApiClient {
    constructor({baseUrl, client}) {
        this.baseUrl = baseUrl;
        this.client = client;
    }

    async makeApiCall(uri) {
        try {
            const response = await this.client(`${this.baseUrl}${uri}`);
            return response.data;
        } catch (err) {
            throw err.message;
        }
    }

    async fetchUsers() {
        return this.makeApiCall('/users');
    }

    async fetchUser(id) {
        return this.makeApiCall(`/users/${id}`);
    }

    async fetchUserStrings(...ids) {
        const users = await Promise.all(ids.map(id => this.fetchUser(id)));
        return users.map(user => this.parseUser(user));
    }

    parseUser(user) {
        return `${user.name}:${user.username}`;
    }
}

export default new ApiClient({
    url: "https://jsonplaceholder.typicode.com/",
    client: axios
});

现在让我们为ApiClient类创建一些测试:

Now lets create some tests for the ApiClient class:

import {ApiClient} from './api';

describe('api tests', () => {

    let api;
    beforeEach(() => {
        api = new ApiClient({
            baseUrl: 'http://test.com',
            client: jest.fn()
        });
    });

    it('makeApiCall should use client', async () => {
        const response = {data: []};
        api.client.mockResolvedValue(response);
        const value = await api.makeApiCall('/foo');
        expect(api.client).toHaveBeenCalledWith('http://test.com/foo');
        expect(value).toBe(response.data);
    });

    it('fetchUsers should call makeApiCall', async () => {
        const value = [];
        jest.spyOn(api, 'makeApiCall').mockResolvedValue(value);
        const users = await api.fetchUsers();
        expect(api.makeApiCall).toHaveBeenCalledWith('/users');
        expect(users).toBe(value);
    });
});

我应该注意,我尚未测试所提供的代码是否有效,但是希望这个概念很清楚.

I should note that I have not tested if the provided code works, but hopefully the concept is clear enough.

这篇关于2018年在模块中测试/模拟功能的最新状态是什么?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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