笑话:如何模拟一个类的一个特定方法

Posted

技术标签:

【中文标题】笑话:如何模拟一个类的一个特定方法【英文标题】:Jest: How to mock one specific method of a class 【发布时间】:2018-10-10 00:19:02 【问题描述】:

假设我有以下课程:

export default class Person 
    constructor(first, last) 
        this.first = first;
        this.last = last;
    
    sayMyName() 
        console.log(this.first + " " + this.last);
    
    bla() 
        return "bla";
    

假设我想创建一个模拟类,其中方法“sayMyName”将被模拟,而方法“bla”将保持原样。

我写的测试是:

const Person = require("../Person");

jest.mock('../Person', () => 
    return jest.fn().mockImplementation(() => 
        return sayMyName: () => 
            return 'Hello'
        ;
    );
);


let person = new Person();
test('MyTest', () => 
    expect(person.sayMyName()).toBe("Hello");
    expect(person.bla()).toBe("bla");
)

第一个 'expect' 语句通过,这意味着 'sayMyName' 被成功模拟。但是,第二个“期望”失败并出现错误:

TypeError: person.bla 不是函数

我知道模拟类删除了所有方法。 我想知道如何模拟一个类,以便只模拟特定的方法。

【问题讨论】:

【参考方案1】:

使用jest.spyOn() 是正确的Jest 方式来模拟单个方法,而让其余方法保持不变。实际上有两种略有不同的方法。

1。仅在单个对象中修改方法

import Person from "./Person";

test('Modify only instance', () => 
    let person = new Person('Lorem', 'Ipsum');
    let spy = jest.spyOn(person, 'sayMyName').mockImplementation(() => 'Hello');

    expect(person.sayMyName()).toBe("Hello");
    expect(person.bla()).toBe("bla");

    // unnecessary in this case, putting it here just to illustrate how to "unmock" a method
    spy.mockRestore();
);

2。修改类本身,让所有的实例都受影响

import Person from "./Person";

beforeAll(() => 
    jest.spyOn(Person.prototype, 'sayMyName').mockImplementation(() => 'Hello');
);

afterAll(() => 
    jest.restoreAllMocks();
);

test('Modify class', () => 
    let person = new Person('Lorem', 'Ipsum');
    expect(person.sayMyName()).toBe("Hello");
    expect(person.bla()).toBe("bla");
);

为了完整起见,这就是模拟静态方法的方式:

jest.spyOn(Person, 'myStaticMethod').mockImplementation(() => 'blah');

【讨论】:

很遗憾官方文档没有这样说。官方文档是一堆令人费解的模块工厂,使用对象文字手工滚动的类模拟,存储在特殊目录中的东西,以及基于属性名称的特殊情况处理。 jestjs.io/docs/en/… 我在上面编辑了我的答案。 @blade 使用mockImplementation 将始终产生通过测试,如果类发生变化,则为误报。 @sesamechicken 不确定我是否遵循始终通过的测试。您永远不应该直接测试模拟方法,这将是零意义的。一个常见的用例是模拟一个为您实际测试的不同方法提供数据的方法。 >你不应该直接测试一个模拟方法这正是上面的例子使用mockImplementation演示的。 确实如此,但它回答了 OP 的问题。我认为在这个答案中详细介绍正确测试的细节是题外话,并且会偏离重点。【参考方案2】:

编辑 05/03/2021

我看到很多人不同意以下方法,这很酷。不过,我确实对@blade 的方法略有不同,因为它实际上并没有测试该类,因为它使用的是mockImplementation。如果班级发生变化,测试仍将始终通过并给出误报。所以这里有一个spyOn 的例子。

// person.js
export default class Person 
  constructor(first, last) 
      this.first = first;
      this.last = last;
  
  sayMyName() 
      return this.first + " " + this.last; // Adjusted to return a value
  
  bla() 
      return "bla";
  


和测试:

import Person from './'

describe('Person class', () => 
  const person = new Person('Guy', 'Smiley')

  // Spying on the actual methods of the Person class
  jest.spyOn(person, 'sayMyName')
  jest.spyOn(person, 'bla')
  
  it('should return out the first and last name', () =>   
    expect(person.sayMyName()).toEqual('Guy Smiley') // deterministic 
    expect(person.sayMyName).toHaveBeenCalledTimes(1)
  );
  it('should return bla when blah is called', () => 
    expect(person.bla()).toEqual('bla')
    expect(person.bla).toHaveBeenCalledTimes(1)
  )
);

干杯! ?


我看不出模拟的实现实际上如何为您解决任何问题。我觉得这更有意义

import Person from "./Person";

describe("Person", () => 
  it("should...", () => 
    const sayMyName = Person.prototype.sayMyName = jest.fn();
    const person = new Person('guy', 'smiley');
    const expected = 
      first: 'guy',
      last: 'smiley'
    

    person.sayMyName();

    expect(sayMyName).toHaveBeenCalledTimes(1);
    expect(person).toEqual(expected);
  );
);

【讨论】:

我不知道这个问题的答案,所以真的很好奇:这是否会改变 Person.prototype.sayMyName 以用于在此之后运行的任何其他测试? @Martin 是的,确实如此。 我认为这不是一个好习惯。它没有使用 Jest 或任何其他框架来模拟该方法,您需要额外的努力来恢复该方法。 请参阅***.com/a/56565849/1248209,了解如何在 Jest 中使用 spyOn 正确执行此操作。【参考方案3】:

一直在问类似的问题,我认为找到了解决方案。无论在哪里实际使用 Person 类实例,这都应该有效。

const Person = require("../Person");

