Why can't nested describe() blocks see vars defined in outer blocks?

JavascriptJasmine

Javascript Problem Overview


I've run into this issue in real code, but I put together a trivial example to prove the point.

The below code works fine. I've set up a variable in my root describe() block that is accessible within my sub-describe()s' it() blocks.

describe('simple object', function () {
    var orchard;

    beforeEach(function () {
        orchard = {
            trees: {
                apple: 10,
                orange : 20
            },
            bushes: {
                boysenberry : 40,
                blueberry: 35
            }
        };
    });

    describe('trees', function () {
        it ('should have apples and oranges', function() {
            var trees = orchard.trees;
            
            expect (trees.apple).toBeDefined();
            expect (trees.orange).toBeDefined();
            
            expect (trees.apple).toEqual(10);
            expect (trees.orange).toEqual(20);
        });
        it ('should NOT have pears or cherries', function() {
            var trees = orchard.trees;
            
            expect (trees.pear).toBeUndefined();
            expect (trees.cherry).toBeUndefined();
        });
    });
});

http://jsfiddle.net/w5bzrkh9/

However, if I try to DRY up my code a little by doing the following, it breaks:

describe('simple object', function () {
    var orchard;

    beforeEach(function () {
        orchard = {
            trees: {
                apple: 10,
                orange : 20
            },
            bushes: {
                boysenberry : 40,
                blueberry: 35
            }
        };
    });

    describe('trees', function () {
        var trees = orchard.trees; // TypeError: Cannot read property 'trees' of undefined
            
        it ('should have apples and oranges', function() {
            expect (trees.apple).toBeDefined();
            expect (trees.orange).toBeDefined();
            
            expect (trees.apple).toEqual(10);
            expect (trees.orange).toEqual(20);
        });
        it ('should NOT have pears or cherries', function() {
            expect (trees.pear).toBeUndefined();
            expect (trees.cherry).toBeUndefined();
        });
    });
});

http://jsfiddle.net/goqcev42/

Within the nested describe() scope, the orchard object is undefined, even though it's defined within the it() blocks within it.

Is this intentional on the part of Jasmine's developers, possibly to avoid issues with resetting the object in beforeEach() and possible breaking some references? How do they make it happen? I could see how this might be useful, I'm just very curious as to how it works. (My guess is some apply() or call() magic, but I'm not sure how...)

--

As a side-note, I can still DRY up my code by simply using another beforeEach() block:

describe('simple object', function () {
    var orchard;

    beforeEach(function () {
        orchard = {
            trees: {
                apple: 10,
                orange : 20
            },
            bushes: {
                boysenberry : 40,
                blueberry: 35
            }
        };
    });

    describe('trees', function () {
        var trees;
        
        beforeEach(function() {
            trees = orchard.trees;
        });
            
        it ('should have apples and oranges', function() {
            expect (trees.apple).toBeDefined();
            expect (trees.orange).toBeDefined();
            
            expect (trees.apple).toEqual(10);
            expect (trees.orange).toEqual(20);
        });
        it ('should NOT have pears or cherries', function() {
            expect (trees.pear).toBeUndefined();
            expect (trees.cherry).toBeUndefined();
        });
    });
});

Javascript Solutions


Solution 1 - Javascript

The body of a describe block is executed before the beforeEach blocks.

This is exactly as expected. The problem is that your var trees variable is trying to access orchard before it has been initialized. The body of a describe block is executed before the beforeEach blocks. To solve this problem, the third code snippet is the only way to go.

Jasmine will first execute the describe blocks, and then execute the beforeEach blocks before running each test.

Solution 2 - Javascript

Well you could still initialize variables outside the beforeEach block. I generally do it for constants and still remain DRY without introducing beforeEach blocks.

describe('simple object', function () {
    const orchard = {
        trees: {
            apple: 10,
            orange: 20
        },
        bushes: {
            boysenberry: 40,
            blueberry: 35
        }
    };


    describe('trees', function () {
        const trees = orchard.trees;

        it('should have apples and oranges', function () {


            expect(trees.apple).toBeDefined();
            expect(trees.orange).toBeDefined();

            expect(trees.apple).toEqual(10);
            expect(trees.orange).toEqual(20);
        });
        it('should NOT have pears or cherries', function () {
            var trees = orchard.trees;

            expect(trees.pear).toBeUndefined();
            expect(trees.cherry).toBeUndefined();
        });
    });
});

Solution 3 - Javascript

Lets take the third code snippet. Further, it can be refactored as below:

describe('simple object', function () {
    var orchard;

    beforeEach(function () {
        orchard = {
            trees: {
                apple: 10,
                orange : 20
            },
            bushes: {
                boysenberry : 40,
                blueberry: 35
            }
        };
    });

    describe('trees', function () {
            
        it ('should have apples and oranges', function() {
            expect (orchard.trees.apple).toBeDefined();
            expect (orchard.trees.orange).toBeDefined();

            expect (orchard.trees.apple).toEqual(10);
            expect (orchard.trees.orange).toEqual(20);
        });
        it ('should NOT have pears or cherries', function() {
            expect (orchard.trees.pear).toBeUndefined();
            expect (orchard.trees.cherry).toBeUndefined();
        });
    });
});

For the new comers to Jasmine, this is how you intrepret the above code :\

  1. describe defines a test suite. The test suite name here is a user defined simple string, say "simple object".

  2. A test suite can itself contain other test suites, meaning describecan contain nested suites.

  3. Just like other programming languages, orchid is global to all the functions & suites defined within simple object test suite.

  4. It block is called a specification or a SPEC. It blocks contain individual tests.

  5. Just when Jasmine executes the test cases, it will first visit the it blocks meaning it will traverse all the it block declarations.

  6. When Jasmine actually executes test cases, it will check for beforeEach function and hence orchard gets trees value assigned to it.

  7. And hence you need not write a beforeEach function, inside a sub suite. You can simply ignore

    beforeEach (function() { trees = orchard.trees; });

  8. Now compare the latest snippet below with the third snippet above.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionKen BellowsView Question on Stackoverflow
Solution 1 - JavascriptAndrew EisenbergView Answer on Stackoverflow
Solution 2 - JavascriptaejeView Answer on Stackoverflow
Solution 3 - Javascriptnow he who must not be named.View Answer on Stackoverflow