2018 年模块内测试/模拟功能的最新技术是什么?

2024-04-24

我有一个用于学习测试的模块,如下所示:

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方法,为此我想模拟/监视两者fetchUser and parseUser。同时 - 我不想要这样的行为parseUser保持嘲笑 - 当我实际测试时。

我遇到的问题是,似乎不可能模拟/监视同一模块中的函数。

以下是我读过的有关它的资源:

如何模拟特定模块功能?笑话 github 问题。 https://github.com/facebook/jest/issues/936(100+ 竖起大拇指)。

我们被告知:

在 JavaScript 中,通过在请求模块后模拟函数来支持上述功能是不可能的——(几乎)没有办法检索 foo 引用的绑定并修改它。

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

通过对象引用函数

他提出的解决方案是 ES5 - 但这篇博文中描述了现代的等效解决方案:

https://luetkemj.github.io/170421/mocking-modules-in-jest/ 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://groups.google.com/forum/#!topic/sinonjs/bPZYl6jjMdg https://stackoverflow.com/a/45288360/1068446 https://stackoverflow.com/a/45288360/1068446

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

将对象分解为模块

另一种方法是,我将分解我的模块,这样我就不会直接在彼此内部调用函数。

eg.

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

用户-api.js

import {makeApiCall} from "./api"; 

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

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

用户服务.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));
}

用户解析器.js

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

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

但我不确定像这样分解模块是否可行 - 我想可能存在循环依赖的情况。

有一些替代方案:

函数中的依赖注入:

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

在我看来,这个看起来很难看。

使用 babel-rewire 插件

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

我必须承认——我还没有看过这么多。

将您的测试拆分为多个文件

现在正在调查这个。

我的问题:这是一种相当令人沮丧且繁琐的测试方式 - 2018 年人们编写单元测试的方式是否有一种标准、好用且简单的方式来专门解决这个问题?


正如您已经发现的,尝试直接测试 ES6 模块是非常痛苦的。在您的情况下,听起来您正在转译 ES6 模块而不是直接测试它,这可能会生成如下所示的代码:

async function makeApiCall(uri) {
    ...
}

module.exports.makeApiCall = makeApiCall;

由于其他方法正在调用makeApiCall直接,而不是导出,即使您尝试模拟导出也不会发生任何事情。按照目前的情况,ES6 模块导出是不可变的,因此即使您没有转译该模块,您可能仍然会遇到问题。


将所有内容附加到“lib”对象可能是最简单的方法,但感觉像是一种黑客攻击,而不是解决方案。或者,使用可以重新连接模块的库是一种潜在的解决方案,但它非常做作,而且在我看来它有味道。通常,当您遇到这种类型的代码气味时,您就会遇到设计问题。

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


我的建议?创建类并使用它们进行测试,然后使模块成为类实例的薄包装器。由于您使用的是类,因此您将始终使用集中对象(thisobject),这将允许您模拟您需要的东西。使用类还可以让您有机会在构造类时注入数据,从而在测试中提供极其细粒度的控制。

让我们重构你的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 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);
    });
});

我应该注意,我还没有测试所提供的代码是否有效,但希望这个概念足够清晰。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

2018 年模块内测试/模拟功能的最新技术是什么? 的相关文章

随机推荐