Каков современный уровень тестирования / проверки функций в модуле в 2018 году? - PullRequest
0 голосов
/ 12 декабря 2018

У меня есть модуль для целей тестирования обучения, который выглядит следующим образом:

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 и parseUser.В то же время - я не хочу, чтобы поведение parseUser оставалось насмешливым, - когда я действительно проверяю это.

Я столкнулся с проблемой, заключающейся в том, что не представляется возможным издеваться / шпионить за функциями в одном модуле.

Вот ресурсы, о которых я читал:

Как смоделировать конкретную функцию модуля?Выпуск Jest Github. (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 году, которые специально решают эту проблему?

1 Ответ

0 голосов
/ 13 декабря 2018

Как вы уже обнаружили, попытка напрямую протестировать модуль 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);
    });
});

Следует отметить, что я не проверял, работает ли предоставленный код, но, надеюсь, концепция достаточно ясна.

...