ES2016 Class, Sinon Stub Constructor - javascript

I'm trying to stub out a super call with sinon, and es2016 but I'm not having much luck. Any ideas why this isn't working?
Running Node 6.2.2, this might be an issue with its implementation of classes/constructors.
.babelrc file:
{
"presets": [
"es2016"
],
"plugins": [
"transform-es2015-modules-commonjs",
"transform-async-to-generator"
]
}
Test:
import sinon from 'sinon';
class Foo {
constructor(message) {
console.log(message)
}
}
class Bar extends Foo {
constructor() {
super('test');
}
}
describe('Example', () => {
it('should stub super.constructor call', () => {
sinon.stub(Foo.prototype, 'constructor');
new Bar();
sinon.assert.calledOnce(Foo.prototype.constructor);
});
});
Result:
test
AssertError: expected constructor to be called once but was called 0 times
at Object.fail (node_modules\sinon\lib\sinon\assert.js:110:29)
at failAssertion (node_modules\sinon\lib\sinon\assert.js:69:24)
at Object.assert.(anonymous function) [as calledOnce] (node_modules\sinon\lib\sinon\assert.js:94:21)
at Context.it (/test/classtest.spec.js:21:18)
Note: this issue seems to only happen for constructors. I can spy on methods inherited from the parent class without any issues.

You'll need to setPrototypeOf the subClass due to the way JavaScript implements inheritance.
const sinon = require("sinon");
class Foo {
constructor(message) {
console.log(message);
}
}
class Bar extends Foo {
constructor() {
super('test');
}
}
describe('Example', () => {
it('should stub super.constructor call', () => {
const stub = sinon.stub().callsFake();
Object.setPrototypeOf(Bar, stub);
new Bar();
sinon.assert.calledOnce(stub);
});
});

You need to spy instead of stub,
sinon.spy(Foo.prototype, 'constructor');
describe('Example', () => {
it('should stub super.constructor call', () => {
const costructorSpy = sinon.spy(Foo.prototype, 'constructor');
new Bar();
expect(costructorSpy.callCount).to.equal(1);
});
});
*****Update******
Above was not working as expected, I added this way and is working now.
describe('Example', () => {
it('should stub super.constructor call', () => {
const FooStub = spy(() => sinon.createStubInstance(Foo));
expect(FooStub).to.have.been.calledWithNew;
});
});

Adding to the accepted answer of Wenshan, there is one step that may be overlooked when stubbing the parent class and replacing the original parent class with setPrototypeOf.
💡 Additionally, to avoid it breaking the succeeding tests it is a good idea to set back the original parent class at the end, like:
const sinon = require("sinon");
class Foo {
constructor(message) {
console.log(message);
}
}
class Bar extends Foo {
constructor() {
super('test');
}
}
describe('Bar constructor', () => {
it('should call super', () => {
const stub = sinon.stub().callsFake();
const original = Object.getPrototypeOf(Bar); // Bar.__proto__ === Foo
Object.setPrototypeOf(Bar, stub); // Bar.__proto__ === stub
new Bar();
sinon.assert.calledOnce(stub);
Object.setPrototypeOf(Bar, original); // Bar.__proto__ === Foo
});
});
The addition is
// saving the reference to the original parent class:
const original = Object.getPrototypeOf(Bar);
// setting back the original parent class after stubbing and the assertion:
Object.setPrototypeOf(Bar, original);

It doesn’t work for me either. I managed a workaround that works for me, i use spy as well:
class FakeSchema {
constructor(newCar) {
this.constructorCallTest();
this.name = newCar.name;
}
constructorCallTest() {
mochaloggger.log('constructor was called');
}
}
// spy that tracks the contsructor call
var fakeSchemaConstrSpy = sinon.spy(FakeCarSchema.prototype,'constructorCallTest');
Hope that was helpful

If you are in a browser environment, the following works too:
let constructorSpy = sinon.spy(window, 'ClassName');
For example this would work with Jasmine.
Mocha instead runs in Node environment, there is no window. The variable you'd be looking for is global

Related

How to mock a specific method of a class whilst keeping the implementation of all other methods with jest when the class instance isn't accessible?

