Mocking our services

It's not at all uncommon to have parts of your service that are harder to test. Some, or most, of those parts are error-related conditions, where it's hard to make an external service such as a database engine return an error that will rarely occur during normal execution.

To be able to test, or at least simulate these kinds of events, we need to mock our services. There are a couple of options around, and Sinon is the most commonly used one in the Node.js ecosystem. This framework provides more than mocking; it also provides the following:

  • Spies: Which monitor function calls and record arguments passed, the returned value and other properties
  • Stubs: Which are enhanced spies with a pre-programmed behavior, helping us drive the execution into a pre-determined path (allowing us to mock a behavior)

Sinon also allows us to bend time, by virtually changing the service perception of time, and to be able to test timed interval calls (remember our interval timer?). With this in mind, let's see if we can make our microservice reach 100% test coverage.

Let's start by installing the framework, as we did with chai:

npm install --save-dev sinon

Now, let's add a test for the image deletion. This method is tested through the other tests and that's why we didn't need to add it before, but now that we want to fully test it, let's add a basic test file called image-delete.js, with the following content:

const chai = require("chai");
const sinon = require("sinon");
const http = require("chai-http");
const tools = require("../tools");

chai.use(http);

describe.only("Deleting image", () => {
beforeEach((done) => {
chai
.request(tools.service)
.delete("/uploads/test_image_delete.png")
.end(() => {
return done();
});
});

it("should return 200 if it exists", (done) => {
chai
.request(tools.service)
.post("/uploads/test_image_delete.png")
.set("Content-Type", "image/png")
.send(tools.sample)
.end((err, res) => {
chai.expect(res).to.have.status(200);
chai.expect(res.body).to.have.status("ok");

chai
.request(tools.service)
.delete("/uploads/test_image_delete.png")
.end((err, res) => {
chai.expect(res).to.have.status(200);

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

Notice that I added the Sinon dependency on top, although I'm not using it just yet. You may run the tests again, but you shouldn't notice any difference.

We'll need to change the database behavior, so let's export a reference to it, so as to be able to access it from the tests. Add the following line in our microservice file before connecting to the database:

app.db = db;

Now, add another test to that file:

it("should return 500 if a database error happens", (done) => {
chai
.request(tools.service)
.post("/uploads/test_image_delete.png")
.set("Content-Type", "image/png")
.send(tools.sample)
.end((err, res) => {
chai.expect(res).to.have.status(200);
chai.expect(res.body).to.have.status("ok");

let query = sinon.stub(tools.service.db, "query");

query
.withArgs("DELETE FROM images WHERE id = ?")
.callsArgWithAsync(2, new Error("Fake"));

query
.callThrough();

chai
.request(tools.service)
.delete("/uploads/test_image_delete.png")
.end((err, res) => {
chai.expect(res).to.have.status(500);

query.restore();

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

What we're doing is uploading an image, but, before requesting to delete it, we create a stub on the db.query method. We then inform Sinon that when the stub is called with the first argument with DELETE, we want it to asynchronously call the third argument (counting starts at 0) with a fake error. For any other call, we want it to just pass through.

Then, after deleting the image, we check that we received an HTTP 500 error code and restore the stub to the original function, ensuring that the other tests pass.

We're able to test this because mocha runs tests in serial; otherwise, we would need to do some gymnastics to ensure that we wouldn't interfere with the other tests.

Now, open the previously created test file, image-stats.js, include Sinon on the top, and add the following test:

it("should return 500 if a database error happens", (done) => {
let query = sinon.stub(tools.service.db, "query");

query
.withArgs("SELECT COUNT(*) total, SUM(size) size, MAX(date_used)
last_used FROM images")
.callsArgWithAsync(1, new Error("Fake"));

query
.callThrough();

chai
.request(tools.service)
.get("/stats")
.end((err, res) => {
chai.expect(res).to.have.status(500);

query.restore();

return done();
});
});

We're now over 97% coverage. Let's bend time and test our timer. Create a new test file called image-delete-old.js, and add the following content:

const chai = require("chai");
const sinon = require("sinon");
const http = require("chai-http");
const tools = require("../tools");

chai.use(http);

describe("Deleting older images", () => {
let clock = sinon.useFakeTimers({ shouldAdvanceTime : true });

it("should run every hour", (done) => {
chai
.request(tools.service)
.get("/stats")
.end((err, res) => {
chai.expect(res).to.have.status(200);

clock.tick(3600 * 1000);
clock.restore();

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

In this test, we're replacing the global timer functions (setTimeoutand setInterval) with fake timers. We then make a simple call to statistics, and then advance time by one hour (the tick call), and then finish.

Now, run the tests and see the results:

We now reach 100% coverage on functions and lines. There's only one branch, with one statement missing. It's the possibility of a connection error:

I'll leave it to you to figure it out how to mock that.

Remember that if you successfully mock the connect method, you'll also need to handle the throw.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset