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

Allow before() hook to be called before describe() also instead of only it() OR beforeDescribe() hook #1628

Closed
MayurVirkar opened this issue Mar 26, 2015 · 31 comments

Comments

@MayurVirkar
Copy link

Hello,
First of all, Mocha is a GREAT testing framework. It has amost everything except one feature which i would like it to have.

If we define a code in before() hook, it only gets executed before it(),
So called before() hook before describe() or added a beforeDescribe() hook.

Is it possible to add?

@Pampattitude
Copy link

I'm having the exact same problem.

My example is:

var expect = require('chai').expect;

var credentials = null;
// File
describe(require('path').basename(__filename), function() {
  before(function(done) {
    return myRequestLib.getCredentials(function(err, crd) {
      if (err)
        return done(err);

      credentials = crd;
      return done();
    });
  });

  describe('do', function() {
    // Route: /v1/:credential/doSomething
    var routeDoSomething = '/v1/' + credentials.userId + '/doSomething';
    describe(routeDoSomething, function() {
      // Test #1: List
      describe('GET', function() {
        it('should return a 200', function(done) {
          return myRequestLib.get(myRequestLib.baseUri, routeDoSomething, {
            'Authorization': credentials.userName,
          })
            .expect(200)
            .end(function(err, res) {
              if (err) return done(new Error((err.message || err) + ' (body was: ' + JSON.stringify(res ? res.body : null) + ')'));

              expect(res.body).to.exist;
              return done();
            });
        });
      });
    });
  });
});

Output:

/home/pampattitude/tests/test.js:28
    var routeDoSomething = '/v1/' + credentials.userId + '/doSomething';
                                            ^
TypeError: Cannot read property 'userId' of null

It could easily be fixed by postponing the routeDoSomething variable creating / assignation, but we use Mocha with Jenkins on a really, really plush API and it really helps having the route name directly given.

Regards,

P.S.: I somehow feel I'm out offtopic, so, if I am, please ignore my comment.

@dasilvacontin
Copy link
Contributor

var getRouteDoSomething = function() {
  return '/v1/' + credentials.userId + '/doSomething';
}

Or you could even make the credentials an argument.

Is it enough of a fix for you?

@MayurVirkar
Copy link
Author

For his example, it can be a fix.
But for my issue I need a hook that runs before describe in that block.
Is there any easy way to add it?
On Mar 27, 2015 4:43 AM, "David da Silva Contín" notifications@github.com
wrote:

var getRouteDoSomething = function() {
return '/v1/' + credentials.userId + '/doSomething';
}

Or you could even make the credentials an argument.

Is it enough of a fix for you?


Reply to this email directly or view it on GitHub
#1628 (comment).

@dasilvacontin
Copy link
Contributor

I just don't see how you could need it when you can set variables inside the before, beforeEach. And you can do it asynchronously, if needed.

describe('setting vars inside before', function () {
    before(function () {
        this.credentials = {id: 123};
    })

    it('it should work', function () {
        (this.credentials.id).should.equal(123);
    })
})

@Pampattitude
Copy link

The problem still holds. mayurvirkar, I don't know what your use case it -- and don't hesitate to tell me if you feel I pollute the conversation -- but having before() be called before it() and not describe() means I can't reference a variable assigned in before() anywhere before the it() body.

It might be needed for logging, caching or anything you'd want to do but exclude from it() (could be for performance / response time reasons, for a more relevant output, etc.. In my case, I can't have relevant logs because of this.

@dasilvacontin, about what you suggested, it does not help with the fact that I can't use the route string in the first describe() argument (because the credential.userId variable is not filled before before is called, i.e. right before the it() call).

@dasilvacontin
Copy link
Contributor

@Pampattitude but why do you need to do it in the describe scope, when you can add as many hooks as you want before the suite or the test, and assign values to variables while you are in there?

You can do the assignments and the logs inside the hooks. If you don't feel like you can explain your case-scenario via words, feel free to show code.

@boneskull
Copy link
Contributor

@Pampattitude @MayurVirkar

I'm pretty certain you're both looking for the --delay flag.

var foo;

// process.nextTick to be replaced with your async setup function
process.nextTick(function() {
  foo = 'bar'; // or "credentials" object w/ user id
  run(); // this is exposed when running mocha with the --delay flag
});

describe(require('path').basename(__filename), function() {
  describe(foo, function() {
    it('should be true', function() {
      require('assert')(true);
    });
  })
});

@boneskull
Copy link
Contributor

@dasilvacontin @Pampattitude is saying you can't define a describe() block after some sort of asynchronous setup--which you couldn't until recently--but now you can with the --delay flag.

@boneskull
Copy link
Contributor