Based on this question (How to mock instance methods of a class mocked with jest.mock?), how can a specific method be mocked whilst keeping the implementation of all other methods?
There's a similar question (Jest: How to mock one specific method of a class) but this only applies if the class instance is available outside it's calling class so this wouldn't work if the class instance was inside a constructor like in this question (How to mock a constructor instantiated class instance using jest?).
For example, the Logger class is mocked to have only method1 mocked but then method2 is missing, resulting in an error:
// Logger.ts
export default Logger() {
constructor() {}
method1() {
return 'method1';
}
method2() {
return 'method2';
}
}
// Logger.test.ts
import Logger from './Logger';
jest.mock("./Logger", () => {
return {
default: class mockLogger {
method1() {
return 'mocked';
}
},
__esModule: true,
};
});
describe("Logger", () => {
it("calls logger.method1() & logger.method2 on instantiation where only method1 is mocked", () => {
const logger = new Logger(); // Assume this is called in the constructor of another object.
expect(logger.method1()).toBe('mocked');
expect(logger.method2()).toBe('method2'); // TypeError: logger.method2 is not a function.
});
});
One solution is to extend the Logger class but this results in an undefined error as the Logger is already mocked:
// ...
jest.mock("./Logger", () => {
return {
default: class mockLogger extends Logger {
override method1() {
return 'mocked';
}
},
__esModule: true,
};
});
// ...
expect(logger.method2()).toBe('method2'); // TypeError: Cannot read property 'default' of undefined
Therefore, what could be the correct way to mock only method1 but keep method2's original implementation?
You can use jest.spyOn and provide a mock implementation for method1.
// Logger.test.ts
import Logger from './Logger';
jest.spyOn(Logger.prototype, "method1").mockImplementation(() => "mocked")
describe("Logger", () => {
it("calls method1 & method2 but only method1 is mocked", () => {
const l = new Logger();
expect(l.method1()).toBe("mocked");
expect(l.method2()).toBe("method2");
})
})
But in case you have many methods and you want to mock each one of them except one single method, then you can get the original implementation of this one single method using jest.requireActual.
// Logger.test.ts
import Logger from "./Logger";
const mockMethod1 = jest.fn().mockReturnValue("mocked");
const mockMethod3 = jest.fn().mockReturnValue("mocked");
const mockMethod4 = jest.fn().mockReturnValue("mocked");
const mockMethod5 = jest.fn().mockReturnValue("mocked");
jest.mock("./Logger", () =>
jest.fn().mockImplementation(() => ({
method1: mockMethod1,
method2: jest.requireActual("./Logger").default.prototype.method2,
method3: mockMethod3,
method4: mockMethod4,
method5: mockMethod5,
}))
);
describe("Logger", () => {
it("calls all methods but only method1 is mocked", () => {
const l = new Logger();
expect(l.method1()).toBe("mocked");
expect(l.method2()).toBe("method2");
expect(l.method3()).toBe("mocked");
expect(l.method4()).toBe("mocked");
expect(l.method5()).toBe("mocked");
});
});
Note: You don't need to define an ES6 class for mocking, a constructor function also just works fine because ES6 classes are actually just syntactic sugar for constructor functions.
Mocking the prototype works:
describe("Logger", () => {
it("calls logger.method1() & logger.method2 on instantiation where only method1 is mocked", () => {
Logger.prototype.method1 = jest.fn(() => 'mocked');
const logger = new Logger();
expect(logger.method1()).toBe('mocked');
expect(logger.method2()).toBe('method2');
});
});
However, I'm not sure if this is the correct way to mock a specific method when the class instance isn't accessible so I'll leave the question open for while in case there are better solutions.

How to properly call an init function in a ts class

I'm writing some tests for a class. In my tests if I don't call browserViewPreload.init() the test fails, but when I do call browserViewPreload.init() it passes.
Why should I need to explicitly call browserViewPreload.init() in the test when I've already done it in my beforeEach block?
//myFile.ts
export default class BrowserViewPreload {
constructor(){
this.init();
}
attachMouse(){
console.log('attaching mouse')
}
init(){
return document.addEventListener('DOMContentLoaded', this.attachMouse)
}
}
//myFile.spec.ts
import BrowserViewPreload from './browserViewPreload'
function bootStrapComponent() {
return new BrowserViewPreload();
};
describe('BrowserViewPreload Class', () => {
var browserViewPreload;
let initSpy
let docspy
let mouseSpy
beforeEach(()=>{
browserViewPreload = bootStrapComponent();
initSpy = jest.spyOn(browserViewPreload, 'init')
docspy = jest.spyOn(document, 'addEventListener')
})
it('should report name', () => {
//browserViewPreload.init(); not including this makes the tests fail. Why do I need to make the call here when I've already done so in the beforeEach
expect(initSpy).toHaveBeenCalled();
expect(docspy).toHaveBeenCalled();
document.dispatchEvent(new Event('DOMContentLoaded'));
expect(mouseSpy).toHaveBeenCalled();
});
});
I guess it's because you are creating BrowserViewPreload object before attaching the initSpy to it.

Run chained method before anything in constructor?

