8

I have a class Qux that inherits from class Baa and I would like to mock Baa while testing Qux. That works in principle if I don't try to spy on the mock BaaMock.

If I would like to spy on the mocked class, the doc says that I should use a jest.fn() instead of the class. However, that does not seem to work correctly: some of the methods of the inheriting class Qux are lost.

Some example code (also available at https://github.com/stefaneidelloth/testDemoES6Jest) :

Super class 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';
    }
}

Inheriting class 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. Test for inheriting class Qux without a possibility to spy (/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. In order to be able to spy on the mocked class, I tried to replace the class with a mock function (also see 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');
    });         

}); 

As a result, the test fails with following errors:

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)

I would expect my instance sut of Qux to still contain the methods quxMethod and overridableMethod that are defined by the class Qux.

=> Is this a bug of jest?

=> If not, why should do I need to implement all methods of Qux in the mock for Baa !!???

=> How do I need to adapt my example code B do successfully mock the class Baa, so that Qux is still able to inherit from it?

Stefan
  • 10,010
  • 7
  • 61
  • 117
  • Does this answer your question? [Jest mock method of base ES6 class (super method) when testing extended class](https://stackoverflow.com/questions/65391369/jest-mock-method-of-base-es6-class-super-method-when-testing-extended-class) – Liam Nov 24 '22 at 19:30

3 Answers3

7

Inside the function of mockImplementation the context this can be used. Following code works:

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

}); 
Stefan
  • 10,010
  • 7
  • 61
  • 117
4

I believe you should not do that way. I see few strong reasons avoiding such an approach:

  1. Yes, you will have to list all the methods in your mock. Even more: for some methods you will have to provide actual implementation(what if some method should return boolean and other method rely on result? what if some method returns number that is used for some calculation in other method?). It's hard to achieve, it's hard to maintain, it breaks really easily on refactoring.
  2. You actually test implementation details. And this never ends well. What if some day you'd like to switch to different parent class or just merge 2 classes in one - you'd definitely need to update test. Even when system still works fine. So you need to do additional work without any value - just to keep tests passing. Is it worth that?
  3. Mocking super class actually means you would be less confident if system works well. Say test for child class mocks some super method and mock differs from actual implementation. Your tests are passing but real system fails.

Summarizing all above I propose you avoid mocking super class at any cost.

skyboyer
  • 22,209
  • 7
  • 57
  • 64
  • Well, my question was not "Should I mock some ES6 super class?". Nevertheless thank you for your comment. Related question to your "answer": https://stackoverflow.com/questions/1595166/why-is-it-so-bad-to-mock-classes – Stefan Aug 12 '19 at 10:18
  • @Stefan sure, I understand this is not actual answer to your question. I even expect some downvoting. But anyway I want to warn: that path is painful, I've been there. – skyboyer Aug 12 '19 at 10:56
2

Modify this will override the original method. for your example, if I want to mock overridableMethod, I would modify overridableMethod of this:

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

This makes the test error:

● 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)

You can use class mock to fix this problem:

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

Use jest.fn() for every function including constructor let us can spy these function.

Peter Chen
  • 85
  • 6