Подтвердить что ты не робот

Как имитировать функции в одном модуле с помощью шутки

Какой лучший способ правильно издеваться над следующим примером?

Проблема заключается в том, что после времени импорта foo хранит ссылку на исходный unmocked bar.

module.js:

export function bar () {
    return 'bar';
}

export function foo () {
    return `I am foo. bar is ${bar()}`;
}

module.test.js:

import * as module from '../src/module';

describe('module', () => {
    let barSpy;

    beforeEach(() => {
        barSpy = jest.spyOn(
            module,
            'bar'
        ).mockImplementation(jest.fn());
    });


    afterEach(() => {
        barSpy.mockRestore();
    });

    it('foo', () => {
        console.log(jest.isMockFunction(module.bar)); // outputs true

        module.bar.mockReturnValue('fake bar');

        console.log(module.bar()); // outputs 'fake bar';

        expect(module.foo()).toEqual('I am foo. bar is fake bar');
        /**
         * does not work! we get the following:
         *
         *  Expected value to equal:
         *    "I am foo. bar is fake bar"
         *  Received:
         *    "I am foo. bar is bar"
         */
    });
});

Спасибо!

EDIT: я мог бы изменить:

export function foo () {
    return `I am foo. bar is ${bar()}`;
}

to

export function foo () {
    return `I am foo. bar is ${exports.bar()}`;
}

но это p. уродливо, на мой взгляд, делать всюду:/

4b9b3361

Ответ 1

fwiw, решение, на котором я остановился, состояло в том, чтобы использовать вложение зависимостей, установив аргумент по умолчанию.

Поэтому я бы изменил

export function bar () {
    return 'bar';
}

export function foo () {
    return `I am foo. bar is ${bar()}`;
}

к

export function bar () {
    return 'bar';
}

export function foo (_bar = bar) {
    return `I am foo. bar is ${_bar()}`;
}

Это не является изменением API API моего компонента, и я могу легко переопределить бар в своем тесте, выполнив следующие

import { foo, bar } from '../src/module';

describe('module', () => {
    it('foo', () => {
        const dummyBar = jest.fn().mockReturnValue('fake bar');
        expect(foo(dummyBar)).toEqual('I am foo. bar is fake bar');
    });
});

Это может привести к еще более хорошему тестовому коду:)

Ответ 2

Проблема, похоже, связана с тем, как вы ожидаете, что диапазон бар будет разрешен.

С одной стороны, в module.js вы экспортируете две функции (вместо объекта, содержащего эти две функции). Из-за того, как экспортируются модули, ссылка на контейнер экспортируемых товаров exports, как вы упомянули.

С другой стороны, вы обрабатываете свой экспорт (который вы aliased module), как объект, содержащий эти функции, и пытаетесь заменить одну из его функций (функциональную панель).

Если вы внимательно посмотрите на свою реализацию foo, вы фактически сохраняете фиксированную ссылку на функцию бара.

Когда вы думаете, что вы заменили функцию бара новым, вы фактически заменили ссылочную копию в области вашего module.test.js

Чтобы foo фактически использовал другую версию бара, у вас есть две возможности:

  • В module.js экспортирует класс или экземпляр, содержащий оба метода foo и bar:

    Module.js:

    export class MyModule {
      function bar () {
        return 'bar';
      }
    
      function foo () {
        return `I am foo. bar is ${this.bar()}`;
      }
    }
    

    Обратите внимание на использование ключевого слова this в методе foo.

    Module.test.js:

    import { MyModule } from '../src/module'
    
    describe('MyModule', () => {
      //System under test :
      const sut:MyModule = new MyModule();
    
      let barSpy;
    
      beforeEach(() => {
          barSpy = jest.spyOn(
              sut,
              'bar'
          ).mockImplementation(jest.fn());
      });
    
    
      afterEach(() => {
          barSpy.mockRestore();
      });
    
      it('foo', () => {
          sut.bar.mockReturnValue('fake bar');
          expect(sut.foo()).toEqual('I am foo. bar is fake bar');
      });
    });
    
  • Как вы сказали, перепишите глобальную ссылку в глобальном контейнере exports. Это не рекомендуется, так как вы можете ввести странное поведение в других тестах, если вы не правильно reset экспортируете в исходное состояние.

Ответ 3

Альтернативным решением может быть импорт модуля в его собственный файл кода и использование импортированного экземпляра всех экспортированных объектов. Вот так:

import * as thisModule from './module';

export function bar () {
    return 'bar';
}

export function foo () {
    return `I am foo. bar is ${thisModule.bar()}`;
}

Теперь насмешка bar очень проста, потому что foo также использует экспортированный экземпляр bar:

import * as module from '../src/module';

describe('module', () => {
    it('foo', () => {
        spyOn(module, 'bar').and.returnValue('fake bar');
        expect(module.foo()).toEqual('I am foo. bar is fake bar');
    });
});

Импорт модуля в его собственный код выглядит странно, но из-за поддержки ES6 для циклического импорта он работает очень плавно.

Ответ 4

Если вы определяете свои экспорты, то можете ссылаться на свои функции как часть объекта экспорта. Затем вы можете переписать функции в ваших макетах по отдельности. Это связано с тем, что импорт работает как ссылка, а не как копия.

module.js:

exports.bar () => {
    return 'bar';
}

exports.foo () => {
    return 'I am foo. bar is ${exports.bar()}';
}

module.test.js:

describe('MyModule', () => {

  it('foo', () => {
    let module = require('./module')
    module.bar = jest.fn(()=>{return 'fake bar'})

    expect(module.foo()).toEqual('I am foo. bar is fake bar');
  });

})

Ответ 5

У меня была та же проблема, и из-за стандартов линтинга проекта определение класса или переписывание ссылок в exports не были приемлемыми вариантами проверки кода, даже если определения лининга не препятствовали этому. То, на что я наткнулся в качестве жизнеспособного варианта, это использовать плагин babel-rewire-plugin, который намного чище, по крайней мере, по внешнему виду. Хотя я обнаружил, что это используется в другом проекте, к которому у меня был доступ, я заметил, что он уже был в ответе на аналогичный вопрос, который я связал здесь. Это фрагмент, скорректированный для этого вопроса (и без использования шпионов), предоставленный из связанного ответа для справки (я также добавил точки с запятой в дополнение к удалению шпионов, потому что я не язычник):

import __RewireAPI__, * as module from '../module';

describe('foo', () => {
  it('calls bar', () => {
    const barMock = jest.fn();
    __RewireAPI__.__Rewire__('bar', barMock);
    
    module.foo();

    expect(bar).toHaveBeenCalledTimes(1);
  });
});

Ответ 6

Работает для меня:

cat moduleWithFunc.ts

export function funcA() {
 return export.funcB();
}
export function funcB() {
 return false;
}

cat moduleWithFunc.test.ts

import * as module from './moduleWithFunc';

describe('testFunc', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  afterEach(() => {
    module.funcB.mockRestore();
  });

  it.only('testCase', () => {
    // arrange
    jest.spyOn(module, 'funcB').mockImplementationOnce(jest.fn().mockReturnValue(true));

    // act
    const result = module.funcA();

    // assert
    expect(result).toEqual(true);
    expect(module.funcB).toHaveBeenCalledTimes(1);
  });
});