Class Instance Cannot Be Passed To Function Containing Jasmine Describe- And It-blocks
Ok, my setup is a little convoluted but the error I get is still interesting.
I have a Jasmine test suite for the webdriverio testrunner which contains a function declared elsewhere to which I pass an instance of a class. This function in turn contains more nested describe- and it-blocks.
const MyObject = require('./my-object');
const { passObject, myFunction } = require('./obj-fns');
var myObjectInstance;
describe("testing passing objects as parameters", function() {
beforeAll(function() {
myObjectInstance = new MyObject();
});
it("for a class instance inside describe/it block directly", function() {
browser.pause(250);
expect(myObjectInstance).not.toBe(undefined);
expect(myObjectInstance.selector).toBe("object-class-instance");
});
it("for a function inside describe/it block directly", function() {
browser.pause(250);
expect(myFunction).not.toBe(undefined);
expect(myFunction().selector).toBe("object-function");
});
passObject(myFunction, myObjectInstance);
});
my-object.js
class MyObject {
constructor() {
this.selector = "object-class-instance";
}
}
module.exports = MyObject;
obj-fns.js
module.exports.passObject = function(fn, obj) {
describe("inside a function that has a describe block", function() {
it("and an it block for a class instance", function() {
browser.pause(250);
expect(obj).not.toBe(undefined);
expect(obj.selector).toBe("object-class-instance");
});
it("and an it block for a function", function() {
browser.pause(250);
expect(fn).not.toBe(undefined);
expect(fn().selector).toBe("object-function");
})
});
}
module.exports.myFunction = function() {
return {
selector: "object-function"
}
}
All tests pass except the it
block inside the passObject
function that checks for the object (class instance that was passed) to not be undefined.
pass: testing passing objects as parameters for a class instance inside describe/it block directly
pass: testing passing objects as parameters for a function inside describe/it block directly
pass: testing passing objects as parameters inside a function that has a describe block and an it block for a function
fail: testing passing objects as parameters inside a function that has a describe block and an it block for a class instance (chrome_undefinedVersion with 0-0 runner)
Error: Expected undefined not to be undefined.
at <Jasmine>
at UserContext.<anonymous> (C:\dev\eclipse-workspace\FrontendTesting\Daimler.Van.SPP.Dashboard.Web.FrontendTesting\spec\obj-fns.js:6:29)
at <Jasmine>
Is this as designed, and if yes, how can I pass a class instance to a function that has describe- and it-blocks? Or am I missing something?
Answer
The issue is that when you call passObject(myFunction, myObjectInstance);
that gets executed immediately before the myObjectInstance
variable gets assigned a value. So when the code inside passObject
runs, it uses obj
as undefined
for each test as that is what was passed in at the time.
You can instead do something different and pass a factory function to your test function. This will allow you to share code between both the main tests and the ones in passObject
while still allowing you the flexibility to run passObject
with a different object.
Here is what the code would look like:
module.exports.passObject = function(fn, createObj) {
describe("inside a function that has a describe block", function() {
beforeAll(function() {
this.obj = createObj();
});
it("and an it block for a class instance", function() {
browser.pause(250);
expect(this.obj).not.toBe(undefined);
expect(this.obj.selector).toBe("object-class-instance");
});
it("and an it block for a function", function() {
browser.pause(250);
expect(fn).not.toBe(undefined);
expect(fn().selector).toBe("object-function");
})
});
}
passObject
now takes a function that creates obj
instead of just taking obj
. It's also using the this
context to pass that object to the tests.
When using this, you would need to extract the initialisation of the object and reuse it in both places:
function createInstance { return new MyObject(); }
describe("testing passing objects as parameters", function() {
beforeAll(function() {
myObjectInstance = createInstance()
});
it("for a class instance inside describe/it block directly", function() {
browser.pause(250);
expect(myObjectInstance).not.toBe(undefined);
expect(myObjectInstance.selector).toBe("object-class-instance");
});
it("for a function inside describe/it block directly", function() {
browser.pause(250);
expect(myFunction).not.toBe(undefined);
expect(myFunction().selector).toBe("object-function");
});
passObject(myFunction, createInstance);
});
This results in slight code repetition as in both cases a beforeAll
calls a function, assigns the result to a variable and re-uses the variable in the tests. That is fine since the variable is different in both cases, so the different tests don't have to call it the same thing. However, you could eliminate even this repetition if you want at the risk of slightly obfuscating your code.
First, extract the entire beforeAll
code into a
function setup() {
this.instance = new MyObject();
}
Then you can use that in your beforeAll
directly
// in the main tests
beforeAll(setup)
module.exports.passObject = function(fn, setupFn) {
describe("inside a function that has a describe block", function() {
beforeAll(setupFn);
/* ...tests...*/
}
}
However, this will mean that both tests have to refer to the same thing: this.instance
and it's not immediately clear what should or would get initialised in the this
context for the tests. Also, one set of tests might need more things initialised, while another less, e.g., one might just use this.instance
another might also need this.foo
.
So, if you opt for this route, you have to keep the scope of the tests in check in order to not have the setup function just grow and grow to accommodate every single possible property.
Also, if you want to use different names for the same thing, then you can still just have two properties that point at the same value. This would alleviate some of the imposed style for the two sets of tests. For example:
function setup() {
//for the main tests
this.myObjectInstance = new MyObject();
//for `passObject` just alias the same thing:
this.obj = this.myObjectInstance;
}
Related Questions
- → How to update data attribute on Ajax complete
- → October CMS - Radio Button Ajax Click Twice in a Row Causes Content to disappear
- → Octobercms Component Unique id (Twig & Javascript)
- → Passing a JS var from AJAX response to Twig
- → Laravel {!! Form::open() !!} doesn't work within AngularJS
- → DropzoneJS & Laravel - Output form validation errors
- → Import statement and Babel
- → Uncaught TypeError: Cannot read property '__SECRET_DOM_DO_NOT_USE_OR_YOU_WILL_BE_FIRED' of undefined
- → React-router: Passing props to children
- → ListView.DataSource looping data for React Native
- → Can't test submit handler in React component
- → React + Flux - How to avoid global variable
- → Webpack, React & Babel, not rendering DOM