Ad

Class Instance Cannot Be Passed To Function Containing Jasmine Describe- And It-blocks

- 1 answer

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?

Ad

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;
}
Ad
source: stackoverflow.com
Ad