在2018年测试模块中的功能/模拟功能的最新技术水平是什么?

时间:2018-12-12 05:47:59

标签: javascript unit-testing jestjs babel-jest

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

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的行为受到嘲笑-在我实际进行测试时。

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

以下是我已阅读的资源:

How to mock a specific module function? Jest github issue.(超过100个大拇指)。

我们被告知的地方:

  

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

     

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

通过对象引用功能

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

https://luetkemj.github.io/170421/mocking-modules-in-jest/

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

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; 

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

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

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

将对象分成模块

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

例如

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}`;
}

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

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

还有其他选择:

函数中的依赖项注入:

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

这个看起来丑陋,imo。

使用babel-rewire插件

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

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

将测试分为多个文件

我正在对此进行调查。

我的问题:这都是一种令人沮丧且愚蠢的测试方式-是否存在人们在2018年编写单元测试的标准,简便易行的方式来专门解决此问题? < / p>

1 个答案:

答案 0 :(得分:1)

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

async function makeApiCall(uri) {
    ...
}

module.exports.makeApiCall = makeApiCall;

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


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

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


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

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

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类创建一些测试:

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

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