(Basically, what happens is there's an implicit describe at the root of your module--the "root suite"--and all describe blocks contained within your module are children of the root suite. By using --delay, we wait until run() is executed to execute all child suites.

This works here because foo is used as the descriptor for a child-of-a-child. If you needed it at the "top" level describe block, you would need to nest the describe within the process.nextTick() callback. See test/delay/delay.js for another example.)

@MayurVirkar
Copy link
Author

Hey @boneskull,
I think this is the solution, I will test it and let you know. Also paste my code here. but again, not only for me, but for many others with similar issue,
I would suggest adding beforeDescribe() hook or executing before() hook before executing describe() function. This will give coders alot more control over describing their test cases. They will also be able to define it() in a loop with data received from callback easily. (This is what i am trying to do)

and plus I also dont think adding a beforeDescribe() hook will be really that difficult :)

@boneskull
Copy link
Contributor

before won't be called before describe. A beforeDescribe() hook is unneccessary given the --delay functionality, and will not be added.

@ORESoftware
Copy link

+1

I need it for dynamic tests with loops like so

first, I tried this:

describe('@Test_Enrichment*', function () {

    var self = null;  // ok, great, we can work with self ....

    before(function () {

        this.config = require('univ-config')(this.test.parent.title, 'setup/testSetup');
        this.constants = this.config.get('sc_constants');
        this.serverURL = this.config.get('test_env').smartconnect_server_config.url;
        this.serverPort = this.config.get('test_env').smartconnect_server_config.port;

          self.testCases = [];

            // Payloads
            var files = fs.readdirSync(__dirname + '/test_data/enriched_payloads');
            for (var i in files) {
                if (path.extname(files[i]) === '.json') {
                    self.testCases.push(__dirname + '/test_data/enriched_payloads/' + files[i]);
                }

            }
    });

    describe('start', function () {

        before(function(done){
               done(); // nothing here...yet...
        });

        self.testCases.forEach(function (json, index) {   // self is null !!!

            describe('test enrichment', function () {

                it('[test] ' + path.basename(json), function (done) {

                    var jsonDataForEnrichment = require(json);
                    jsonDataForEnrichment.customer.accountnum = "8497404620452729";
                    jsonDataForEnrichment.customer.data.accountnum = "8497404620452729";
                    var options = {
                        url: self.serverURL + ':' + self.serverPort + '/event',
                        json: true,
                        body: jsonDataForEnrichment,
                        method: 'POST'
                    };

                    request(options, function (error, response, body) {
                        if (error) {
                            cb(error);
                        }
                        else {
                            assert(response.statusCode == 201, "Error: Response Code");
                            cb(null);
                        }

                    });
                });
            });
        });
    });
});

then I just kicked the can further down the road by trying this:

describe('@Test_Enrichment*', function () {

    var self = null;  // ok, great, we can work with self ....

    before(function () {

        this.config = require('univ-config')(this.test.parent.title, 'setup/testSetup');
        this.constants = this.config.get('sc_constants');
        this.serverURL = this.config.get('test_env').smartconnect_server_config.url;
        this.serverPort = this.config.get('test_env').smartconnect_server_config.port;

    });

    describe('start', function () {

        before(function(){

            self.testCases = [];

            // Payloads
            var files = fs.readdirSync(__dirname + '/test_data/enriched_payloads');
            for (var i in files) {
                if (path.extname(files[i]) === '.json') {
                    self.testCases.push(__dirname + '/test_data/enriched_payloads/' + files[i]);
                }

            }
        });

        self.testCases.forEach(function (json, index) {   //self is null !!!

            describe('test enrichment', function () {

                it('[test] ' + path.basename(json), function (done) {

                    var jsonDataForEnrichment = require(json);
                    jsonDataForEnrichment.customer.accountnum = "8497404620452729";
                    jsonDataForEnrichment.customer.data.accountnum = "8497404620452729";
                    var options = {
                        url: self.serverURL + ':' + self.serverPort + '/event',
                        json: true,
                        body: jsonDataForEnrichment,
                        method: 'POST'
                    };

                    request(options, function (error, response, body) {
                        if (error) {
                            cb(error);
                        }
                        else {
                            assert(response.statusCode == 201, "Error: Response Code");
                            cb(null);
                        }

                    });
                });
            });
        });
    });
});

I am totally HOSED because I cannot nest it()s, but I can't use self.testCases, because the before hook doesn't run before the describe, only before the it()

so it turns out, unless you can help me, I do NEED a before hook for describe

@danielstjules
Copy link
Contributor

Unless I'm mistaken, you don't need to put it in the hook. You don't have to rely on hooks for test setup, especially when generating specs. See http://mochajs.org/#dynamically-generating-tests

describe('@Test_Enrichment*', function () {
  var config, constants, serverURL, serverPort;

  before(function () {
    // This doesn't even need to be in the hook
    config = require('univ-config')(this.test.parent.title, 'setup/testSetup');
    constants = config.get('sc_constants');
    serverURL = config.get('test_env').smartconnect_server_config.url;
    serverPort = config.get('test_env').smartconnect_server_config.port;
  });

  describe('start', function () {
    var dir = __dirname + '/test_data/enriched_payloads';
    var files = fs.readdirSync(dir).filter(function(file) {
      return (path.extname(file) === '.json');
    }).map(function(file) {
      return __dirname + '/test_data/enriched_payloads/' + file;
    });

    files.forEach(function(file) {
      it('[test] ' + path.basename(file), function (done) {
        var jsonDataForEnrichment = require(file);
        jsonDataForEnrichment.customer.accountnum = "8497404620452729";
        jsonDataForEnrichment.customer.data.accountnum = "8497404620452729";
        var options = {
          url: serverURL + ':' + serverPort + '/event',
          json: true,
          body: jsonDataForEnrichment,
          method: 'POST'
        };

        request(options, function (error, response, body) {
          if (error) return done(error);

          assert(response.statusCode == 201, "Error: Response Code");
          done();
        });
      });
    });
  });
});

@ORESoftware
Copy link

thanks @danielstjules , that worked very well, but re: the line "// This doesn't even need to be in the (before) hook" - as it stands, it does need to be in the hook because of the following line which contains "this.test.parent.title" which leads me to another beef with Mocha, why the context isn't that consistent between before() it() and describe()

@danielstjules
Copy link
Contributor

as it stands, it does need to be in the hook because of the following line which contains "this.test.parent.title"

I'm not sure why you can't hardcode the string? You're defining the title as @Test_Enrichment* There's certainly some limitations, but workarounds should be simple enough

@ORESoftware
Copy link

thanks, I looked into it, FYI in the describe call back, you can use 'this.title' instead of this.test.parent.title in the before hook. this keeps it dynamic and you can share code between tests

@ORESoftware
Copy link

so I removed the before hook as you suggested

@OhDavit
Copy link

OhDavit commented May 30, 2016

I need this feature, because within before I execute some async code by preparing date for test.

@ScottFreeCode
Copy link
Contributor

@OhDavit, is the async code needed in order to define the tests, or only in order to run them? If it's only needed to run them, you should be able to use a done parameter to the before callback. If it is needed in order to define the tests, can it be handled using the delay option and both defining the test suite and calling run() in the asynchronous callback?

@bridiver
Copy link

we have some nested tests that all have common setup, but we'd like that common setup to run before each describe block and not before each it block. The setup is time consuming and each describe block has multiple it blocks where assertions are made. I don't think the delay flag applies to this case

@ScottFreeCode
Copy link
Contributor

@bridiver Sounds like you're looking for plain old before hooks? You can put one in each describe block and it will run only once before all of that block's it tests. You can also put it in an outer describe block and it will only run once even if there are inner describe blocks, if you want to share the resources that were set up (or you can even put it outside any describe block if you want it to be shared across all the tests). And if you do need it to run multiple times but it should run the same thing each time, since JavaScript functions are first-class you can just write the function elsewhere and pass it into multiple different before calls.

Basically, --delay is for defining the suite asynchronously and before is for everything else.

@bridiver
Copy link

bridiver commented May 17, 2017 via email

@ScottFreeCode
Copy link
Contributor

I need to learn to stop giving people all the possible angles... Here's the money quote:

And if you do need it to run multiple times but it should run the same thing each time, since JavaScript functions are first-class you can just write the function elsewhere and pass it into multiple different before calls.

In other words, instead of this:

describe("outer", function() {
  someNewHookMochaMustImplement(function () {
    console.log("I run at the start of each nested describe!")
  })
  describe("inner 1", function() {
    // multiple `it` here
  })
  describe("inner 2", function() {
    // multiple `it` here
  })
})

You can just do this:

function reusableSetup() {
  console.log("I should be used at the start of each describe!")
}
describe("outer", function() {
  describe("inner 1", function() {
    before(reusableSetup)
    // multiple `it` here
  })
  describe("inner 2", function() {
    before(reusableSetup)
    // multiple `it` here
  })
})

Now, I'm not necessarily saying there are no disadvantages to the latter over the former or no advantages to the former over the latter. But I am saying, this can be done without a new hook, so if you want to argue for it you'll need to make a more specific case that the difference between those two ways of doing it, one of which is already available, outweighs the added maintenance cost and increased user learning curve of adding Yet Another Hook Variation. (Keep in mind there are... a lot of different possible behaviors for hooks, so much so that we have multiple issues open already about whether the hooks we already have behave as they ought.)

