it-roy-ru.com

Как макетировать функции в том же модуле, используя шутку

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

Проблема заключается в том, что после времени импорта foo сохраняет ссылку на исходный неизмененный 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"
         */
    });
});

Спасибо!

Правка: я мог бы изменить:

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

в 

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

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

33
Mark

между прочим, я решил использовать внедрение зависимости , установив аргумент по умолчанию.

Так что я бы изменил

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 моего компонента, и я могу легко переопределить панель в моем тесте, выполнив следующие действия.

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

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

4
Mark

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

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

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

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

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

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

  1. В файле 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');
      });
    });
    
  2. Как вы сказали, перепишите глобальную ссылку в глобальный контейнер exports. Это не рекомендуемый путь, так как вы, возможно, введете странные поведения в других тестах, если не вернете экспорт должным образом к его начальному состоянию.

12
John-Philip

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

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 циклического импорта он работает очень гладко.

9
MostafaR

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

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

})
1
Sean

У меня была та же проблема, и из-за стандартов linting проекта определение класса или переписывание ссылок в exports не были допустимыми вариантами проверки кода, даже если определения linting не препятствовали этому. В качестве жизнеспособного варианта я наткнулся на использование 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);
  });
});

https://stackoverflow.com/a/45645229/6867420

0
Brandon Hunter

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

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);
  });
});
0
Сергей Галкин