Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mocking a Class #157

Closed
LandonSchropp opened this issue Dec 9, 2016 · 13 comments · Fixed by #158
Closed

Mocking a Class #157

LandonSchropp opened this issue Dec 9, 2016 · 13 comments · Fixed by #158
Assignees
Labels

Comments

@LandonSchropp
Copy link

Thanks for creating such an awesome library! We've been enjoying using it so far.

Is it possible to mock a class with testdouble.js? For instance, let's say I wanted to mock the following module:

module.exports = class ES6Class {
  test() {
    return "hello";
  }
}

I'd like to mock this class like so:

let ES6ClassMock = testdouble.replace('./es6_class');
testdouble.when(ES6ClassMock.prototype.test()).thenReturn("This doesn't work :(");

When I run this, I got the following error:

TypeError: ES6ClassMock.prototype.test is not a function

When I try this with an ES5 constructor function, I receive a different error:

function ES5Class() {}

ES5Class.prototype.test = function() {
  return "hello";
};

module.exports = ES5Class;
let ES5ClassMock = testdouble.replace('./es5_class');
testdouble.when(ES5ClassMock.prototype.test()).thenReturn("This doesn't work :(");
TypeError: Cannot read property 'test' of undefined

Is it possible to accomplish what I'm trying to do? Thanks in advance!

@LandonSchropp
Copy link
Author

As a temporary workaround, we've gotten around this issue by doing the following:

function mockClass(clazz) {
  let mock = function() {};

  let prototype = _(Object.getOwnPropertyNames(clazz.prototype))
    .map((property) => [property, td.function(property)])
    .fromPairs()
    .value();

  mock.prototype = prototype;
  return mock;
}

function replaceClass(path) {
  let clazz = require(path);
  let mock = mockClass(clazz);
  td.replace(path, mock);
  return mock;
}

However, these solutions are quick hacks to enable mocking classes. They likely won't work in all circumstances. It would be great to have this behavior built into testdouble.js.

@searls
Copy link
Member

searls commented Dec 10, 2016

There has been various talk of this in other issues and I agree it's time we figured this out. I don't see (as yet) any reason it shouldn't be possible.

It's going to be a little bit difficult to test this except through our babel example project, so I'm starting there with some fresh examples of class replacement. See this as a starting point: https://github.com/testdouble/testdouble.js/pull/158/files#diff-6cfe233169444f306186c38b5c879c74R2

Next step I'm going to go peek at other discussion of this and then figure out an approach to implementing it.

@LandonSchropp
Copy link
Author

LandonSchropp commented Dec 11, 2016

Wow, that was really fast! I'll give the new changes a shot tomorrow. Thanks @searls!

@kealthou
Copy link

I don't quite understand this, so enlighten me.

This and #158 sound like testdouble supports ES6 native classes now, but it doesn't and you need babal-plugin-transform-es2015-classes, right? Am I supposed to use the transformer (via the es2015 preset or the env one or whatever) to use native classes as examples/babel suggests?

test.js

class A {}

var FakeA = require("testdouble").constructor(A);

new FakeA();
$ npm i testdouble
$ node test.js

/tmp/td/node_modules/testdouble/lib/constructor.js:41
      var _this = _possibleConstructorReturn(this, (TestDoubleConstructor.__proto__ || Object.getPrototypeOf(TestDoubleConstructor)).apply(this, arguments));
                                                                                                                                     ^

TypeError: Class constructor A cannot be invoked without 'new'
    at new TestDoubleConstructor (/tmp/td/node_modules/testdouble/lib/constructor.js:41:134)
    at Object.<anonymous> (/tmp/td/test.js:5:1)
    at Module._compile (module.js:571:32)
    at Object.Module._extensions..js (module.js:580:10)
    at Module.load (module.js:488:32)
    at tryModuleLoad (module.js:447:12)
    at Function.Module._load (module.js:439:3)
    at Module.runMain (module.js:605:10)
    at run (bootstrap_node.js:423:7)
    at startup (bootstrap_node.js:147:9)

With transform-es2015-classes, I got no error.

$ npm i babel-cli babel-plugin-transform-es2015-classes
$ $(npm bin)/babel-node --plugins transform-es2015-classes test.js
# no error

@searls
Copy link
Member

searls commented Apr 19, 2017

I'm only glancing at this from my phone but that's probably because without transpilation your runtime supports classes and enforces their runtime with the rule that they must be instaniated with the new keyword.

But when you transpile them, babel is converting them to normal constructor functions that's don't care whether you invoke them with new at runtime

@searls
Copy link
Member

searls commented Apr 19, 2017

(In case it's not clear I don't think your issue has anything to do with testdouble.js itself)

@svyatogor
Copy link

I am having the same issue as @kealthou. Node version 6.x, no babel used i the test scenario

@searls
Copy link
Member

searls commented Apr 26, 2017

Can you please provide a minimum example project that reproduces this, @svyatogor? I don't understand the issue.

@svyatogor
Copy link

@searls sure, here is minimal sample: https://github.com/svyatogor/td-node6-157

this is not exactly the setup I have in my project, as I use ES6-style import/export default, but the resulting error is exactly the same

@searls
Copy link
Member

searls commented Apr 29, 2017

Ok, I figured out the root cause. testdouble.js is creating a custom class which extends the replaced type so that it passes instanceof checks. However, babel generates this code:

      var _this = _possibleConstructorReturn(this, (TestDoubleConstructor.__proto__ || Object.getPrototypeOf(TestDoubleConstructor)).apply(this, arguments));

Which invokes the original constructor without new, which is not allowed by native ES classes and the interpreter throws the above error. I found this looking at facebook/create-react-app#1751 (comment)

It looks like @jdalton had a PR to babel to fix this but it was never merged. babel/babel#3582

I am not sure how I should proceed.

@searls searls reopened this Apr 29, 2017
@kealthou
Copy link

I think it's funny to try to invoke the original constructor when you want to get a mock. I'm going to use sinon's terms from here. Test doubles that testdouble.js creates are meant to be stubs (am I wrong?), but if the original implementation is invoked it's a spy.

@searls
Copy link
Member

searls commented Apr 30, 2017

@kethou - no, we definitely do not want to invoke the original constructor. There is a rule in ES class inheritance that requires super to be called in certain cases, however, and I threw that in here when converting from CoffeeScript without thinking about it.

@searls
Copy link
Member

searls commented May 5, 2017

I'm going to start tracking this in #241 now that we know what's going on

@searls searls closed this as completed May 5, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants