mochajs / mocha

☕️ simple, flexible, fun javascript test framework for node.js & the browser
https://mochajs.org
MIT License
22.57k stars 3.01k forks source link

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

Closed MayurVirkar closed 9 years ago

MayurVirkar commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago
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 commented 9 years ago

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 https://github.com/mochajs/mocha/issues/1628#issuecomment-86750456.

dasilvacontin commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

@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 commented 9 years ago

@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 commented 9 years ago

@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 commented 9 years ago

(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 commented 9 years ago

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 commented 9 years ago

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

ORESoftware commented 9 years ago

+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 commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

so I removed the before hook as you suggested

OhDavit commented 8 years ago

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

ScottFreeCode commented 8 years ago

@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 commented 7 years ago

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 commented 7 years ago

@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 commented 7 years ago

No, that is definitely not what I'm looking for. I want a common setup to run once for each describe block. 'before' only runs once for the entire block and all nested blocks, not once for each nested block

On May 16, 2017, at 8:19 PM, Scott Santucci notifications@github.com wrote:

@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.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or mute the thread.

ScottFreeCode commented 7 years ago

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 commented 6 years ago

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?

Delilovic commented 5 years ago

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 commented 5 years ago
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 commented 5 years ago

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 commented 4 years ago

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 commented 4 years ago

@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 commented 3 years ago

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 commented 3 years ago

describe('waiting', function() { it(async data, function(done1) { done1(); describe(asyncData.name, () => { asyncData.children.forEach(elder => { describe(elder.name, () => { elder.children.forEach(child => { it(child.name, () => { console.log(child.name) }) }) }) }) }) }) }) // try this

пт, 21 трав. 2021 о 10:39 bx2651 @.***> пише:

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) }) }) }) }) }) })

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/mochajs/mocha/issues/1628#issuecomment-845732888, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACTYYZLO6O5YJXPS7JWMGMDTOYE4RANCNFSM4A6UDGWA .