如何模拟 ES6 超类并用 jest.js 监视它?

Posted

技术标签:

【中文标题】如何模拟 ES6 超类并用 jest.js 监视它?【英文标题】:How to mock ES6 super class and spy on it with jest.js? 【发布时间】:2019-12-18 20:26:29 【问题描述】:

我有一个类Qux 继承自类Baa,我想在测试Qux 时模拟Baa。如果我不尝试监视模拟 BaaMock,这原则上是有效的。

如果我想监视模拟类,doc 说我应该使用 jest.fn() 而不是类。但是,这似乎无法正常工作:继承类Qux 的一些方法丢失了。

一些示例代码(也可在https://github.com/stefaneidelloth/testDemoES6Jest 获得):

超级类 Baa (/src/baa.js):

import Foo from './foo.js';

export default class Baa extends Foo 
    
    constructor(name)
        super(name);    
    
    
    baaMethod()
        return 'baaMethod';
    
    
    overridableMethod() 
        return 'baa';
    


继承类Qux(/src/qux.js):

import Baa from './baa.js';

export default class Qux extends Baa 
    
    constructor(name)
        super(name);        
    
    
    quxMethod()
        return 'quxMethod';
    
    
    overridableMethod() 
        return 'qux';
    


A. 测试继承类 Qux 而不能被窥探(/test/qux.test.js):

jest.mock('./../src/baa.js', () => 
    return class BaaMock 
        constructor(name)
            this.name = name;
        

        baaMethod()
            return 'baaMockedMethod';
        
       
);

import Qux from './../src/qux.js';

describe('Qux', function()

    var sut;        

    beforeEach(function()          
        sut = new Qux('qux');       
    );

    it('quxMethod', function()         
        expect(sut.quxMethod()).toEqual('quxMethod');
    ); 

    it('baaMethod', function()         
        expect(sut.baaMethod()).toEqual('baaMockedMethod');
    ); 

    it('overridableMethod', function()         
        expect(sut.overridableMethod()).toEqual('qux');
    );         

); 

B.为了能够窥探模拟类,我尝试用模拟函数替换该类(另见https://jestjs.io/docs/en/es6-class-mocks):

import Baa from './../src/baa.js';
jest.mock('./../src/baa.js', 
    function()
        return jest.fn().mockImplementation(
            function(name)
                return 
                    name:name,
                    baaMethod: () => return 'baaMockedMethod';
                ;
            
        );
    
);

import Qux from './../src/qux.js';

describe('Qux', function()

    var sut;        

    beforeEach(function()
        
        //Baa.mockClear();
        sut = new Qux('qux');           
        //expect(Baa).toHaveBeenCalledTimes(1);
    );

    it('quxMethod', function()         
        expect(sut.quxMethod()).toEqual('quxMethod');
    ); 

    it('baaMethod', function()         
        expect(sut.baaMethod()).toEqual('baaMockedMethod');
    ); 

    it('overridableMethod', function()         
        expect(sut.overridableMethod()).toEqual('qux');
    );         

); 

因此,测试失败并出现以下错误

FAIL test/qux.test.js

  Qux
    × quxMethod (7ms)
    √ baaMethod (4ms)
    × overridableMethod (2ms)

  ● Qux › quxMethod

    TypeError: sut.quxMethod is not a function

      28 | 
      29 |  it('quxMethod', function()         
    > 30 |      expect(sut.quxMethod()).toEqual('quxMethod');
         |                 ^
      31 |  ); 
      32 | 
      33 |  it('baaMethod', function()         

      at Object.quxMethod (test/qux.test.js:30:14)

  ● Qux › overridableMethod

    TypeError: sut.overridableMethod is not a function

      36 | 
      37 |  it('overridableMethod', function()         
    > 38 |      expect(sut.overridableMethod()).toEqual('qux');
         |                 ^
      39 |  );         
      40 | 
      41 | );  

      at Object.overridableMethod (test/qux.test.js:38:14)

我希望我的实例 sutQux 仍包含由类 Qux 定义的方法 quxMethodoverridableMethod

=>这是开玩笑的错误吗?

=>如果不是,为什么我需要在模拟中为 Baa 实现 Qux 的所有方法!!???

=>我需要如何调整我的示例代码 B 成功地模拟了 Baa 类,以便 Qux 仍然能够从它继承?

【问题讨论】:

【参考方案1】:

我相信你不应该那样做。我认为避免这种方法的理由很少:

    是的,您必须列出模拟中的所有方法。甚至更多:对于某些方法,您将必须提供实际实现(如果某些方法应该返回布尔值而其他方法依赖于结果怎么办?如果某些方法返回用于其他方法中的某些计算的数字怎么办?)。很难实现,也很难维护,重构时很容易崩溃。 您实际上是在测试实现细节。这永远不会结束。如果有一天你想切换到不同的父类或只是将 2 个类合并为一个 - 你肯定需要更新 test.即使系统仍然可以正常工作。所以你需要做一些没有任何价值的额外工作——只是为了保持测试通过。值得吗? 模拟超类实际上意味着如果系统运行良好,您将不太自信。说对子类的测试模拟了一些超级方法,并且模拟与实际实现不同。你的测试通过了,但真正的系统失败了。

总结以上所有内容,我建议您不惜一切代价避免嘲笑超类。

【讨论】:

好吧,我的问题不是“我应该模拟一些 ES6 超类吗?”。尽管如此,还是感谢您的评论。与您的“答案”相关的问题:***.com/questions/1595166/… @Stefan 当然,我知道这不是您问题的实际答案。我什至期待一些downvoting。但无论如何我想警告:那条路很痛苦,我去过那里。【参考方案2】:

mockImplementation的函数内部可以使用上下文this。以下代码有效:

import Baa from './../src/baa.js';
jest.mock('./../src/baa.js', 
    function()
        return jest.fn().mockImplementation(
            function(name)
                this.name=name;
                this.baaMethod = ()=>
                    return 'baaMockedMethod';
                ;
                return this;                
            
        );
    
);

import Qux from './../src/qux.js';

describe('Qux', function()

    var sut;        

    beforeEach(function()

        Baa.mockClear();
        sut = new Qux('qux');           
        expect(Baa).toHaveBeenCalledTimes(1);
    );

    it('quxMethod', function()         
        expect(sut.quxMethod()).toEqual('quxMethod');
    ); 

    it('baaMethod', function()         
        expect(sut.baaMethod()).toEqual('baaMockedMethod');
    ); 

    it('overridableMethod', function()         
        expect(sut.overridableMethod()).toEqual('qux');
    );         

); 

【讨论】:

【参考方案3】:

修改this 将覆盖原来的方法。对于你的例子,如果我想模拟overridableMethod,我会修改overridableMethodthis

jest.mock('../../src/baa.js',
    function()
        return jest.fn().mockImplementation(
            function(name)
                this.name=name;
                this.baaMethod = ()=>
                    return 'baaMockedMethod';
                ;

                // mock overridable method
                this.overridableMethod = ()=>
                    return 'mock'
                

                return this;
            
        );
    
);

这会导致测试错误:

● Qux › overridableMethod

    expect(received).toEqual(exptected) // deep equality

    Expected: "qux"
    Received: "mock"

      51 |
      52 |   it("overridableMethod", function () 
    >          expect(sut.overridableMethod()).toEqual("qux");
         |                                     ^
      54 |   );
      55 | );
      56 |

      at Object.<anonymous> (tests/unit/qux.spec.js:53:37)

你可以使用类模拟来解决这个问题:

const mockConstructor = jest.fn().mockImplementation(function(name)this.name = name)
const mockOverridableMethod = jest.fn(() => 'mock')
const mockBaaMethod = jest.fn(() => 'baaMockedMethod')

jest.mock("../../src/baa.js", () => 
  return class BaaMock 
    constructor(name) 
      return (mockConstructor.bind(this))(name)
    

    overridableMethod() 
      return mockOverridableMethod()
    

    baaMethod() 
      return mockBaaMethod()
    
  ;
);

import Qux from "../../src/qux.js";

describe("Qux", function () 
  var sut;

  beforeEach(function () 
    mockConstructor.mockClear()
    sut = new Qux("qux");
    expect(mockConstructor).toHaveBeenCalledTimes(1); // use mockConstructor check if have been called
  );

  it("quxMethod", function () 
    expect(sut.quxMethod()).toEqual("quxMethod");
  );

  it("baaMethod", function () 
    expect(sut.baaMethod()).toEqual("baaMockedMethod");
  );

  it("overridableMethod", function () 
    expect(sut.overridableMethod()).toEqual("qux");
  );
);

对每个函数(包括构造函数)使用jest.fn(),让我们可以窥探这些函数。

【讨论】:

以上是关于如何模拟 ES6 超类并用 jest.js 监视它?的主要内容,如果未能解决你的问题,请参考以下文章

使用 Typescript 从 Jest 手动模拟中导入函数

如何在玩笑中模拟/监视 useState 钩子?

使用 Jest 模拟 Es6 类

如何在 Angular 单元测试中模拟/监视导入的函数

Android 设备监视器使模拟器脱机

如何模拟 ES6 模块的导入?