I have this simple class:
class Foo {
constructor() {
this.init();
return this;
}
init() {
this.onInit();
}
onInit(callback) {
this.onInit = () => callback();
return this;
}
}
new Foo().onInit(() => console.log('baz'));
It's obviously flawed, because it will call init before the onInit method is able to define the onInit property/callback.
How can I make this work without change the interface?
How can I make this work without change the interface?
You can't, the interface is inherently flawed. That's really the answer to your question.
Continuing, though, with "what can I do instead":
If you need to have a callback called during initialization, you need to pass it to the constructor, not separately to the onInit method.
class Foo {
constructor(callback) {
this.onInit = () => {
callback(); // Call the callback
return this; // Chaining seemed important in your code, so...
};
// Note: Constructors don't return anything
}
}
new Foo(() => console.log('baz'));
In a comment you've said:
I see your point, the fact is that my library is new Something().onCreate().onUpdate()
It sounds like you might want to adopt the builder pattern instead:
class Foo {
constructor(callbacks) {
// ...use the callbacks here...
}
// ...
}
Foo.Builder = class {
constructor() {
this.callbacks = {};
}
onCreate(callback) {
this.callbacks.onCreate = callback;
}
onUpdate(callback) {
this.callbacks.onUpdate = callback;
}
// ...
build() {
// Validity checks here, do we have all necessary callbacks?
// Then:
return new Foo(this.callbacks);
}
};
let f = new Foo.Builder().onCreate(() => { /*...*/}).onUpdate(() => { /*... */}).build();
...although to be fair, a lot of the advantages (though not all) of the builder pattern can be realized in JavaScript by just passing an object into constructor directly and doing your validation there, e.g.:
let f = new Foo({
onCreate: () => { /*...*/},
onUpdate: () => { /*...*/}
});
Assuming that onInit is supposed to be some sort of hook to be called synchronously whenever an object is instantiated, you can't solve this on the instance level.
You can make onInit a static function, like so:
class Foo {
constructor() {
// whatever
Foo.onInit();
}
static onInit() {} // empty default
}
Foo.onInit = () => console.log('baz'); // Override default with your own function
const f = new Foo();
const f2 = new Foo();

flowtype - How to handle mixins

I have an interface/type defined and I don't want to redefine it everywhere I want to use the mixin. Think facebook's mixins: [] property
Example usecase:
const mixin = root => Object.assign(root, {bar: () => 'baz'));
class Foo {
constructor () { mixin(this); }
test () { console.log(this.bar()) } // <-- this gives an error
}
From what I could find online, this is what i've tried:
interface MyMyxin {
bar () : string;
}
function mixin <T:Object> (root: T) : T & MyMyxin {
return Object.assign(root, {bar: () => 'baz'});
}
class Foo {
constructor () {
mixin(this);
}
test () {
console.log(this.bar()); // <-- no luck
}
}
mixin(Foo.prototype) // <--- this also does not work
But I can't get flow to understand that the mixin method adds an extra property to the object

Mocking/Stubbing `super` calls

I would like to mock out super calls, especially constructors in some ES6 classes. For example
import Bar from 'bar';
class Foo extends Bar {
constructor(opts) {
...
super(opts);
}
someFunc() {
super.someFunc('asdf');
}
}
And then in my test, I would like to do something like
import Foo from '../lib/foo';
import Bar from 'bar';
describe('constructor', function() {
it('should call super', function() {
let opts = Symbol('opts');
let constructorStub = sinon.stub(Bar, 'constructor');
new Foo(opts);
sinon.assert.calledWith(constructorStub, opts);
});
})
describe('someFunc', function() {
it('should call super', function() {
let funcStub = sinon.stub(Bar, 'someFunc');
let foo = new Foo(opts);
foo.someFunc();
sinon.assert.calledWith(funcStub, 'asdf');
});
})
Figured it out, and #Bergi was on the right track.
In reponse to #naomik's question - My main purpose for wanting to stub this out was two fold. First, I didn't want to actually instantiate the super class, merely validate that I was calling the proper thing. The other reason (which didn't really come through since I was trying to simplify the example), was that what I really cared about was that I was doing certain things to opts that I wanted to make sure were carried through properly to the super constructor (for example, setting default values).
To make this work, I needed to dosinon.stub(Bar.prototype, 'constructor');
This is a better example and working test.
// bar.js
import Memcached from 'memcached'
export default class Bar extends Memcached {
constructor(opts) {
super(opts);
}
}
// foo.js
import Bar from './bar.js';
export default class Foo extends Bar {
constructor(opts) {
super(opts);
}
}
// test.js
import Foo from './foo.js';
import Bar from './bar.js';
import Memcached from 'memcached';
import sinon from 'sinon';
let sandbox;
let memcachedStub;
const opts = '127.0.0.1:11211';
describe('constructors', function() {
beforeEach(function() {
sandbox = sinon.sandbox.create();
memcachedStub = sandbox.stub(Memcached.prototype, 'constructor');
});
afterEach(function() {
sandbox.restore();
});
describe('#bar', function() {
it('should call super', function() {
new Bar(opts);
sinon.assert.calledOnce(memcachedStub);
sinon.assert.calledWithExactly(memcachedStub, opts);
});
});
describe('#foo', function() {
it('should call super', function() {
new Foo(opts);
sinon.assert.calledOnce(memcachedStub);
sinon.assert.calledWithExactly(memcachedStub, opts);
});
});
});

Categories

Resources