jest.mock("../Person", function () 
    const  default: mockRealPerson  = jest.requireActual('../Person');

    mockRealPerson.prototype.sayMyName = function () 
        return "Hello";
        

    return mockRealPerson
);

test('MyTest', () => 
    const person = new Person();
    expect(person.sayMyName()).toBe("Hello");
    expect(person.bla()).toBe("bla");
);

【讨论】:

【参考方案4】:

没有真正回答这个问题,但我想展示一个用例,您想模拟一个依赖类来验证另一个类。

例如:Foo 依赖于 Bar。在内部 Foo 创建了一个 Bar 的实例。你想模拟Bar 来测试Foo

酒吧类

class Bar 
  public runBar(): string 
    return 'Real bar';
  


export default Bar;

Foo 类

import Bar from './Bar';

class Foo 
  private bar: Bar;

  constructor() 
    this.bar = new Bar();
  

  public runFoo(): string 
    return 'real foo : ' + this.bar.runBar();
  


export default Foo;


测试:

import Foo from './Foo';
import Bar from './Bar';

jest.mock('./Bar');

describe('Foo', () => 
  it('should return correct foo', () => 
    // As Bar is already mocked,
    // we just need to cast it to jest.Mock (for TypeScript) and mock whatever you want
    (Bar.prototype.runBar as jest.Mock).mockReturnValue('Mocked bar');
    const foo = new Foo();
    expect(foo.runFoo()).toBe('real foo : Mocked bar');
  );
);


注意:如果您使用箭头函数在类中定义方法(因为它们是实例之间的差异),这将不起作用。将其转换为常规实例方法将使其工作。

另见jest.requireActual(moduleName)

【讨论】:

【参考方案5】:

您可以像这样扩展它,而不是模拟类:

class MockedPerson extends Person 
  sayMyName () 
    return 'Hello'
  

// and then
let person = new MockedPerson();

【讨论】:

好主意!我发现它对模拟类非常有用! ❤️【参考方案6】:

如果您使用的是 Typescript,您可以执行以下操作:

Person.prototype.sayMyName = jest.fn().mockImplementationOnce(async () => 
        await 'my name is dev'
);

在你的测试中,你可以这样做:

const person = new Person();
const res = await person.sayMyName();
expect(res).toEqual('my name is dev');

希望这对某人有所帮助!

【讨论】:

我们如何断言 mock 被调用了?【参考方案7】:

我结合了@sesamechicken 和@Billy Reilly 的答案,创建了一个实用函数来模拟类的(一个或多个)特定方法,而不会影响类本身。

/**
* @CrazySynthax class, a tiny bit updated to be able to easily test the mock.
*/
class Person 
    constructor(first, last) 
        this.first = first;
        this.last = last;
    

    sayMyName() 
        return this.first + " " + this.last + this.yourGodDamnRight();
    

    yourGodDamnRight() 
        return ", you're god damn right";
    


/**
 * Return a new class, with some specific methods mocked.
 *
 * We have to create a new class in order to avoid altering the prototype of the class itself, which would
 * most likely impact other tests.
 *
 * @param Klass: The class to mock
 * @param functionNames: A string or a list of functions names to mock.
 * @returns Class a new class.
 */
export function mockSpecificMethods(Klass, functionNames) 
    if (!Array.isArray(functionNames))
        functionNames = [functionNames];

    class MockedKlass extends Klass 
    

    const functionNamesLenght = functionNames.length;
    for (let index = 0; index < functionNamesLenght; ++index) 
        let name = functionNames[index];
        MockedKlass.prototype[name] = jest.fn();
    ;

    return MockedKlass;


/**
* Making sure it works
*/
describe('Specific Mocked function', () => 
    it('mocking sayMyName', () => 
        const walter = new (mockSpecificMethods(Person, 'yourGodDamnRight'))('walter', 'white');

        walter.yourGodDamnRight.mockReturnValue(", that's correct"); // yourGodDamnRight is now a classic jest mock;

        expect(walter.sayMyName()).toBe("walter white, that's correct");
        expect(walter.yourGodDamnRight.mock.calls.length).toBe(1);

        // assert that Person is not impacted.
        const saul = new Person('saul', 'goodman');
        expect(saul.sayMyName()).toBe("saul goodman, you're god damn right");
    );
);

【讨论】:

【参考方案8】:

我试图让它在一个已经被模拟的类上工作。因为它已经被模拟了,所以我没有可以修改的原型,所以我找到了这个解决方法。

我不喜欢这个解决方案,所以如果有人知道将方法更新到已经被模拟的类的更好方法,我会全力以赴。

为了澄清,这个问题的主要答案是使用未模拟的类。在我的情况下,该类已被模拟出来,我正在尝试将其中一种方法更新为已模拟的类。

我的解决方案:


const previousClassInstance = new PreviouslyMockedClass();
PreviouslyMockedClass.mockImplementation(() => 
    return 
        // "Import" the previous class methods at the top
        ...previousClassInstance,

        // Then overwrite the ones you wanna update
        myUpdatedMethod: jest.fn(() => 
            console.log(
                "This method is updated, the others are present and unaltered"
            );
        ),
    ;
);

【讨论】:

以上是关于笑话:如何模拟一个类的一个特定方法的主要内容,如果未能解决你的问题,请参考以下文章

笑话:如何正确模拟节点模块?

笑话:当同一个模块也有命名导出时,如何模拟默认导出组件?

笑话:如何模拟监听事件的依赖项?

笑话:模拟 RxJs 管道

运行特定的笑话项目

笑话模拟工厂不适用于模拟类