Also, this is a somewhat old issue and wasn't actually about your suggestion as far as I can tell -- there doesn't seem to be any prior discussion of hooks that automatically run before multiple describes at the test run phase, and prior discussion appears to center around having a hook that would run before a describe at the test definition phase; so if you really want to make the case for a beforeEveryNestedDescribe hook or something along those lines, we should probably stop bugging the folks on this issue with further discussion of it.

@ScottFreeCode
Copy link
Contributor

For future reference for anyone else digging up this issue, so that the last thing on the page is the answer:

Is the async code needed in order to define the tests, i.e. to determine the number of tests or their names? Or only in order to run them?

  • If it's only needed to run them, you should be able to use asynchronous before/beforeEach hooks either with the done parameter or returning a promise; save any needed data in a local variable of the describe callback and access it in the tests.
  • If it is needed in order to define the tests (determine the names or number of tests), use the delay option, write your asynchronous code at the top level of your test file (outside any describe) and both define the test suite and afterward call run() in the asynchronous code's callback.

@Delilovic
Copy link

Delilovic commented Jun 16, 2019

Something from a new mocha user.

Learning mocha was really fast, but this is the only case which wasn't intuitive for me. I really expected to find data in my next describe block from the before hook of the parent describe block.

describe('Requesting response from validateResponse function using invalid data', () => {
      let response = null;
      const someInvalidData = null;

      before(() => {
        const wrapped = test.wrap(myFunctions.validateResponse);
        response = await wrapped(someInvalidData);
      });


      describe('Checking if response is valid', () => {
        testResponseObjStructure(response); // @this place response is still null
      });

Code from testResponseObjStructure

const expect = require('chai').expect;

module.exports = function(response) {
  it('Expect response to be an Object', () => {
    expect(response).to.be.an('Object');
  });
};

@plroebuck
Copy link
Contributor

describe('Requesting response from "validateResponse" function using invalid data', () => {
  let response;
  const someInvalidData = null;
 
  before(async () => {      // <= maybe wait for answer?
    const wrapped = test.wrap(myFunctions.validateResponse);
    response = await wrapped(someInvalidData);
  });

  describe('Checking if response is valid', () => {
    testResponseObjStructure(response); // @this place response is still null
  });
});

@Delilovic
Copy link

Delilovic commented Jun 16, 2019

Maybe for easier testing this code reproduces the same error:

AssertionError: expected null to be an object

//index.test.js
const testResponseObjStructure = require('./test.response');

describe.only('Testing validateResponse function', () => {
    describe('Requesting response from validateResponse function using invalid data', () => {
      let response = null;

      before(async () => {
        response = await {}; // here i get the response from server
      });

      describe('Checking if response is valid', () => {
        testResponseObjStructure(response); //@this place response is still null
      });
    });
  });
//test.response.js
const expect = require('chai').expect;

module.exports = function(response) {
  it('Expect response to be an Object', () => {
    expect(response).to.be.an('Object');
  });
};

@SpiritOfSpirt
Copy link

SpiritOfSpirt commented Aug 28, 2020

const asyncAnswer = null;

before(doHereAsyncStuff and get asyncAnswer);

describe('foo', function() {
    it(`bar`, function(done1) {
        done1();
        describe('baz', function() {
            it(`Start real test with asyncAnswer`, function(done) {
                Here real test with asyncAnswer
            });
        });
    });
});

@dasilvacontin
Copy link
Contributor

@SpiritOfSpirt As far as I know, you can't define new suites and tests after test execution has started. But I may be wrong.

@Delilovic I'd suggest writing all your code/logic inside hooks and tests. In other words, things that get run during test execution. (Runnables)

@bx2651
Copy link

bx2651 commented May 21, 2021

I want get some async data in before() hook , but it only gets executed before it(),
how should i do ?

describe("my test", () => {
  let asyncData
  before(() => {
    asyncData = getAsyncData()
    // asyncData = {
    //   name: grandfather,
    //   children: [
    //     {
    //       name: father,
    //       children: [
    //         { name: Lily },
    //         { name: Lucy }]
    //     },
    //     {
    //       name: uncle,
    //       children: []
    //     }]
    // }
  })
  describe(asyncData.name, () => {
    asyncData.children.forEach(elder => {
      describe(elder.name, () => {
        elder.children.forEach(child => {
          it(child.name, () => {
            console.log(child.name)
          })
        })
      })
    })
  })
})

@SpiritOfSpirt
Copy link

SpiritOfSpirt commented May 21, 2021 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests