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 年模块内测试/模拟功能的最新技术是什么? 的相关文章

  • 计算字符串中的唯一单词

    下面我尝试将字符串数组提供给一个函数 该函数将唯一单词添加到单词数组中 并且如果该单词已经在数组中 则增加计数数组中相应元素的计数 var words var counts calculate a b calculate a c funct
  • Firebase Auth - 最近登录多长时间

    我有一个个人资料选项卡 用户可以在其中按编辑并编辑他们的个人资料 我只想在必要时才需要他们的密码 所以想知道用户登录的时间是多少毫秒 这使得它不是最近登录 其中firebase会抛出错误 auth requires recent login
  • 当标题中包含“&”时,电子邮件标题无法正确显示,如何在 JavaScript 中修复?

    我有一些代码以以下格式显示文章标题列表 简短描述和作者姓名 标题 作者姓名 描述 作者的姓名和描述与此处无关 因为它们始终显示正确 大多数标题也可以正确显示 以下是一些虚构的示例 关于银行业务您需要了解的最重要的一件事 作者姓名 正确显示
  • Excel 宏与 Javascript

    我希望使用 Javascript 中的宏而不是默认的 VBA 来操作 Excel 电子表格 我可以使用以下 VBA 代码执行 javascript 代码 javascript to execute Dim b As String b fun
  • 根据传递的参数覆盖 Javascript 函数

    是否可以根据传递给函数的参数数量来重写函数 例如 function abc name document write My name is name function abc name friend document write My nam
  • 所有事件的 HTML5 EventSource 监听器?

    我使用 EventSource 在 JavaScript 客户端应用程序中推送通知 我可以像这样附加事件监听器 source addEventListener my custom event type function e console
  • 以编程方式在指令内添加指令

    我想将指令的另一个实例附加到父指令中 但我无法使用 apply 重新编译我的指令 我想我在某个地方错过了一些东西 我的 HTML 代码 div div div div
  • 如何在 HTML 表格上使用分页?

    我正在尝试使用这个分页library http flaviusmatis github io simplePagination js 在我的 HTML 表格页面 特别是浅色主题 中 但不知何故 我无法理解如何在我的 HTML 页面中以这种方
  • 将事件添加到 Google Maps API InfoWindow 内的元素

    我想在 Google Maps API v3 InfoWindow 内放置一个带有输入字段和提交按钮的表单 提交后 我想调用一个函数 该函数使用输入字段中输入的地址启动方向服务 这是我的代码 我目前只测试方向事件是否被触发 我已经编写了完整
  • 修剪日期格式 PrimeNG 日历 - 删除时间戳、角度反应形式

    我将以下内容推入我的反应形式 obj 中2016 01 01T00 00 00 000Z但我想要以下2016 01 01 有谁知道有一个内置函数可以实现上述目的 我已经搜索过文档here https www primefaces org p
  • 如何避免 TypeScript 中出现虚假的“未使用参数”警告

    我遇到过很多次这种情况 最后决定弄清楚正确的方法是什么 如果我有一个声明方法的抽象父类 然后一些具体子类在其实现中实现真正的逻辑 并且显然使用方法参数 但某些子类不需要在该方法中执行任何操作 因此不要使用方法参数 那些不必执行任何操作的方法
  • 如何在 Angular 2 应用程序中使 DateAdapter 单例?

    我正在开发一个带有延迟加载模块的 Angular 7 应用程序 我也使用有角度的材料组件 我想在日期选择器组件中本地化并支持多个区域设置 当应用程序语言发生变化时 我想在整个应用程序中全局更改它 可以通过 DateAdapter setLo
  • Gmail 和 Google Chrome 12+ 中的“从剪贴板粘贴图像”功能如何工作?

    我注意到一个来自 Google 的博文 http gmailblog blogspot com 2011 06 pasting images into messages just got html其中提到 如果您使用的是最新版本的 Chro
  • 如何使用 jest 通过 Promise.all 设置多次提取测试

    我在测试中使用 jest 我正在使用 React 和 Redux 并且执行以下操作 function getData id notify return dispatch gt dispatch anotherFunction Promise
  • 如何更改元素的 CSS 类并在单击时删除所有其他类

    我如何处理 AngularJS 2 中的一种情况 即单击一个元素需要更改其自己的样式 并且如果其他元素具有该样式 则需要将其删除 最好在一个函数中 如同Angular js 如何在单击时更改元素 css 类并删除所有其他元素 https s
  • 我们如何使用 thymeleaf 绑定对象列表的列表

    我有一个表单 用户可以在其中添加任意数量的内容表对象这也可以包含他想要的列对象 就像在 SQL 中构建表一样 我尝试了下面的代码 但没有任何效果 并且当我尝试绑定两个列表时 表单不再出现 控制器 ModelAttribute page pu
  • 什么是 TinyMCE jQuery 包?

    我被要求在项目中使用 TinyMCE 编辑器 在下载页面上 有一个主包 然后是一个 jQuery 包 This package contains special jQuery build of TinyMCE and a jQuery in
  • YouTube iFrame Player API 无法在 DOMWindow 上执行 postMessage

    我正在尝试使用以下命令将 youtube 加载到我的网页中YouTube iFrame Player API 并在加载时出现以下错误 Failed to execute postMessage on DOMWindow The target
  • Javascript/jQuery 外部高度()

    Does idOfLememt outerHeight 对所有浏览器产生相同的结果 IE7 有什么不同吗 只要去http api jquery com outerHeight http api jquery com outerHeight
  • JavaScript 中“键”的类型是什么?

    当我失去焦点并开始思考一个愚蠢的问题时 我遇到了这样的时刻 var a b value b 的类型是什么 我的意思不是 值 的类型 而是标记为 b 的实际键 背景 当我必须创建一个字符串键时 我开始想知道这一点 var a b value

随机推荐