0

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.

Adam Zerner
  • 17,797
  • 15
  • 90
  • 156

1 Answers1

4

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('test@example.com');
        });
    });
});

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

Community
  • 1
  • 1
Yaroslav Admin
  • 13,880
  • 6
  • 63
  • 83
  • I'm confused - why isn't it the case that the tests are collected in the callback and then executed? – Adam Zerner Jul 24 '15 at 16:12
  • I don't know the exact motivation authors have, but it's implemented like this. It collects all tests and then runs them. And it's not possible to schedule more tests, while executing already collected ones. See UPD for possible solutions. – Yaroslav Admin Jul 24 '15 at 16:16
  • Looking closer at your code: your request is asynchronous operation, so it finishes *most likely* after all tests are collected (step 1 is finished) and that's the problem. – Yaroslav Admin Jul 24 '15 at 16:24
  • When exactly do steps 1 and 2 happen? – Adam Zerner Jul 24 '15 at 16:25
  • How do you know that in the callback examples, the calls to `it` are happening during step 2? – Adam Zerner Jul 24 '15 at 16:31
  • The important fact is that functions `it()` and `describe()` are both synchronous. When Node loads your file with tests, your calls to `describe()` and `it()` gets executed. What they basically do is collecting your tests (functions you pass each time you call `it()`) into some data structure. Note, that all this happens synchronously. It's step 1. When it's completed step 2 starts and executes all the collected tests. – Yaroslav Admin Jul 24 '15 at 16:31
  • To answer your second question you need to know how [event loop](https://nodesource.com/blog/understanding-the-nodejs-event-loop) works. When interpreter reaches your request, it send it, puts callback to the queue of callback and continues execution until current synchronous block is over. And it's the same moment step 1 is over and step 2 is started. – Yaroslav Admin Jul 24 '15 at 16:35
  • I've updated my third example to used named functions to better communicate. So my understanding is that it starts executing the file, and initially calls `describe()` passing in `cb1`. This ends up putting `cb1` on some queue. It then continues executing this file after this first `describe`, but doesn't find anything else, so it starts calling the functions on the queue, which so far only contains `cb1`. So it then calls `cb1`, logs `foo`, and makes an HTTP request. It then finishes, doesn't have anything left in any queues, and is then done. – Adam Zerner Jul 24 '15 at 16:56
  • In the first two examples, when `cb1` is executing it calls the `it` functions, which put `cb1a` and `cb1b` on the queue to be later ran. The problem with the third example is that at the time `cb1` finishes and it's looking at the queue for more things to call, the HTTP request hasn't yet responded and we need that response to put `cb1a` and `cb1b` on the queue. – Adam Zerner Jul 24 '15 at 16:59
  • @AdamZerner No, it's not accurate. My comments was related to your second case (sorry if it was misleading). See updated answer for more details about all cases. Hope it's clear now. – Yaroslav Admin Jul 26 '15 at 11:37