frank frank - 5 months ago 46
Node.js Question

Making sequential mongoose queries in a for loop inside a mocha test

I am trying to use mocha as a test driver for my API application.

I have a json of user names to song names, that I am trying to feed to an API endpoint.

json:

{
"description": "hold the lists that each user heard before",
"userIds":{
"a": ["m2","m6"],
"b": ["m4","m9"],
"c": ["m8","m7"],
"d": ["m2","m6","m7"],
"e": ["m11"]
}
}


Let's say the User and Song schema is just _id with name. What I want to do is after parsing the json file, convert "a" to user_id, by doing a mongoose lookup of "a" by name, let's say id=0. Convert each of the "m2" into song_id by name let's say id=1, and then call
the request, http://localhost:3001/listen/0/1

Here is what I have so far for my mocha test:

test.js:

it('adds the songs the users listen to', function(done){
var parsedListenJSON = require('../listen.json')["userIds"];
//console.log(parsedListenJSON);
for (var userName in parsedListenJSON) {
if (parsedListenJSON.hasOwnProperty(userName)) {
var songs = parsedListenJSON[userName];
var currentUser;
//console.log(userName + " -> " + songs);
var userQuery = User.findOne({'name': userName})
userQuery.then(function(user){
//console.log("user_id: " + user["_id"]);
currentUser = user;
});

for (var i=0; i<songs.length; i++)
{
//console.log(songs[i]);
var songQuery = Song.findOne({'name': songs[i]})
songQuery.then(function(song){
console.log("user_id: " + currentUser["_id"]);
console.log("song_id: " + song["_id"]);
// need to ensure we have both the user_id and song_id

api_request = "http://localhost:3001/listen/" + currentUser["_id"] + "/" + song["_id"];
console.log(api_request);
// listen/user_id/song_id
//.post('http://localhost:3001/listen/0/1')
request
.post(api_request)
.end(function(res){
expect(res.status).to.equal(200);
});

});
}
}
}
done();
})


Questions:

1) I need to make sure I have both the user_id and song_id before I make the API cal. Right now, each of the User.findOne and Song.findOne query has their own then clauses. I am passing the user_id through the currentUser variable into the then block of the Song query. But since the query is asynchronous, I don't think this is proper. How can I structure this code so that I proceed with the API call when both the then block have executed.

2) When I run the code as is, only the first user gets executed, and not the rest,
ie. the print out is:
user_id: 0
song_id: 1
http://localhost:3001/listen/0/1
user_id: 0
song_id: 5
http://localhost:3001/listen/0/5

3) The API endpoint works with postman, and in a simpler Mocha test below. But it doesn't seem to work in my original code.

var request = require('superagent');
var expect = require('expect.js');
var User = require('../models/user');
var Song = require('../models/song');
var mongoose = require('mongoose');
mongoose.connect('localhost/TestSongRecommender');
...
it('adds the songs', function(){
request
.post('http://localhost:3001/listen/0/2')
.end(function(res){
expect(res.status).to.equal(200);
});
});


Update:

The async.forEach approach works. Here's my final snippet:

updated test.js

var request = require('superagent');
var expect = require('expect.js');
var async = require('async');
var User = require('../models/user');
var Song = require('../models/song');
var mongoose = require('mongoose');
mongoose.connect('localhost/Test');

describe('song recommendations', function(){
it('adds the songs the users listen to', function(done){
var parsedListenJSON = require('../listen.json')["userIds"];
//console.log(parsedListenJSON);
async.forEach(Object.keys(parsedListenJSON), function forAllUsers(userName, callback) {
var songs = parsedListenJSON[userName];
//var currentUser;
//console.log(userName + " -> " + songs);
var userQuery = User.findOne({'name': userName})
userQuery.then(function(user){
//console.log("user_id: " + user["_id"]);
//console.log(songs);
//currentUser = user;
async.forEach(songs, function runSongQuery(songName, smallback) {
//console.log(songName);
var songQuery = Song.findOne({'name': songName})
songQuery.then(function(song){
//console.log("user_id: " + user["_id"]);
//console.log("song_id: " + song["_id"]);
// need to ensure we have both the user_id and song_id
api_request = "http://localhost:3001/listen/" + user["_id"] + "/" + song["_id"];
console.log(api_request);
// listen/user_id/song_id
//.post('http://localhost:3001/listen/0/1')
request
.post(api_request)
.end(function(err, res){
expect(res.status).to.equal(200);
smallback()
});

});
}, function allSongs(err) {
callback();
})
});
}, function allUserNames(err) {
done()
})
})
});

Answer

1&2) You're going to want to use the async package from NPM. You can get it with the command npm install async --save in your main folder and var async = require('async') in your code.

You'll have to replace each for loop with async.forEach. async.forEach takes an array and two functions as arguments. It calls the first function on each item in the array and the second function once all callbacks have returned.

The way you're currently doing it, with for loops isn't going to work. By the time asynchronous things return, your loop has iterated past them (non-blocking IO, remember) and your variables are no longer set correctly.

You also need things set up to call done only once all your code has run.

It will end up looking something like this if written properly:

it('adds the songs the users listen to', function(done){
    var parsedListenJSON = require('../listen.json')["userIds"];
    //console.log(parsedListenJSON);
    async.forEach(parsedListenJSON, function forAllUsers(userName, callback) {
        var songs = parsedListenJSON[userName];
        //console.log(userName + " -> " + songs);
        var userQuery = User.findOne({'name': userName})
        userQuery.then(function(user){
            //console.log("user_id: " + user["_id"]);
            var currentUser = user;
            async.forEach(songs, function runSongQuery(songName, smallback) {
              var songQuery = Song.findOne({'name': songName})
              songQuery.then(function(song){
                console.log("user_id: " + currentUser["_id"]);
                console.log("song_id: " + song["_id"]);
                // need to ensure we have both the user_id and song_id

                api_request = "http://localhost:3001/listen/" + currentUser["_id"] + "/" + song["_id"];
                console.log(api_request);
                // listen/user_id/song_id
                //.post('http://localhost:3001/listen/0/1')
                request
                    .post(api_request)
                    .end(function(res){
                      expect(res.status).to.equal(200);
                      smallback()
                    });

              });
            }, function allRan(err) {
              callback();
            })
          });
    }, function allUserNames(err) {
      done()
    }) 
  })

3) You're going to want to use a testing framework for testing your API. I recommend supertest. Once you have supertest, require your app and supertest:

var request = require('supertest');
var app = require('./testApp.js');

Use it in tests like:

request(app)
  .post(api_request)
  .end(function(res){
       expect(res.status).to.equal(200);
       smallback()
  });
Comments