├── .gitignore ├── README.md ├── article1.md ├── article2.md ├── failing-test-1.png ├── failing-test-2.png ├── package.json ├── passing-test-1.png ├── src ├── part1 │ ├── cart-summary.js │ └── tax.js └── part2 │ └── tax.js └── tests ├── part1 └── cart-summary-test.js └── part2 └── tax-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-testing 2 | 3 | To run the following tests: 4 | 5 | ``` 6 | npm install 7 | mocha tests --recursive --watch 8 | ``` 9 | -------------------------------------------------------------------------------- /article1.md: -------------------------------------------------------------------------------- 1 | Unit Testing in Node - Part 1 2 | ============================= 3 | 4 | Testing is an important practice in software development to improve software quality. There are many forms of testing; manual testing, acceptance testing, unit testing, and a few others. In this post we are going to look at unit testing in Node using the Mocha test framework. Unit tests typically make up the majority of test suites. They test small units of code, typically a method or a function, __in isolation__. The key thing to remember is the __in isolation__ aspect. 5 | 6 | In this post, we'll start off writing unit tests for a function that simply takes some input, returns some output, and has no dependencies. Then we will look at two types of test doubles, stubs and spies, using the Sinon library. Lastly, we will look at how to test asynchronous code in Mocha. Let's get started! 7 | 8 | ### Installing Mocha and Chai 9 | 10 | To install Mocha, simply run: 11 | 12 | ``` 13 | npm install mocha -g 14 | ``` 15 | 16 | Unlike other JavaScript testing frameworks like Jasmine and QUnit, Mocha does not come with an assertion library. Instead, Mocha allows you to choose your own. Popular assertion libraries used with Mocha include should.js, expect.js, Chai, and Node's built in `assert` module. In this post, we are going to use Chai. 17 | 18 | First, let's create a `package.json` file and install Chai: 19 | 20 | ``` 21 | touch package.json 22 | echo {} > package.json 23 | npm install chai --save-dev 24 | ``` 25 | 26 | Chai comes with three different assertion flavors. It has the `should` style, the `expect` style, and the `assert` style. They all get the job done and choosing one is just a matter of preference in how you want the language of your tests to read. Personally I like the `expect` style so we will be using that. 27 | 28 | ### Your First Test 29 | 30 | For our first example, we will use test driven development (TDD) to create a `CartSummary` constructor function, which will be used to total up items placed in a shopping cart. In short, TDD is the practice of writing tests before an implementation to drive the design of your code. TDD is practiced in the following steps: 31 | 32 | 1. Write a test and watch it fail 33 | 2. Write the minimal amount of code to make that test pass 34 | 3. Repeat 35 | 36 | By following this process, you are guaranteed to have tests for your code because you are writing them first. It is not always possible, or it is sometimes very difficult, to write unit tests after the fact. Anyways, enough about TDD, let's see some code! 37 | 38 | ```js 39 | // tests/part1/cart-summary-test.js 40 | var chai = require('chai'); 41 | var expect = chai.expect; // we are using the "expect" style of Chai 42 | var CartSummary = require('./../../src/part1/cart-summary'); 43 | 44 | describe('CartSummary', function() { 45 | it('getSubtotal() should return 0 if no items are passed in', function() { 46 | var cartSummary = new CartSummary([]); 47 | expect(cartSummary.getSubtotal()).to.equal(0); 48 | }); 49 | }); 50 | ``` 51 | 52 | The `describe` function is used to set up a group of tests with a name. I tend to put the module under test as the name, in this case `CartSummary`. A test is written using the `it` function. The `it` function is given a description as the first argument of what the module under test should do. The second argument of the `it` function is a function that will contain one or more assertions (also called expectations) using Chai in this example. Our first test simply verifies that the subtotal is 0 if the cart has no items. 53 | 54 | To run this test, run `mocha tests --recursive --watch` from the root of the project. The recursive flag will find all files in subdirectories, and the watch flag will watch all your source and test files and rerun the tests when they change. You should see something like this: 55 | 56 | ![failing-test-1.png](failing-test-1.png) 57 | 58 | Our test is failing because we have not yet implemented `CartSummary`. Let's do that. 59 | 60 | ```js 61 | // src/part1/cart-summary.js 62 | function CartSummary() {} 63 | 64 | CartSummary.prototype.getSubtotal = function() { 65 | return 0; 66 | }; 67 | 68 | module.exports = CartSummary; 69 | ``` 70 | 71 | Here we've written the minimal amount of code to make our test pass. 72 | 73 | ![passing test-1](passing-test-1.png) 74 | 75 | 76 | Let's move on to our next test. 77 | 78 | ```js 79 | it('getSubtotal() should return the sum of the price * quantity for all items', function() { 80 | var cartSummary = new CartSummary([{ 81 | id: 1, 82 | quantity: 4, 83 | price: 50 84 | }, { 85 | id: 2, 86 | quantity: 2, 87 | price: 30 88 | }, { 89 | id: 3, 90 | quantity: 1, 91 | price: 40 92 | }]); 93 | 94 | expect(cartSummary.getSubtotal()).to.equal(300); 95 | }); 96 | ``` 97 | 98 | ![failing-test-2.png](failing-test-2.png) 99 | 100 | The failing output shows what value `getSubtotal` returned in red and what value we expected in green. Let's revise `getSubtotal` so our test passes. 101 | 102 | ```js 103 | // src/part1/cart-summary.js 104 | function CartSummary(items) { 105 | this._items = items; 106 | } 107 | 108 | CartSummary.prototype.getSubtotal = function() { 109 | if (this._items.length) { 110 | return this._items.reduce(function(subtotal, item) { 111 | return subtotal += (item.quantity * item.price); 112 | }, 0); 113 | } 114 | 115 | return 0; 116 | }; 117 | ``` 118 | 119 | Our test passes! We have successfully used TDD to implement the `getSubtotal` method. 120 | 121 | ### Stubs with Sinon 122 | 123 | Let's say we now want to add tax calculation to `CartSummary` in a `getTax()` method. The end usage will look like this: 124 | 125 | ```js 126 | var cartSummary = new CartSummary([ /* ... */ ]); 127 | cartSummary.getTax('NY', function() { 128 | // executed when the tax API request has finished 129 | }); 130 | ``` 131 | 132 | The `getTax` method will use another module we will create called `tax` with a `calculate` method that will deal with the intricacies of calculating tax by state. Even though we have not implemented `tax`, we can still finish our `getTax` method as long as we identify a contract for the `tax` module. This contract will state that there should be a module called `tax` with a `calculate` method that takes three arguments: a subtotal, a state, and a callback function that will execute when the tax API request has completed. As mentioned before, unit tests test units in isolation. We want to test our `getTax` method isolated from `tax.calculate`. As long as `tax.calculate` abides by its code contract, or interface, `getTax` should work. What we can do is fake out `tax.calculate` when testing `getTax` using a stub, a type of test double that acts as a controllable replacement. Test doubles are often compared to stunt doubles, as they replace one object with another for testing purposes, similar to how actors and actresses are replaced with stunt doubles for dangerous action scenes. We can create this stub using the Sinon library. 133 | 134 | To install Sinon, run: 135 | 136 | ``` 137 | npm install sinon --save-dev 138 | ``` 139 | 140 | The first thing we have to do before we can stub out the `tax.calculate` method is define it. We don't have to implement the details of it, but the method `calculate` must exist on the `tax` object. 141 | 142 | ```js 143 | // src/part1/tax.js 144 | module.exports = { 145 | calculate: function(subtotal, state, done) { 146 | // implemented later or in parallel by our coworker 147 | } 148 | }; 149 | ``` 150 | 151 | Now that `tax.calculate` has been created, we can stub it out with our pre-programmed replacement using Sinon: 152 | 153 | ```js 154 | // tests/part1/cart-summary-test.js 155 | // ... 156 | var sinon = require('sinon'); 157 | var tax = require('./../../src/part1/tax'); 158 | 159 | describe('getTax()', function() { 160 | beforeEach(function() { 161 | sinon.stub(tax, 'calculate', function(subtotal, state, done) { 162 | setTimeout(function() { 163 | done({ 164 | amount: 30 165 | }); 166 | }, 0); 167 | }); 168 | }); 169 | 170 | afterEach(function() { 171 | tax.calculate.restore(); 172 | }); 173 | 174 | it('get Tax() should execute the callback function with the tax amount', function(done) { 175 | var cartSummary = new CartSummary([{ 176 | id: 1, 177 | quantity: 4, 178 | price: 50 179 | }, { 180 | id: 2, 181 | quantity: 2, 182 | price: 30 183 | }, { 184 | id: 3, 185 | quantity: 1, 186 | price: 40 187 | }]); 188 | 189 | cartSummary.getTax('NY', function(taxAmount) { 190 | expect(taxAmount).to.equal(30); 191 | done(); 192 | }); 193 | }); 194 | }); 195 | ``` 196 | 197 | We start by requiring Sinon and our tax module into the test. To stub out a method in Sinon, we call the `sinon.stub` function and pass it the object with the method being stubbed, the name of the method to be stubbed, and a function that will replace the original during our test. 198 | 199 | ```js 200 | var stub = sinon.stub(object, 'method', func); 201 | ``` 202 | 203 | In this example, I have simply stubbed out `tax.calculate` with the following: 204 | 205 | ```js 206 | function(subtotal, state, done) { 207 | setTimeout(function() { 208 | done({ 209 | amount: 30 210 | }); 211 | }, 0); 212 | } 213 | ``` 214 | 215 | This is just a function that calls `done` with a static tax details object containing a tax amount of 30. `setTimeout` is used to mimic the asynchronous behavior of this method since in reality it will be making an asynchronous API call to some tax service. This happens in a `beforeEach` block which executes before every test. After each test, the `afterEach` block is executed which restores the original `tax.calculate`. 216 | 217 | This test verifies that the callback function passed to `getTax` is executed with the tax amount, not the entire tax details object that gets passed to the callback function for `tax.calculate`. As you can see, our test for `getTax` is passing even though we haven't implemented `tax.calculate` yet. We've merely defined the interface of it. As long as `tax.calculate` upholds to this interface, both modules should work correctly together. 218 | 219 | This example also exhibits asynchronous testing. Specifying a parameter in the `it` function (called `done` in this example), Mocha will pass in a function and wait for it to execute before ending the test. The test will timeout and error if `done` is not invoked within 2000 milliseconds. If we had not made this an asynchronous test, the test would have finished before our expectation has run, leading us to think all of our tests are passing when in reality they are not. 220 | 221 | Now let's write the implementation for `getTax` to make our test pass: 222 | 223 | ```js 224 | CartSummary.prototype.getTax = function(state, done) { 225 | tax.calculate(this.getSubtotal(), state, function(taxInfo) { 226 | done(taxInfo.amount); 227 | }); 228 | }; 229 | ``` 230 | 231 | ### Spies with Sinon 232 | 233 | One issue that our `getTax` method has is that our test does not verify that `tax.calculate` is called with the correct subtotal and state. Our test would still pass if we hardcoded subtotal and state values in the `getTax` implementation. Go ahead and give it a try in the [sample code](https://github.com/skaterdav85/node-testing). That's no good! To verify `tax.calculate` is called with the correct arguments, we can leverage Sinon spies. 234 | 235 | A spy is another type of test double that records how a function is used. This includes information such as what arguments a spy is called with, how many times a spy is called, and if the spy throws an error. The great thing about Sinon stubs is that they are built on top of spies! Here is our updated test: 236 | 237 | ```js 238 | it('getTax() should execute the callback function with the tax amount', function(done) { 239 | var cartSummary = new CartSummary([ 240 | { 241 | id: 1, 242 | quantity: 4, 243 | price: 50 244 | }, 245 | { 246 | id: 2, 247 | quantity: 2, 248 | price: 30 249 | }, 250 | { 251 | id: 3, 252 | quantity: 1, 253 | price: 40 254 | } 255 | ]); 256 | 257 | cartSummary.getTax('NY', function(taxAmount) { 258 | expect(taxAmount).to.equal(30); 259 | expect(tax.calculate.getCall(0).args[0]).to.equal(300); 260 | expect(tax.calculate.getCall(0).args[1]).to.equal('NY'); 261 | done(); 262 | }); 263 | }); 264 | ``` 265 | 266 | Two more expectations were added to this test. `getCall` is used to get the first call to the stub for `tax.calculate`. `args` contains the arguments for that call. We are simply verifying that `tax.calculate` was called with the correct subtotal and state as opposed to hardcoded values. 267 | 268 | Sinon is a very powerful library and offers a lot of test double functionality for both Node and browser JavaScript testing that you will find useful so definitely check out the documentation. 269 | 270 | ### Conclusion 271 | 272 | In this post, we looked at a few practical examples of unit testing in Node using the Mocha testing framework, the Chai assertion library, and Sinon for test doubles in the form of stubbing and spying. I hope you enjoyed this post. If you have any questions, ask them below or reach me on Twitter [@skaterdav85](https://twitter.com/skaterdav85). 273 | 274 | [Source code](https://github.com/skaterdav85/node-testing) 275 | -------------------------------------------------------------------------------- /article2.md: -------------------------------------------------------------------------------- 1 | Unit Testing and TDD in Node - Part 2 2 | ===================================== 3 | 4 | In the [last article](https://www.codementor.io/nodejs/tutorial/unit-testing-nodejs-tdd-mocha-sinon), we looked at how to write unit tests using Mocha and Chai using a test driven development (TDD) approach. We also looked at how and when to use test doubles using the Sinon library. Specifically, we used a type of test double called a stub to act as a controllable replacement for the `tax` module since it had not been implemented yet and `CartSummary` depended on it. In this article, we will look at how to write unit tests for that `tax` module that makes an HTTP request. Let's get started! 5 | 6 | ### Testing HTTP Requests 7 | 8 | So you might be wondering how to write units for functions that make HTTP requests. Aren't unit tests supposed to be isolated? Yes, unit tests are supposed to be isolated. Thanks to a library called [Nock](https://github.com/pgte/nock), we can fake out HTTP requests made from Node and return canned responses. In short, Nock is an HTTP mocking library for Node. This library works by overriding Node's `http.request` function so that HTTP requests are not made. Instead, Nock intercepts your HTTP requests and allows you to provide a custom response. Let's see how we can use this to test `tax.calculate`. 9 | 10 | First, install Nock: 11 | 12 | ``` 13 | npm install nock --save-dev 14 | ``` 15 | 16 | Nock is not a library you would use in production. It is development tool used for testing, so we save it to our development dependencies. Now, let's write our first test. 17 | 18 | ```js 19 | // tests/part2/tax-test.js 20 | var nock = require('nock'); 21 | // ... 22 | it('calculate() should resolve with an object containing the tax details', function(done) { 23 | nock('https://some-tax-service.com') 24 | .post('/request') 25 | .reply(200, { 26 | amount: 7 27 | }); 28 | 29 | tax.calculate(500, 'CA', function(taxDetails) { 30 | expect(taxDetails).to.eql({ amount: 7 }); 31 | done(); 32 | }); 33 | }); 34 | ``` 35 | 36 | When a POST request is made to https://some-tax-service.com/request, Nock will return the following static JSON response: 37 | 38 | ```json 39 | { 40 | "amount": 7 41 | } 42 | ``` 43 | 44 | This static response should mimic what the API would really respond with. Our test is failing because we have no implementation yet. Let's do that: 45 | 46 | Install the `request` module, which is used to make HTTP requests: 47 | 48 | ``` 49 | npm install request --save 50 | ``` 51 | 52 | Then add the following: 53 | 54 | ```js 55 | // src/part2/tax.js 56 | var request = require('request'); 57 | module.exports = { 58 | calculate: function(subtotal, state, done) { 59 | request.post({ 60 | url: 'https://some-tax-service.com/request', 61 | method: 'POST', 62 | json: {} 63 | }, function(error, response, body) { 64 | done(body); 65 | }); 66 | } 67 | }; 68 | ``` 69 | 70 | We've written the minimal amount of code to get our test to pass. One issue with this though is that we don't need to pass the subtotal in the request for our test to pass. Let's see how we can capture that in a test. 71 | 72 | ```js 73 | // tests/part2/tax-test.js 74 | it('calculate() should send the subtotal in the request', function(done) { 75 | nock('https://some-tax-service.com') 76 | .post('/request') 77 | .reply(200, function(uri, requestBody) { 78 | return { 79 | amount: JSON.parse(requestBody).subtotal * 0.10 80 | }; 81 | }); 82 | 83 | tax.calculate(100, 'CA', function(taxDetails) { 84 | expect(taxDetails).to.eql({ amount: 10 }); 85 | done(); 86 | }); 87 | }); 88 | ``` 89 | 90 | Here we have written a test where instead of specifying a static JSON response, we have specified a function to execute that reads the subtotal from the request and calculates a 10% tax. This function is called with an argument `uri` containing the value `/request` and `requestBody`, which is a JSON string containing the request data. We are assuming CA has a 10% tax rate in our test. Now, our test fails until we send over the subtotal. 91 | 92 | ```js 93 | // src/part2/tax.js 94 | var request = require('request'); 95 | 96 | module.exports = { 97 | calculate: function(subtotal, state, done) { 98 | request.post({ 99 | url: 'https://some-tax-service.com/request', 100 | method: 'POST', 101 | json: { 102 | subtotal: subtotal // added the subtotal in the request payload 103 | } 104 | }, function(error, response, body) { 105 | done(body); 106 | }); 107 | } 108 | }; 109 | ``` 110 | 111 | Our test passes! Now your client comes to you and says to only call the tax API if the state is CA. Otherwise, don't charge any tax. Our previous tests already handle the case when the state is CA. Let's write a test to handle when the state is not CA. 112 | 113 | ```js 114 | // tests/part2/tax-test.js 115 | it('calculate() should not make a request if the state is not CA', function(done) { 116 | nock('https://some-tax-service.com') 117 | .post('/request') 118 | .reply(200, function(uri, requestBody) { 119 | return { 120 | amount: JSON.parse(requestBody).subtotal * 0.10 121 | }; 122 | }); 123 | 124 | tax.calculate(100, 'NY', function(taxDetails) { 125 | expect(taxDetails).to.eql({ amount: 0 }); 126 | done(); 127 | }); 128 | }); 129 | ``` 130 | 131 | Our test fails. The implementation for this test to pass becomes: 132 | 133 | ```js 134 | // src/part2/tax.js 135 | module.exports = { 136 | calculate: function(subtotal, state, done) { 137 | if (state !== 'CA') { 138 | done({ amount: 0 }); 139 | } 140 | 141 | request.post({ 142 | url: 'https://some-tax-service.com/request', 143 | method: 'POST', 144 | json: { 145 | subtotal: subtotal 146 | } 147 | }, function(error, response, body) { 148 | done(body); 149 | }); 150 | } 151 | }; 152 | ``` 153 | 154 | ### Conclusion 155 | 156 | In this post, we looked at how to test modules that make HTTP requests in isolation using a library called Nock. Behind the scenes, Nock overrides Node's `http.request` function which is used by the `request` module. I hope you enjoyed this post. If you have any questions, ask them below or reach me on Twitter [@skaterdav85](https://twitter.com/skaterdav85). 157 | 158 | [Source code](https://github.com/skaterdav85/node-testing) 159 | -------------------------------------------------------------------------------- /failing-test-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamdtang/node-testing/b332bd10529123b5e1b67ff8a3f6791db976aab9/failing-test-1.png -------------------------------------------------------------------------------- /failing-test-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamdtang/node-testing/b332bd10529123b5e1b67ff8a3f6791db976aab9/failing-test-2.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "request": "^2.65.0" 4 | }, 5 | "devDependencies": { 6 | "chai": "^3.4.0", 7 | "nock": "^2.17.0", 8 | "sinon": "^1.17.2" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /passing-test-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamdtang/node-testing/b332bd10529123b5e1b67ff8a3f6791db976aab9/passing-test-1.png -------------------------------------------------------------------------------- /src/part1/cart-summary.js: -------------------------------------------------------------------------------- 1 | var tax = require('./tax'); 2 | 3 | function CartSummary(items) { 4 | this._items = items; 5 | } 6 | 7 | CartSummary.prototype.getSubtotal = function() { 8 | if (this._items.length) { 9 | return this._items.reduce(function(subtotal, item) { 10 | return subtotal += (item.quantity * item.price); 11 | }, 0); 12 | } 13 | 14 | return 0; 15 | }; 16 | 17 | CartSummary.prototype.getTax = function(state, done) { 18 | tax.calculate(this.getSubtotal(), state, function(taxInfo) { 19 | done(taxInfo.amount); 20 | }); 21 | }; 22 | 23 | module.exports = CartSummary; 24 | -------------------------------------------------------------------------------- /src/part1/tax.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | calculate: function() { 3 | 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /src/part2/tax.js: -------------------------------------------------------------------------------- 1 | var request = require('request'); 2 | 3 | module.exports = { 4 | calculate: function(subtotal, state, done) { 5 | if (state !== 'CA') { 6 | return done({ amount: 0 }); 7 | } 8 | 9 | request.post({ 10 | url: 'https://some-tax-service.com/request', 11 | method: 'POST', 12 | json: { 13 | subtotal: subtotal 14 | } 15 | }, function(error, response, body) { 16 | done(body); 17 | }); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /tests/part1/cart-summary-test.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var sinon = require('sinon'); 3 | var expect = chai.expect; 4 | var CartSummary = require('./../../src/part1/cart-summary'); 5 | var tax = require('./../../src/part1/tax'); 6 | 7 | describe('CartSummary', function() { 8 | beforeEach(function() { 9 | sinon.stub(tax, 'calculate', function(subtotal, state, done) { 10 | setTimeout(function() { 11 | done({ 12 | amount: 30 13 | }); 14 | }, 0); 15 | }); 16 | }); 17 | 18 | afterEach(function() { 19 | tax.calculate.restore(); 20 | }); 21 | 22 | it('getSubtotal() should return 0 if no items are passed in', function() { 23 | var cartSummary = new CartSummary([]); 24 | expect(cartSummary.getSubtotal()).to.equal(0); 25 | }); 26 | 27 | it('getSubtotal() should return the sum of the price * quantity for all items', function() { 28 | var cartSummary = new CartSummary([ 29 | { 30 | id: 1, 31 | quantity: 4, 32 | price: 50 33 | }, 34 | { 35 | id: 2, 36 | quantity: 2, 37 | price: 30 38 | }, 39 | { 40 | id: 3, 41 | quantity: 1, 42 | price: 40 43 | } 44 | ]); 45 | expect(cartSummary.getSubtotal()).to.equal(300); 46 | }); 47 | 48 | it('getTax() should execute the callback function with the tax amount', function(done) { 49 | var cartSummary = new CartSummary([ 50 | { 51 | id: 1, 52 | quantity: 4, 53 | price: 50 54 | }, 55 | { 56 | id: 2, 57 | quantity: 2, 58 | price: 30 59 | }, 60 | { 61 | id: 3, 62 | quantity: 1, 63 | price: 40 64 | } 65 | ]); 66 | 67 | cartSummary.getTax('NY', function(taxAmount) { 68 | expect(taxAmount).to.equal(30); 69 | expect(tax.calculate.getCall(0).args[0]).to.equal(300); 70 | expect(tax.calculate.getCall(0).args[1]).to.equal('NY'); 71 | done(); 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /tests/part2/tax-test.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | var nock = require('nock'); 3 | var tax = require('./../../src/part2/tax'); 4 | var expect = chai.expect; 5 | 6 | describe('tax', function() { 7 | it('calculate() should resolve with an object containing the tax details', function(done) { 8 | nock('https://some-tax-service.com') 9 | .post('/request') 10 | .reply(200, { 11 | amount: 7 12 | }); 13 | 14 | tax.calculate(500, 'CA', function(taxDetails) { 15 | expect(taxDetails).to.eql({ amount: 7 }); 16 | done(); 17 | }); 18 | }); 19 | 20 | it('calculate() should send the subtotal in the request', function(done) { 21 | nock('https://some-tax-service.com') 22 | .post('/request') 23 | .reply(200, function(uri, requestBody) { 24 | return { 25 | amount: JSON.parse(requestBody).subtotal * 0.10 26 | }; 27 | }); 28 | 29 | tax.calculate(100, 'CA', function(taxDetails) { 30 | expect(taxDetails).to.eql({ amount: 10 }); 31 | done(); 32 | }); 33 | }); 34 | 35 | it('calculate() should not make a request if the state is not CA', function(done) { 36 | nock('https://some-tax-service.com') 37 | .post('/request') 38 | .reply(200, function(uri, requestBody) { 39 | return { 40 | amount: JSON.parse(requestBody).subtotal * 0.10 41 | }; 42 | }); 43 | 44 | tax.calculate(100, 'NY', function(taxDetails) { 45 | expect(taxDetails).to.eql({ amount: 0 }); 46 | done(); 47 | }); 48 | }); 49 | 50 | }); 51 | --------------------------------------------------------------------------------