-
Notifications
You must be signed in to change notification settings - Fork 0
Testing
In this module we're going to talk about testing and I know everybody tests differently and everybody prefers different things, so we're going to take a couple of different approaches to testing, so that you can understand a couple different ways to do it, and then take a pick as to the way that works best for you and your application. As a start we've built out a RESTful API with all of the HTTP verbs necessary, we haven't yet talked about hyper media, but we will get to that in our next module. For right now we're going to talk about unit testing to start with with Mocha and as part of unit testing the first thing we need to do is separate out our code, so that we have something that we can wrap in a test, so we're going to build out controllers for our routes, and then we're going to mock out our response objects with Sinon, so that we can then unit test our controllers. After that we're going to talk about integration tests and as part of that we'll use something called Supertest that's going to allow us to execute our HTTP calls and carry that all the way through to our database, so we can do our posts and our gets and test everything end to end in a testing framework.
Now if you've been following along your code should have a file called bookRoutes and this is where we've put all of our routing code for our API. Now what we want to do as part of our separation into controllers is if you look at this book route right here for slash, when you post we execute this function, which is an anonymous function. What we want to do as part of building out our controllers for unit testing is we want to take these anonymous functions and stick them in controllers, so that instead of that instead I just say (Typing) bookController.post and what that's going to do is execute whatever function that is, and I'll just take that function code that we just had and drop it in a controller called bookController, so the only other piece of code we need up here is we need var bookController (Typing) = require and we're going to pull this in from a folder called controllers/bookController, bookController.js. Alright, to get this going we're going to do a var bookController equals a function (Typing) and then we're going to do module.exports = bookController, now execute. Okay, now in this bookController we need to create a method called post because that's what we're going to call over on the other side and we're going to make that equal to that function that we copy and pasted out of our bookRoutes. Now notice we need this book and remember book is the Mongoose model that does all of the database work for us. Now I don't want to create a new instance of that inside my controller because I want to be able to mock that later because remember, this module is all about unit testing, so what I want to do is I want to pass that in to my bookController when it's first created, so now I added book to my function call on var bookController. Now over here in bookRoutes I want to make sure that in my bookController = require Controllers --- bookController, here I'm going to pass in Book when I do my require. Let's go ahead and do the get and so we'll pull this function out and do bookController.get. Come back over to bookController, do var get = and that function call. Here we're using something called the revealing module pattern and what that means is I have a controller and I have a series of functions and now I have to return back the functions that I'm going to expose to the outside world and, in this case, we're going to return two things. We're going to return our post function and we're going to return our get function. Now my bookRoutes will have access to two functions, the post function and the get function. Now the first big test to make sure all of this works is to come over here and type gulp to make sure everything compiles and it starts up and it's running. Here we go, we're up and running. Let's take a quick look and we can test these two controllers and make sure they're working and then we'll build some unit tests, so that you can see how this all works.
Okay, so if we've done everything correctly we should be able to come into Postman and do localhost8000/API/books, click Send, and pull back our list of books, so that part works, let's try doing our post, and I'm going to post just a genre and an author. Let's post, make sure that works, and that worked. Now you'll notice I just posted and created a new book without a title and we probably should check for that and make sure that we don't allow books being created without a title, so in proper TDD unit testing kind of framework let's create a test that replicates this issue, and then we'll write the code to fix it, so let's break back over, and let's start looking at how to unit test our code using Mocha.
Let's start by creating a new directory called Tests and in that we're going to create a new Javascript file called bookControllerTests.js and this is where we're going to put all of our test code for our bookController. Now before we get started we need to do an NPM install of gulp-mocha, gulp is what we're going to use to run our Mocha tests, so we need the gulp plugin, gulp-mocha, we're going to do should, which is an assertion framework that we're going to use, and we're going to need Sinon, which is our mocking frame, and don't forget --save, so that it will remember that we need these. Once that's done we're going to come back over here and we're going to do a var should. We're going to pull in an instance of should or a reference to should and we're going to pull in a reference through Sinon. Now we don't need a reference to Mocha because it's actually going to run inside the Mocha framework. In our case it's going to run inside gulpMocha, but it's essentially the same thing. Now Mocha lays out very similar to a standard BDD kind of style, so you start with the describe keyword and describe what it is we're doing, so we're going to start with describe Book Controller Tests, and then it takes a function because it's Javascript (Typing) and then we can actually chain these. We're going to do another describe and here we're going to describe our Post tests, and what that means is at this point we are testing the Post method of Book Controller, and now we can layout our tests. In this case we're going to do it and then plain text, should not allow an empty title on post. Now let's take a quick look at the Book Controller and see what this Post method does. Now it's going to take Book and it's going to create an instance of Book with req.body and it's going to do save on that book and it's going to do a res.status, so in order to mock out the things that we need we need to send in a mock book, we need to send in a mock request, and a mock response, and we're going to check for a status coming back that says, hey you don't have all the stuff in here that you need, so let's go back over here, and we'll start writing this out. Let's create our function (Typing). Now one of the nice things here is that Javascript is not a type safe language, which means I don't need to have an objects of type book in order to do any of this, I can just create a book (Typing) that's just a function that takes a book and has something called save (Typing) that doesn't really do anything because if you remember over here on bookController it just calls save. It doesn't need save to do anything, so we don't need save to work in this case for this test, so I'm going to do var Book = function(book), this.save = function just like that and now I have a mock book object that I can use in my bookController test. Now unfortunately for the request and the response it's not going to be quite that easy because I actually need those to do something, so in this case we're going to do var the request actually has to contain a body that actually has some data in it and, in this case, we're just going to send it an author. Remember this test is to see whether or not it's throwing an error when there's no title, so we're going to send in an author, no title, and see what happens, so let's send just me as the author, and that's the request. The response becomes a little more difficult because if we look back over in our controller we'll see that I actually have to call some functions and I want to know what is sent as part of the status and I want to know what's sent as part of the book and so here I'm going to have to actually mock something out using our Sinon mocking framework. Now, in this case, our response needs to have two things in it, it needs to have a status and it needs to have a send. Those are the two functions that we need. Now in order to mock this out we're going to use something called a spy, so we're going to do sinon.spy and send is also going to be sinon.spy (Typing) and what this is doing is it's creating a spy using the Sinon framework that's going to keep track of what is called, what it's called with, how many times it's called, all of those types of things, so I can actually now check to see if status is called and what it's called with, and so the way we do that down here is we're going to do a res.status.calledWith. We're going to want to call this with a 400, which just means it's a bad request, and the fact that it's called with a 400 this is going to return a true or false whether or not it was called with a 400, so we want it to be, using the should assertion framework we can just say should.equal(true). Now what this also lets us do is include a helpful message, so we could just say Bad Status and then add to that what it was actually called with, so we can actually do a res.status.args, and what args is is an array of each time this function is called, so we only care about the first time, and what the arguments were for each time it was called, so in this case we only care about the first argument. This will give us an error message that says, this was called with a bad status, and here's what the status was that this was called with, and so that'll be really helpful when we're error handling our unit tests. Now we also want to check on send, so we're going to do a res.send.calledWith, and we want this to make sure we're sending back a helpful error, so let's say Title is required should.equal(true). Now we've gotten to the point where we've got our assertions going, we've got our mocks done, but we haven't called anything yet, so let's hook this all up to our bookController, and then we'll run our tests. The way we're going to do that is by creating an instance of the bookController, so we'll do var bookController (Typing) and just pull it in using require, just like we do when we use it in our actual application, so we do ../controllers/bookController (Typing), and we need to pass in that mock book that we created up above. Now in order to call this we just need to do bookController.post and make sure we send it in req and res. Now that's all we need. This is a full test. We're sending a req.body without a title, we're mocking up our book, our Mongoose model book, so that we can execute on that. We've mocked up our status and our send in our response object and we executed the Post method on our bookController. Now in order to run this we need to run it through Mocha and we're going to do that in Gulp using gulpMocha, so let's jump over to our Gulp file and we'll get that setup.
Now in order to get Mocha running in Gulp we first have to pull in gulpMocha, so we're going to do gulpMocha (Typing) = require ('gulp-mocha'), and we installed that back when we did our npm install for should Sinon and gulpMocha. Now once we have a reference to gulpMocha we're going to create a Gulp task called test (Typing). Now we just create our function and, in this case, we're going to do the full Gulp pipeline, so we're going to do gulp.src, and in this case it's going to be all of our test files, so it's going to be tests/*.js. We're going to pull in everything in our tests directory and we're going to run that. Right now we just have one, but that doesn't always mean we'll always have one (Typing). We're going to pipe that into gulpMocha and gulpMocha takes a reporter and, in this case, we're going to use the nyan reporter just because it looks a little bit better. One last thing we're going to have change before we can run our test is to come back over to here to our bookController and see we're doing res.status and we're chaining on our .send. The way we're doing our mocking that's not going to work, so we're going to need to do a res just separate out these two calls, res.status, res.send, and we'll save that. Now in order to run these tests we're going to come over here and we're going to do a gulp test and we're doing test because that's the name of the task that we gave it and we're going to hit Enter and we should get a failed test and you'll get 1 test failed and if you come up here and look it will say AssertionError: Bad Status 201, so this is exactly what we were looking for coming out of our bookController because nothing was called with a 400 error, so let's flip back over to bookController and we'll write some code for this, and then we'll retest it and make sure we've got our test passing. Over in our bookController in our post right here let's do an if(!req.body.title), so if title does not exist in req.body we're going to do a res.status of 400 and a res.send of (Typing) Title is required. Otherwise, we'll do the rest. Now with all of that done, now that we're checking for all of this, I should be able to come out of here, rerun my test, and now you'll see I have a passing test. What we have here in our bookController test is what we call a unit test, so we are just testing the bookController all by itself. We've mocked everything around it and we know that the bookController is executing the way we want it to execute. Next we're going to look at some integration tests, which is more of an end to end test where we are actually going to replicate the HTTP post and all the way down to the database there.
Now we're going to start looking at integration tests or end to end tests for our application. In order to do that we're going to need to install two npm packages. The first one is going to be something called supertest and that's what we're going to use to execute our HTTP calls and the next one's going to be gulp-env, and what that's going to do is let us manipulate the environment variables in Gulp, so that we can setup a test environment, so that we can test everything end to end including the database. --save-dev to make sure that we're saving this in our dev environ.ent, but we're not going to install it when we get up into our production environments. It helps if we use install, so make sure we've got our install in there (Typing), and then that'll go. Alright, now over in our Gulp file we are going to make sure that we have env (Typing) and supertest (Typing). Now down here in our tests we're going to make sure that we set our environmental variables, so we're going to do env with vars (Typing) and we're going to set our environment to Test (Typing), and what that's going to do is when we get into our app.js I can do a process.env and pull in our environment, so it'll either be prod, dev or test and that'll be governed by our Gulp execution. The way we're going to do that is here on app.js instead of just connecting to our environment right here we're going to create this virtual and if(process.env.ENV is equal to test we are going to set db equal to bookAPI_test. Otherwise, (Typing) we're going to set it to bookAPI, so that way when our Gulp test runs it's going to set the env to test and our database is actually going to connect to this new MongoDB instance. Now that doesn't exist yet, but when we run our test Mongoose will see that it doesn't exist, and it'll just create it for us. Now we're going to create a new Javascript file called bookIntegrationTests (Typing) and then we've got to just write this out. We're going to start with should (Typing). Now in this case we're going to need a reference to our app, so we're going to set app equal to ../app.js, and this is what supertest is going to use to execute our HTTP calls (Typing). Here I can just pull in my model Book directly from Mongoose because it's loaded into Mongoose in our app.js, so I don't have to go through the hassle of pulling it in from the model, I can just pull it straight from Mongoose, and we're going to end with something called agent and what agent is is what we'll actually use from supertest to execute all of our HTTP calls (Typing) based on our app (Typing). The rest of this works very similar to the way that unit test works, so we do a describe, and this is going to be our Book Crud Test, so we're testing insert, update, delete, and get, and that's going to take a function (Typing). Alright, and here in the describe is where you layout all of your individual tests, so we're going to do it('Should allow a book to be posted and return a read and _id') because the way that we're going to know whether or not a book has been posted all the way to the database is by whether or not we get a read because that was a default, and an _id because that'll be created when we get to the database, and that's going to take our function (Typing) with done. Now the first thing we're going to want to do is create a book and, in this case, we're just going to do a bookPost with a title and author and a genre and that's going to be our req.body, that's going to be what we're actually posting into our function, and then we're going to use our agent to actually do the work. We're going to start with agent.post and we're going to tell it to post to api/books and when it posts to api/books we're going to send our bookPost and then we're going to expect (Typing) to get a 200 back and when it's all done we're going to have this callback called with either an err or some results and this is where we get to do our assertions, and so the first assertion we should do is results.body.read.should.equal(false) and results.body.should.have.property('_id'). Now when all of our tests are done (Typing) we're going to run a function and that function is basically going to just clear our database back again (Typing), by doing a Book.remove (Typing) and we'll run that. Then we'll call our done just to let everything know that we're all done and we're good. Now what this is going to do is it's going to send a post to api/books, that's going to go all the way through to our test database because in our app.js if our environment is test we're going to do our new API test, and then it's going to expect to have a read property that's false, and it should also now have an id. One other quick thing to remember is on here it needs to be /api/books, that's a super test thing that we just have to make sure we've got right, and also down here at the bottom we're going to want to put done. What done does, just like we have down here, is it lets supertest know, hey this test is done, now move on to the next thing, and so this done gets called, we go down to afterEach, and then that done gets called. Now the last thing we're going to have to do in order to make this work is we're going to have to have to export our app over in our app.js, so down at the bottom we need to do a module.exports = app, and that's what allows supertest to execute on the app. Alright, now that all of that's done we can come over here and we can do a Gulp test and we can run our test and see we have two passing tests, so let's change this so we can see a failing test, so result.body.read should not equal false. We'll run that and we know it will equal false because that becomes the default and you'll see now we have a failed test, expected false not to be false, so we see that this is actually running end to end all the way to the database and all the way back, so this is a good way just to wrap a set of tests around your entire application, so that you know that all of the moving pieces are working together and doing what you expect them to do.
We're still working our way through building a RESTful API with Node and Express and in this module we talked about unit testing with Mocha and we worked through how to separate our code, so that it was mockable, so that we could test just specific pieces without having to test the whole thing, and then we moved onto integration tests and we used supertest to be able to run our tests end to end, so we know the whole system works together the way we expect it to from the HTTP calls all the way back to the database.