Search code examples
javascriptnode.jsjasminejasmine-node

Can you nest it() blocks in a request callback?


It seems that you could put "stuff" inside describe but outside it:

describe('1', function() {
  var foo = 'bar';
  console.log(foo);
  it('1a', function(done) {
    expect(true).toBe(true);
    done()
  });
  it('1b', function(done) {
    expect(true).toBe(true);
    done();
  });
});

And it seems that you could put the it blocks inside of a function that is called:

describe('1', function() {
  (function() {
    it('1a', function(done) {
      expect(true).toBe(true);
      done()
    });
    it('1b', function(done) {
      expect(true).toBe(true);
      done();
    });
  })()
});

In both cases the output is:

~/code/study-buddies $ jasmine-node server
bar
..

Finished in 0.005 seconds
2 tests, 2 assertions, 0 failures, 0 skipped

But when I try to put my it blocks inside of a Node request callback, it doesn't seem to run the tests:

describe('1', function cb1() {
  var foo = 'bar';
  console.log(foo);
  var request = require('request');
  request.get('http://localhost:3000/api/posts', function() {
    console.log('here');
    it('1a', function cb1a(done) {
      console.log('1a');
      expect(true).toBe(true);
      done();
    });
    it('1b', function cb1b(done) {
      console.log('1b');
      expect(true).toBe(true);
      done();
    });
  });
});

Output:

~/code/study-buddies $ jasmine-node server
bar


Finished in 0.007 seconds
0 tests, 0 assertions, 0 failures, 0 skipped


here

When I run my real code:

var request = require('request');
var base_url = 'http://localhost:3000/api/posts';

describe('Posts', function() {
  var post;

  beforeEach(function(done) {
    var options = {
      url: base_url,
      method: 'POST',
      json: {
        title: 'one'
      }
    };
    request(options, function(err, response, body) {
      if (err) console.log('ERROR IN THE SET UP.');
      else { post = body; }
      done();
    });  
  });

  afterEach(function(done) {
    request.del(base_url+'/'+post._id, function(err, response, body) {
      if (err) console.log('ERROR IN THE TEAR DOWN.');
      else { post = null; }
    });
    done();
  });
  describe('GET /', function() {
    it('returns a status code of 200', function(done) {
      request.get(base_url, function(err, response, body) {
        expect(response.statusCode).toBe(200);
        done();
      });
    });
    it('returns the right posts', function(done) {
      request.get(base_url, function(err, response, body) {
        expect(JSON.parse(body)).toEqual([post]);
        done();
      });
    });
  });
  describe('GET /:id', function() {
    it('returns a status code of 200', function(done) {
      request.get(base_url+'/'+post._id, function(err, response, body) {
        expect(response.statusCode).toBe(200);
        done();
      });
    });
    it('returns the right post', function(done) {
      request.get(base_url+'/'+post._id, function(err, response, body) {
        expect(JSON.parse(body)).toEqual(post);
        done();
      });
    });
  });
  describe('POST /', function() {
    var options = {
      url: base_url,
      method: 'POST',
      json: {
        title: 'two'
      }
    };

    request(options, function(err, response, body) {
      it('returns a status code of 201', function(done) {
        expect(response.statusCode).toBe(201);
        done();
      });
      it('returns the created Post', function(done) {
        expect(body).toEqual({title: 'two'});
        done();
      });
    });
  });
  describe('PUT /:id', function() {

  });
  describe('DELETE /:id', function() {

  });
});

I get this no output:

~/code/study-buddies $ jasmine-node server
~/code/study-buddies $

Why is this?

Note: I'm trying to nest the it blocks under the request because both it blocks are making the same request, so I want to be DRY.


Solution

  • There are two steps here:

    1. Collect tests (calls to it()).
    2. Execute collected tests.

    In your first example your tests are defined, while handling step 1. In your second example (with callback) you calls to it are executed during step 2 and therefore they are not handled by Jasmine.

    How to handle

    You need either define separate tests or use only calls to expect() inside your callback without calls to it().

    Usually you want to define separate test if you need to send request to the same route, but with different parameters. Typical example is to test API behavior with valid and invalid data:

    describe('POST /users/', function() {
        it('should create user', function (done) {
            request.post('/users/', { /* valid data */ }, function () { /* expect clauses */ });
        });
    
        it('should respond with errors given invalid data', function (done) {
            request.post('/users/', { /* invalid data */ }, function () { /* expect clauses */ });
        });
    });
    

    You want to use multiple expect() statements inside single test, when you want to test different parts of a single request. For example check response code and values of few parameters:

    describe('GET /users/{id}/', function() {
        it('should return user', function (done) {
            request.get('/users/14/', function (err, response, body) { 
                expect(response.code).toBe(200);
                expect(body.username).toBe('test');
                expect(body.email).toBe('[email protected]');
            });
        });
    });
    

    Obviously there are some in-between cases and that's up to you to decide whether each concrete case is important enough to be put into separate test (and duplicate the same request) or can be handle by additional expect() statements inside single test.

    In depth explanation

    This requires some background knowledge from reader:

    • understand difference between synchronous & asynchronous
    • know what is event loop in Node.js and how it works
    • understand that function, which accepts another function is not necessary asynchronous (uses event loop for callback) (like forEach for example)

    Some facts before continue:

    • Jasmine expects all test cases to be defined synchronously. After call to main describe() in file is completed it starts execution of collected tests.
    • Calls to describe() and it() are synchronous, while execution of HTTP requests are asynchronous (uses event loop).
    • describe() creates a namespace for tests and synchronously calls function provided as second argument. it() adds it's second argument as a test to current namespace.

    Let's trace your first case:

    1. Call to describe() creates namespace 1 and synchronously executes cb1().
    2. Call to it() synchronously adds test 1a.
    3. Call to it() synchronously adds test 1b.
    4. Execution of cb1() is completed, as well as test collection step.
    5. Start execution of collected tests.
    6. Execute tests.
    7. Print results.
    8. Exit program.

    Addition of IIFE doesn't change anything, since it's just calls itself synchronously.

    Let's trace your second case:

    1. Call to describe() creates namespace 1 and synchronously executes cb1().
    2. Send request, put callback to the event loop queue (remember HTTP requests are asynchronous?) and continue execution on the next line.
    3. Execution of cb1() is completed, as well as test collection step.
    4. Start execution of collected tests.
    5. Since there are not tests collected, print 0 tests executed.
    6. Execution of the current call stack is finished. So take next function from the event loop queue, which is request callback.
    7. Call to it() tries to add test 1a, but execution of tests is already completed.
    8. Exit program.

    At this point it should be clear that you can't and shouldn't define tests inside asynchronous callbacks. So your third example should never be written. And question why it completes without any output should never be asked.


    But just out of the interest I looked at the Jasmine source code and did some debugging to see what actually happened. Here you go.

    You probably noticed that functions describe() and it() aren't imported by you, but provided by Jasmine itself. Besides importing these functions for you Jasmine also provides some internal state used by it() and describe() to collect tests. While importing tests this internal state is set, but while running it's not.

    When you call it() from the request callback you do this at the stage of running tests and internal set is not set. So it fails with Error: jasmine.Suite() required error. This error causes Jasmine to exit immediately without any output.

    You will probably ask why does it print results in the second example then? That's easy: in your second example you don't have any other tests, so at the moment of call to it() results are already printed. You can check it by adding another console.log() call between calls to it() (it will never be printed).