Home > Software engineering >  Mocking request & result to have smallest unit-test possible
Mocking request & result to have smallest unit-test possible

Time:12-15

i'm building a back-for-front applications with Express. It is exclusively called from the front via routes, then calls an external API to give back the result. Here is a sample code of the logic :

dashboard.route.ts

const router = Router();
const dashboardController = new DashboardController();

router.get("/distantCall", dashboardController.getDistantCall);

dashboard.controller.ts

import { Request, Response, NextFunction } from "express";
import DashboardService from "../services/dashboard.service";

export class DashboardController {
    async getDistantCall(req: Request, res: Response, next: NextFunction) {
        DashboardService.getDistantCalls()
            .then((result: any) => {
                res.status(200).send(result);
            }).catch((error: any) => {
                next(error);
            });
    }
}

dashboard.service.ts

import { DashboardApi } from './dashboard.api';

class DashboardService {
    public async getDistantCall() {
        return new Promise((resolve, reject) => {
            new DashboardApi().getDistantCall()
                .then((response: any) => {
                    resolve({
                        distantResponse: response.body
                    });
                })
                .catch((error) => {
                    reject(error);
                });
        });
    }

The DashboardAPI class makes an external http call and returns a promise. For this sample, it returns a simple text "distantSuccess"

For my tests, I can quite easily write integration tests

dashboard.routes.spec.ts

import chai from "chai";
import chaiHttp from "chai-http";
import { expect } from "chai";
chai.use(chaiHttp);

import createServer from "../../src/server";
const app = createServer();

describe("dashboard routes", function() {    
    it('nominal distant call', async () => {
        const res = await chai.request(app).get("/dashboard/distantCall");
        expect(res.status).to.eq(200);
        expect(res.body).to.be.a('object');
        expect(res.body).to.have.property('distantResponse');
        expect(res.body.distantResponse).to.eq('distantSuccess');
    });
});

My problem is building unit tests. As I understand it, I should only test the controller or the service, and using mocks & stubs to simulate the elements outside of the scope. Here are the two tests I made :

dashboard.controller.spec.ts

import { Request, Response, NextFunction } from "express";
import chai from "chai";
import chaiHttp from "chai-http";
import { expect } from "chai";
import sinon from "sinon";
chai.use(chaiHttp);

import createServer from "../../src/server";
const app = createServer();
import { DashboardController } from "../../src/controllers/dashboard.controller";
const dashboardController = new DashboardController();
import DashboardService from "../../src/services/dashboard.service";

describe("dashboard routes with fake objects", function () {
    it("distant call by controller", async () => {
        const mockRequest: any = {
            headers: {},
            body: {},
        };
        const mockResponse: any = {
            body: { distantResponse: "About..." },
            text: "test",
            status: 200,
        };
        const mockNext: NextFunction = () => {};

        await dashboardController.getDistantCallSucces(mockRequest, mockResponse, mockNext);

        expect(mockResponse.status).to.eq(200);
        expect(mockResponse.body).to.be.a("object");
        expect(mockResponse.body).to.have.property("distantResponse");
        expect(mockResponse.body.distantResponse).to.eq("About...");
    });
});

describe("dashboard routes with stubs", function () {
    before(() => {
        sinon
            .stub(DashboardService, "getDistantCall")
            .yields({ distantResponse: "distantSuccess" });
        });

    it("distant call by controller", async () => {
        const mockRequest: any = {};
        const mockResponse: any = {};
        const mockNext: NextFunction = () => {};

        const res = await dashboardController.getDistantCall(mockRequest, mockResponse, mockNext);
        console.log(res);
    });
});

For the first test, I clearly don't understant the use of it. i'm testing an object I just created, without even knowing if the service is called. I feel I shoud do something more like the second test, but I get this error : TypeError: getDistantCall expected to yield, but no callback was passed.

CodePudding user response:

I finaly found the solution.

I created two separated files : one for integration testing, one for unit-testing. I tweaked a bit the response from the distant server, which is now 'from distant server' instead of the "distantResponse" set in the previous message. On the controller & service, I also changed getDistantCall to two different functions getDistantCallSucces and getDistantCallError to force a resolve and a reject for integration tests.

dashboard-integration.spec.ts

import chai, { expect } from "chai";
import chaiHttp from "chai-http";
chai.use(chaiHttp);
import sinon from "sinon";

import app from "../src/app";
import { DashboardAPI } from '../src/dashboard/dashboard.api';

describe("Dashboard intagration tests", () => {    
    describe("Integration with server online", () => {
        it('Dashboard routes up', async () => {
            const res = await chai.request(app).get("/dashboard");
            expect(res.status).to.eq(200);
            expect(res.text).to.eq('dashboard');
        });
        it('full process with expected success', async () => {
            const res = await chai.request(app).get("/dashboard/distantCallSuccess").set("code", "12345");
            expect(res.status).to.eq(200);
            expect(res.body).to.be.a('object');
            expect(res.body).to.have.property('distantResponse');
            expect(res.body.distantResponse).to.eq('from distant server');
        });
        it('full process with expected error', async () => {
            const res = await chai.request(app).get("/dashboard/distantCallError");
            expect(res.status).to.eq(500);
        });
    });

    describe("Integration with mocked server", () => {
        beforeEach(() => {
            sinon.restore();
        });
        it('full process with expected resolve', async () => {
            const mockedResponse = {body: 'mocked'};
            sinon.stub(DashboardAPI.prototype, 'getDistantCallSuccess').resolves(mockedResponse);
            const res = await chai.request(app).get("/dashboard/distantCallSuccess").set("code", "12345");
            expect(res.status).to.eq(200);
            expect(res.body).to.be.a('object');
            expect(res.body).to.have.property('distantResponse');
            expect(res.body.distantResponse).to.eq('mocked');
        });
        it('full process with expected reject', async () => {
            sinon.stub(DashboardAPI.prototype, 'getDistantCallSuccess').rejects({mockedError: true});
            const res = await chai.request(app).get("/dashboard/distantCallSuccess").set("code", "12345");
            expect(res.status).to.eq(500);
        });
    });
});

dashboard-unit.spec.ts

I had to use node-mocks-http to simulate the request & response objects

import chai, { expect } from "chai";
import chaiHttp from "chai-http";
import sinon from "sinon";
import httpMocks from "node-mocks-http";
chai.use(chaiHttp);

import { DashboardController } from "../src/dashboard/dashboard.controller";
import DashboardService from "../src/dashboard/dashboard.service";
import { DashboardAPI } from '../src/dashboard/dashboard.api';

describe("Unit Testing the Dashboard process", () => {
    describe("Unit Testing the controller", () => {
        const dashboardController = new DashboardController();
        beforeEach(() => {
            sinon.restore();
        });
        it("testing controller call without headers [catch]", async () => {
            var request = httpMocks.createRequest({});
            var response = httpMocks.createResponse();
            const next = () => {};
        
            await dashboardController.getDistantCallSuccess(request, response, next);

            expect(response._getStatusCode()).to.eq(500);
            expect(response._getData()).to.eq("Missing HEADER Parameter");
        });
        it("testing controller call with headers [resolve]", async () => {
            const mockedResponse = {
                mockedResponse: true
            };
            sinon.stub(DashboardService, 'getDistantCallSuccess').resolves(mockedResponse);
            var request = httpMocks.createRequest({
                headers: { code: "123" }
            });
            var response = httpMocks.createResponse();
            const next = () => {};
        
            await dashboardController.getDistantCallSuccess(request, response, next);
            expect(response._getStatusCode()).to.eq(200);
            expect(response._getData()).to.eql({mockedResponse: true});
        });
        it("testing controller call with headers [reject]", async () => {
            sinon.stub(DashboardService, 'getDistantCallSuccess').rejects({customError: true});
            
            const request = httpMocks.createRequest({});
            const response = httpMocks.createResponse();
            const next = (res) => {
                expect(res).to.eql({customError: true});
            };
            await dashboardController.getDistantCallError(request, response, next);
        });
    });
    
    describe("Unit Testing the service", () => {
        beforeEach(() => {
            sinon.restore();
        });
        it("testing service call with resolve", async() => {
            const mockedResponse = {
                body: 'mocked'
            };
            sinon.stub(DashboardAPI.prototype, 'getDistantCallSuccess').resolves(mockedResponse);
            
            let result;
            await DashboardService.getDistantCallSuccess().then(res => {
                result = res;
            });
            expect(result).to.be.a('object');
            expect(result).to.be.haveOwnProperty('distantResponse');
            expect(result.distantResponse).to.eq('mocked');
        });
        it("testing service call with reject", async() => {
            sinon.stub(DashboardAPI.prototype, 'getDistantCallSuccess').rejects({mockedError: true});
            
            let result;
            await DashboardService.getDistantCallSuccess()
                .then(res => {
                    result = res;
                })
                .catch(err => {
                    result = err;
                });
            expect(result).to.eql({mockedError: true});
        });
    });
});
  • Related