I'm trying to create a unit test for the following service, using Sinon.
as you can see the "_createRedisConnection" is called on the constructor, so in the unit test I must mock the Redis connection.
import { inject, injectable } from "inversify";
import { TYPES } from "../../inversify/types";
import { Logger } from "winston";
import { Config } from "../../interfaces/config.interface";
import { BaseService } from "../base.service";
import * as Redis from "ioredis";
import { HttpResponseError } from "../../interfaces/HttpResponseError.interface";
import { BaseResponse } from "../../interfaces/BaseResponse.interface";
#injectable()
export class RedisService extends BaseService {
private _redisClient;
private _isRedisConnected: boolean;
constructor(#inject(TYPES.Logger) private logger: Logger,
#inject(TYPES.Config) private config: Config) {
super(logger, config);
this._isRedisConnected = false;
this._createRedisConnection();
}
public async set(key, value, epu, receivedTtl): Promise<BaseResponse> {
if (this._isRedisConnected) {
const encryptedKey = this.createEncryptedKey(epu, key);
if (!encryptedKey || !value) {
throw new HttpResponseError("General error", "Missing attributes in request body", 422);
}
const ttl = this.limitTtl(receivedTtl);
let response;
if (ttl >= 0) {
await this._redisClient.setex(encryptedKey, ttl, value)
.then(() => {
response = new BaseResponse("success", "Data saved successfully", ttl);
})
.catch((errorMessage: string) => {
throw new HttpResponseError("General error", `Error while saving data. err = ${errorMessage}`, 500);
});
} else {
await this._redisClient.set(encryptedKey, value)
.then(() => {
response = new BaseResponse("success", "Data saved successfully", ttl);
})
.catch((errorMessage: string) => {
throw new HttpResponseError("General error", `Error while saving data. err = ${errorMessage}`, 500);
});
}
return response;
}
throw new HttpResponseError("General error", "Cache is not responding", 503);
}
private _createRedisConnection(): void {
this._redisClient = new Redis({
sentinels: [{ host: this.config.redisConfig.host, port: this.config.redisConfig.port }],
name: "mymaster",
dropBufferSupport: true,
});
this._redisClient.on("connect", () => {
this._isRedisConnected = true;
});
this._redisClient.on("error", (errorMessage: string) => {
this._isRedisConnected = false;
});
}
}
My problem is with mocking the Redis connection. I'm trying stub the 'connect' event, but while debugging it I see that the event never triggered (even not the error event).
import "reflect-metadata";
import { expect } from "chai";
import { Logger } from "winston";
import * as Redis from "ioredis";
import { stub } from "sinon";
import { RedisService } from "./redis.service";
import { config } from "../../config";
class LoggerMock {
public info(str: string) { }
public error(str: string) { }
}
describe("RedisService Service", () => {
const redisStub = stub(Redis.prototype, "connect").returns(Promise.resolve());
const logger = new LoggerMock() as Logger;
const redisService = new RedisService(logger, config);
it("Should success set data", async () => {
const redisClientStub = stub(Redis.prototype, "set").resolves(new Promise((resolve, reject) => { resolve('OK'); }));
const result = await redisService.set("key", "value", "epu", -1);
expect(result.message).to.equals("success");
expect(result.response).to.equals("Data saved successfully");
redisClientStub.restore();
redisStub.restore();
});
});
What is the right way to test this service? why no event is triggered when stubbing this way?
Thanks
This is an example to how to stub ioredis Redis.prototype.connect.
// File test.js
const { expect } = require('chai');
const Redis = require('ioredis');
const sinon = require('sinon');
describe('connection', function () {
it('should emit "connect" when connected', function (done) {
// Create stub on connect.
const stubRedisConnect = sinon.stub(Redis.prototype, 'connect');
stubRedisConnect.callsFake(async function () {
// This will trigger connect event.
this.setStatus('connect');
});
const redis = new Redis();
redis.on('connect', function () {
// Do not forget to restore the stub.
stubRedisConnect.restore();
done();
});
});
});
When I run it on my terminal:
$ npx mocha test.js
connection
✓ should emit "connect" when connected
1 passing (6ms)
If the test stub failed, there will be default timeout error for 2000ms because done not get called.
Related
I am using the official mongodb driver package to develop some interfaces for operating the database, but I found that even if I use try and catch, I still cannot catch some errors. I use the windows operating system for development. If I want to connect to my database, I have to start the mongdb service first, but when I do not start the service and then try to use the official mongodb driver package to connect to the database, this npm package will not throw any error, this is just one of the cases where no error is thrown. Does this mongodb npm package provide any other way for users to catch errors?
This is the npm package I use: mongodb
This is my code:
db_config.js
const defaultUrl = 'mongodb://localhost:27017'
const defaultName = 'tumbleweed'
class DataBaseConfig {
#dbUrl
#dbName
constructor() {
this.#dbUrl = this.dbConnectUrl
this.#dbName = this.dbConnectName
}
get dbConnectUrl() {
return this.#dbUrl === undefined ? defaultUrl : this.#dbUrl
}
set dbConnectUrl(value) {
this.#dbUrl = value === undefined ? defaultUrl : value
}
get dbConnectName() {
return this.#dbName === undefined ? defaultName : this.#dbName
}
set dbConnectName(value) {
this.#dbName = value === undefined ? defaultName : value
}
}
const DataBaseShareConfig = new DataBaseConfig()
export default DataBaseShareConfig
db.js
import { MongoClient } from "mongodb";
import DataBaseShareConfig from "./db_config.js";
class DataBase {
#db
constructor() {
this.#db = null
}
async #connect() {
return new Promise(async (resolve, reject)=> {
try {
console.log(`begain to connecting: ${DataBaseShareConfig.dbConnectUrl}`)
const client = await MongoClient.connect(DataBaseShareConfig.dbConnectUrl)
this.#db = client.db(DataBaseShareConfig.dbConnectName)
console.log(`db: ${DataBaseShareConfig.dbConnectName} connected succeed`)
resolve(this.#db)
} catch (error) {
reject(error)
}
})
}
async find(collectionName, json) {
console.log("begain to find...")
return new Promise(async (resolve, reject)=> {
try {
if(!this.#db) {
await this.#connect()
const collection = this.#db.collection(collectionName)
const result = await collection.find(json).toArray()
resolve(result)
} else {
const collection = this.#db.collection(collectionName)
const result = await collection.find(json).toArray()
resolve(result)
}
} catch (error) {
reject(error)
}
})
}
}
const DataBaseShareInstance = new DataBase()
export default DataBaseShareInstance
main.js
import DataBaseShareInstance from "./db/db.js"
import DataBaseShareConfig from "./db/db_config.js"
DataBaseShareConfig.dbConnectUrl = 'mongodb://localhost:27017'
DataBaseShareConfig.dbConnectName = 'tumbleweed'
const main = (function () {
DataBaseShareInstance.find("users", {name: 'fq'}).then(result => {
console.log(result)
}).catch(error => {
console.log(error)
})
})()
I trying to create a common database connection class using typescript for my nodejs express application that returns the MongoDB database object as follows but I always get TypeError: dbConn.GetInstance is not a function
const config = require("config");
const MongoClient = require('mongodb').MongoClient;
export class dbConn {
private db = null;
static url = config.database.uri;
static options = {
bufferMaxEntries: 0,
reconnectTries: 5000,
useNewUrlParser: true,
useUnifiedTopology: true,
};
private constructor() { }
private static instance: dbConn;
public static GetInstance() {//I also tried removing static keyword but still the error remains
if (dbConn.instance == null)
{
dbConn.instance = new dbConn();
}
return dbConn.instance;
}
public getDb() {
if (dbConn.instance.db) {
return dbConn.instance.db;
}
MongoClient.connect(dbConn.url, dbConn.options, function(err: any, db: any){
if(err) {
console.log(err);
return null;
}
dbConn.instance.db = db.db(config.database.name);
return dbConn.instance.db;
});
}
}
Updated 01-Aug-2020
I invoke the above instance from app.ts and my controllers as follows:
app.ts file
const dbConn = require('./utils/db/dbConn');
...//code removed for clarity
const app = express();
const server = http.createServer(app);
...//code removed for clarity
server.listen(port, ()=> {
dbConn.GetInstance().getDb();//I get the error here
console.log('Server running')
});
module.exports = app;
my controller file
getAll = async (pageNumber:any, pageSize:any) : Promise<PageResult<Team>> => {
return new Promise<PageResult<Team>>(async function (resolve, reject){
let result = new PageResult<Team>(pageSize, pageNumber);
var dbo = dbConn.GetInstance().getDb();//same error here too.
var query = {};
var recCount = await dbo.collection("teams").find().count();
if (recCount == 0) {
result.IsSuccessful = true;
result.ReasonForFailure = process.env.NO_RECORDS || "No record(s) to show.";
return resolve(result);
}
if (pageSize == -1) { //-1 means to return all records
dbo.collection("teams")
.find(query)
.sort({ name: 1 })
.toArray(function(err: any, resultSet: any) {
if (err) {
result.IsSuccessful = false;
result.ReasonForFailure = err.message;
return reject(result);
} else {
result.IsSuccessful = true;
result.TotalRecords = recCount;
result.PageNumber = parseInt(pageNumber);
result.PageSize = parseInt(pageSize);
result.Data = resultSet;
return resolve(result);
}
});
} else {
dbo.collection("teams")
.find(query)
.sort({ name: 1 })
.skip((parseInt(pageNumber)-1)*parseInt(pageSize))
.limit(parseInt(pageSize)).toArray(function(err: any, resultSet: any) {
if (err) {
result.IsSuccessful = false;
result.ReasonForFailure = err.message;
return reject(result);
} else {
result.IsSuccessful = true;
result.TotalRecords = recCount;
result.PageNumber = parseInt(pageNumber);
result.PageSize = parseInt(pageSize);
result.Data = resultSet;
return resolve(result);
}
});
}
});
}
Can you please assist what is wrong or what is the missing piece to get this to work?
Thanks,
Hemant.
It seems you're using commonjs as module-resolution strategy. Your import will be the problem in that case. Try changing it to:
const dbConn = require('./utils/db/dbConn').dbConn;
or
const { dbConn } = require('./utils/db/dbConn');
or
import {dbConn } from './utils/db/dbConn';
Here's a simple example to show what's going on. Consider this simple ts-class:
export class TestClass {
static test():void {
console.log("it works")
}
}
It will be transpiled into:
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TestClass = void 0;
class TestClass {
static test() {
console.log("in workds");
}
}
exports.TestClass = TestClass;
//# sourceMappingURL=index.js.map
If you then require this with const TestClassModule = require('./test-class');, TestClassModule will yield:
{ TestClass: [Function: TestClass] }
Hence, you need to use const { TestClass } = require('./test-class');.
service.js
Class ClassWithConstructor {
constructor(arg1, arg2) {
super();
this.param1 = arg1;
this.param2 = arg2;
}
getInfo() {
//returns promise
}
}
controller.js
export function getData(req, res) {
const svc = new ClassWithConstructor.ClassWithConstructor(req.user.param1, req.user.param2);
svc.getInfo()
.then(result => {
return respondWithSearchResult(res, 200, result, 'success');
})
.catch(err => {
//return error
});
}
For unit test getData() method I need to mock the line const svc = new ClassWithConstructor.ClassWithConstructor(req.user.param1, req.user.param2);
I am using Sinon for mocking. I have been trying for a long. Can someone help me out?
mocha + chai + ts-mockito is executing unit-test of the node.js application.
In order to test the Service class, we mock the method of the Repository class which is called in the Service class.
Running will not work as expected.
I expect ErrorDto, but the string "TypeError: Can not read property 'then' of null" will be returned.
Please tell me something is wrong.
Test code.
let mockedRepository: MyRepository = tsmockito.mock(MyRepository);
describe("MyService Test", () => {
it("getAll() test", async () => {
const errorDto = new ErrorDto();
errorDto.addMessage("test message");
tsmockito.when(mockedRepository.getAll()).thenCall(() => {
return new Promise((resolve, reject) => {
reject(errorDto);
});
});
let mockedRepositoryInstance: MyRepository = tsmockito.instance(mockedRepository);
Container.set(MyRepository, mockedRepositoryInstance);
let service = new MyService();
let res = await service.getAll();
// I expect ErrorDto for res,
// but "TypeError: Can not read property 'then' of null" is returned.
if (res instanceof ErrorDto) {
// Do not reach below
let msg = res.getMessages();
expect(msg[0]).to.eql("test message");
} else {
// Test failure
expect(true).to.false;
}
});
});
Service class to be tested
#Service()
export default class MyService {
#Log()
public getAll() {
const repository = Container.get(MyRepository);
return new Promise<MyServiceTreeDto[]>((resolve, reject) => {
repository.getAll()
.then((res: MyTreeDomain[]) => {
const resDto = new Array<MyServiceTreeDto>();
//do something...
resolve(resDto);
}).catch((err) => {
reject(err);
});
});
}
}
Repository class to be mocked
#Service()
export default class MyRepository {
public async getAll(domain: MyDomain): Promise<MyTreeDomain[]> {
try {
let res: MyTreeDomain[] = new Array<MyTreeDomain>();
//do async process
return res;
} catch (err) {
if (err instanceof ErrorDto) {
throw err;
} else {
const errorDto = new ErrorDto();
errorDto.addMessage("error!");
throw errorDto;
}
}
}
}
I am trying to get few values from redis, combine them and eventually send. But I just can't make those promises work.
This is the simple get functions from redis
client.get('user:1:id',function(err,data){
// here I have data which contains user ID
});
client.get('user:1:username',function(err,data){
// here I have data which contains username
});
Now I want to get ID and username and send them, but I have no idea how to make that work. I manage to make it work with callbacks but it is very messy result, so then i tried to wrap anonymous functions into Q.fcall and after call .then which looks something like that
client.get('user:1:id',Q.fcall(function(err,data){
return data;
}).then(function(val) {
// do something
}));
but that gives me error for too many arguments been passed and I'm not even sure if that would help me even if it would work.
Q.all([Q.ninvoke(client, 'get', 'user:1:id'),
Q.ninvoke(client, 'get', 'user:1:username')]).then(function (data) {
var id = data[0];
var username = data[1];
// do something with them
});
See https://github.com/kriskowal/q#adapting-node
I use a simple RequireJS module using node-redis and whenjs to create a lifted redis wrapper:
define [
'redis/lib/commands'
'when'
'when/node/function'
], (Commands, When, NodeFn) ->
'use strict'
lift = (redis) ->
wrapped = {}
Commands.map (cmd) ->
wrapped[cmd] = (args...) ->
def = When.defer()
args.push NodeFn.createCallback def.resolver
redis[cmd].apply redis, args
def.promise
wrapped
{lift}
Usage is straightforward:
client = lift redis.createClient()
client.get("hello").then console.log, console.error
Using Promise, Bluebird and node_redis:
import { RedisClient, createClient, ClientOpts } from "redis";
import { promisifyAll, PromisifyAllOptions } from "bluebird";
export module FMC_Redis {
export class Redis {
opt: ClientOpts;
private rc: RedisClient;
private rcPromise: any;
private static _instance: Redis = null;
public static current(_opt?: ClientOpts): Redis {
if (!Redis._instance) {
Redis._instance = new Redis(_opt);
Redis._instance.redisConnect();
}
return Redis._instance;
}
public get client(): RedisClient {
if (!this.rc.connected) throw new Error("There is no connection to Redis DB!");
return this.rc;
}
/******* BLUEBIRD ********/
public get clientAsync(): any {
// promisifyAll functions of redisClient
// creating new redis client object which contains xxxAsync(..) functions.
return this.rcPromise = promisifyAll(this.client);
}
private constructor(_opt?: ClientOpts) {
if (Redis._instance) return;
this.opt = _opt
? _opt
: {
host: "127.0.0.1",
port: 6379,
db: "0"
};
}
public redisConnect(): void {
this.rc = createClient(this.opt);
this.rc
.on("ready", this.onReady)
.on("end", this.onEnd)
.on("error", this.onError);
}
private onReady(): void { console.log("Redis connection was successfully established." + arguments); }
private onEnd(): void { console.warn("Redis connection was closed."); }
private onError(err: any): void { console.error("There is an error: " + err); }
/****** PROMISE *********/
// promise redis test
public getRegularPromise() {
let rc = this.client;
return new Promise(function (res, rej) {
console.warn("> getKeyPromise() ::");
rc.get("cem", function (err, val) {
console.log("DB Response OK.");
// if DB generated error:
if (err) rej(err);
// DB generated result:
else res(val);
});
});
}
/******* ASYNC - AWAIT *******/
// async - await test function
public delay(ms) {
return new Promise<string>((fnResolve, fnReject) => {
setTimeout(fnResolve("> delay(" + ms + ") > successfull result"), ms);
});
}
public async delayTest() {
console.log("\n****** delayTest ")
let a = this.delay(500).then(a => console.log("\t" + a));
let b = await this.delay(400);
console.log("\tb::: " + b);
}
// async - await function
public async getKey(key: string) {
let reply = await this.clientAsync.getAsync("cem");
return reply.toString();
}
}
}
let a = FMC_Redis.Redis.current();
// setTimeout(function () {
// console.warn(a.client.set("cem", "naber"));
// console.warn(a.client.get("cem"));
// console.warn(a.client.keys("cem"));
// }, 1000);
/***** async await test client *****/
a.delayTest();
/** Standart Redis Client test client */
setTimeout(function () {
a.client.get("cem", function (err, val) {
console.log("\n****** Standart Redis Client")
if (err) console.error("\tError: " + err);
else console.log("\tValue ::" + val);
});
}, 100)
/***** Using regular Promise with Redis Client > test client *****/
setTimeout(function () {
a.getRegularPromise().then(function (v) {
console.log("\n***** Regular Promise with Redis Client")
console.log("\t> Then ::" + v);
}).catch(function (e) {
console.error("\t> Catch ::" + e);
});
}, 100);
/***** Using bluebird promisify with Redis Client > test client *****/
setTimeout(function () {
var header = "\n***** bluebird promisify with Redis Client";
a.clientAsync.getAsync("cem").then(result => console.log(header + result)).catch(console.error);
}, 100);