Extend subclass with callback function to superclass - javascript

I have two classes:
Socket baseclass - Which handles basic operations for a socket connection
Socket subclass - Which should apply specific operations i.e. when a message is received
In my application, I have multiple socket connections that I may open - Hence why I don't want to repeat all the logic for opening and closing my socket connection.
Similarly, when I receive a message on one of the connections I may handle the messages differently, depending on what connection I have opened.
Therefore, I came up with the following solution:
class SocketBase {
constructor(messageHandler, errorHandler) {
this.messageHandler = messageHandler
this.errorHandler = errorHandler
}
openSocket = async () => {
//Logic for opening up socket
//Subscribe to socket events
.subscribe(
(x) => this.messageHandler(x),
(err) => this.errorHandler(err),
() => console.log("Observer got a complete notification"),
)
}
}
As you can see, I want to pass two callback functions to my above super-class - Respectively one for handling incoming messages, and another for handling eventual error events.
My sub-class looks as follows:
class Sub extends SocketBase {
constructor(link) {
super(this.messageHandler, this.errorHandler)
}
errorHandler = (err) => {
//some logic
}
messageHandler = (message) => {
//some logic
}
}
Since the incoming messages are events, I thought passing a callback function to the super-class made sense. However, I realize that my sonarlint is complaining.
i.e.
'this' is not allowed before 'super()'.sonarlint(javascript:S3854)
It is however possible for me to define the callback function in my constructor arguments as follows:
super(function(message){console.log(message)}, function(error){console.log(error)})
Ofcause this is not what I want, for obvious reasons the above solution may get very messy.
Another solution would be for me to construct my base-class, but that would give me some undesired issues as well.. For example, I would have to do as follows:
this.socketBase = new SocketBase(this.messageHandler, this.errorHandler)
//Open socket (Not pretty)
openSocket = () => {
this.socketBase.openSocket()
}
Conclusively, I want to understand how I should approach this problem?

With your code example, there is no need to pass the functions, at all:
class SocketBase {
openSocket = async () => {
//Logic for opening up socket
//Subscribe to socket events
.subscribe(
(x) => this.messageHandler(x),
(err) => this.errorHandler(err),
() => console.log("Observer got a complete notification"),
)
}
}
class Sub extends SocketBase {
constructor(link) {
super();
// do whatever you need to do with `link` parameter
}
errorHandler = (err) => {
//some logic
}
messageHandler = (message) => {
//some logic
}
}
If for whatever strange reason you need the passing to the super class, here you go.
You can define constants and pass those to super, and by calling super, you already assign those to your instance this.
class Sub extends SocketBase {
constructor(link) {
const errorHandler = (err) => {
// errorHandler Code here
}
const messageHandler = (msg) => {
// messageHandler Code here
}
super(messageHandler, errorHandler)
}
}
You can also work with closures here, but in that case remember to use regular functions and bind them in the superclass:
function errorHandler(err) {
// errorHandler Code here
}
function messageHandler(msg) {
// messageHandler Code here
}
class Sub extends SocketBase {
constructor(link) {
super(messageHandler, errorHandler);
}
}
class SocketBase {
constructor(messageHandler, errorHandler) {
this.messageHandler = messageHandler.bind(this);
this.errorHandler = errorHandler.bind(this);
}
}

Related

How do I test a class's method that has arguments using sinon.js

Hi I am using sequelize ORM with Postgres database for this node.js express.js app. As for testing I am using mocha, chai and sinon.
I am trying to complete a test for a class's method. The class instant i call it userService and the method is findOneUser .. This method has got an argument id .. So in this moment I want to test for a throw error the test works if I dont put an argument. That means this test is obviously not complete.
Here is the class method I want to test
userService.js
module.exports = class UserService {
async findOneUser(id) {
try {
const user = await User.findOne({ where: { id: id } }); // if null is returned error is thrown
if (!user) {
throw createError(404, "User not found");
}
return user;
} catch (err) {
throw err
}
}
}
And here is my test code
userService.spec.js
describe.only("findOne() throws error", () => {
let userResult;
const error = customError(404, "User not found"); // customError is a function where I am throwing both status code and message
before("before hook last it block issue withArgs" , async () => {
// mockModels I have previously mocked all the models
mockModels.User.findOne.withArgs({ where: { id: fakeId } }).resolves(null); // basically have this called and invoked from calling the method that it is inside of based on the argument fakeId
userResult = sinon.stub(userService, "findOneUser").throws(error); // 🤔this is the class instances method I wonder how to test it withArguments anything I try not working but SEE BELOW COMMENTS🤔
});
after(() => {
sinon.reset()
});
it("userService.findOneUser throws error works but without setting arguments 🤔", () => {
expect(userResult).to.throw(error);
});
/// this one below still not working
it("call User.findOne() with incorrect parameter,, STILL PROBLEM 🤯", () => {
expect(mockModels.User.findOne).to.have.been.calledWith({ where: { id: fakeId } });
})
});
But for the method of my class findOneUser has an argument (id) how can I pass that argument into it where I am stubbing it?
Or even any ideas on how to fake call the class method?? I want both it blocks to work
EDIT
I forgot to mention I have stubbed the mockModels.User already and that was done before the describe block
const UserModel = {
findByPk: sinon.stub(),
findOne: sinon.stub(),
findAll: sinon.stub(),
create: sinon.stub(),
destroy: sinon.stub()
}
const mockModels = makeMockModels( { UserModel } );
// delete I am only renaming UserModel to User to type things quicker and easier
delete Object.assign(mockModels, {['User']: mockModels['UserModel'] })['UserModel']
const UserService = proxyquire(servicePath, {
"../models": mockModels
});
const userService = new UserService();
const fakeUser = { update: sinon.stub() }
SOLUTION
I think this might be the solution to my problem strange but it works the tests is working with this
describe.only("findOne() throws error", async () => {
const errors = customError(404, "User not found"); // correct throw
const errors1 = customError(404, "User not foundss"); // on purpose to see if the test fails if should.throw(errors1) is placed instead of should.throw(errors)
after(() => {
sinon.reset()
});
// made it work
it("call User.findOne() with incorrect parameter, and throws an error, works some how! 🤯", async () => {
userResult = sinon.spy(userService.findOneUser);
try {
mockModels.User.findOne.withArgs({ where: { id: fakeId } }).threw(errors);
await userResult(fakeId);
} catch(e) {
// pass without having catch the test fails 😵‍💫
} finally {
expect(mockModels.User.findOne).to.have.been.calledWith({ where: { id: fakeId } });
}
});
it("throws error user does not exist,,, WORKS", () => {
expect(mockModels.User.findOne.withArgs(fakeId).throws(errors)).to.throw
mockModels.User.findOne.withArgs(fakeId).should.throw(errors); // specially this part without having the catch test fails. but now test works even tested with errors1 variable
expect(userResult).to.throw;
});
});
MORE CLEANER SOLUTION BELOW
I like this solution more as I call the method inside within the describe block, then I do the test of the two it blocks.
The problem was I should not have stubbed the class method but should have called it directly, or used sinon.spy on the method and call it through the spy. As for checking that the errors are as expected then running this line of expect(mockModels.User.findOne.withArgs(fakeId).throws(errors)).to.throw(errors); was the solution I needed.
describe.only('findOne() user does not exists, most cleanest throw solution', () => {
after(() => {
sinon.reset()
});
const errors = customError(404, "User not found");
mockModels.User.findOne.withArgs({ where: { id: fakeId } }).threw(errors);
userResult = sinon.spy(userService, "findOneUser"); // either invoke the through sinon.spy or invoke the method directly doesnt really matter
userResult(fakeId);
// userResult = userService.findOneUser(fakeId); // invoke the method directly or invoke through sinon.spy from above
it('call User.findOne() with invalid parameter is called', () => {
expect(mockModels.User.findOne).to.have.been.calledWith({ where: { id: fakeId } });
})
it('test to throw the error', () => {
expect(mockModels.User.findOne.withArgs(fakeId).throws(errors)).to.throw(errors);
expect(userResult).to.throw;
})
});

Try/catch async function outside of async context

I have a few classes which use 'dns' from node.js. But when an error occurs, my app is thrown. I made a siimple example with classes and throwable functions and I faced with the same problem. It's works if an exception is thrown from function but it doesn't work if an exception is thorwn from class.
Example:
class Test {
constructor() {
this.t();
}
async t() {
throw new Error("From class");
}
}
async function test(){
new Test();
}
try {
test().catch(e => {
console.log("From async catch");
});
} catch (e) {
console.log("From try catch");
}
Output:
Uncaught (in promise) Error: From class
at Test.t (<anonymous>:6:11)
at new Test (<anonymous>:3:10)
at test (<anonymous>:11:3)
at <anonymous>:15:3
How to catch errors from try/catch block in this example?
UPD:
Full code (typescript):
export class RedisService {
client: any;
expirationTime: any;
constructor(args: RedisServiceOptions) {
let redisData: any = {};
if (args.password)
redisData["defaults"] = { password: args.password };
dns.resolveSrv(args.host, (err, addresses) => {
if (err) {
/// Handling error in main func
}
else {
log.info("Using Redis cluster mode");
redisData["rootNodes"] = addresses.map(address => {
log.info(`Adding Redis cluster node: ${address.name}:${address.port}`);
return Object({ url: `redis://${address.name}:${address.port}` })
});
this.client = createCluster(redisData);
};
this.client.on('error', (err: Error) => log.error(`Redis error: ${err.message}`));
this.client.connect().then(() => { log.info("Connected to Redis") });
});
this.expirationTime = args.expirationTime;
}
/// Class functions
}
it doesn't work if an exception is thrown from class.
In particular, when an asynchronous error event occurs in the constructor, yes. Like your question title says, you can't handle errors outside of an async context, and a constructor is not that.
Your current implementation has many issues, from client being undefined until it is initialised to not being able to notify your caller about errors.
All this can be solved by not putting asynchronous initialisation code inside a constructor. Create the instance only once you have all the parts, use an async helper factory function to get (and wait for) the parts.
export class RedisService {
client: RedisClient;
expirationTime: number | null;
constructor(client: RedisClient, expirationTime: number | null) {
this.client = client;
this.expirationTime = expirationTime;
}
static create(args: RedisServiceOptions) {
const addresses = await dns.promises.resolveSrv(args.host);
log.info("Using Redis cluster mode");
const redisData = {
defaults: args.password ? { password: args.password } : undefined,
rootNodes: addresses.map(address => {
log.info(`Adding Redis cluster node: ${address.name}:${address.port}`);
return { url: `redis://${address.name}:${address.port}` };
}),
};
const client = createCluster(redisData);
client.on('error', (err: Error) => log.error(`Redis error: ${err.message}`));
await this.client.connect();
log.info("Connected to Redis");
return new RedisService(client, args.expirationTime);
}
… // instance methods
}
Now in your main function, you can call create, use await, and handle errors from it:
async function main(){
try {
const service = await RedisService.create(…);
} catch(e) {
console.log("From async catch", e);
}
}
You generate multiple async-requests but you can only catch errors from the first one:
You create a promise with async function test().
Then you create a syncronous call within it, with new Test(), every error syncronously thrown from within it will be catched by the catch.
Then you generate another promise call from within the syncronous constructor, this error can't be caught by the try/catch-block or .catch at, or above the async function test().
It is similar to this:
constructor() {
new Promise(() => throw new Error(''))
}
So you have 3 possible ways to solve it:
You catch the error inside the async t() {}-call.
You catch it inside the constructor with this.t().catch(console.error) (which can't be forwarded to the try/catch block) as it is forwarded to the catch block of the Promise behind the async call. And if there is no .catch on the async call, you get the "Unhandled Promise rejection"-Error.
Don't call the async function from within the constructor at all, use it like this:
class Test {
async t() {
throw new Error("From class");
}
}
async function test(){
await (new Test()).t();
}
try {
test().catch(e => {
console.log("From async catch");
});
} catch (e) {
console.log("From try catch");
}
Don't make async methods)))
Your solution looks somewhat like this.
class Test {
constructor() {
this.t();
}
t() {
(async () => {
try {
throw new Error("From class");
} catch (e) {
console.log(e);
}
})();
}
}
Have a nice rest of your day

Socket.io multiple handlers for `on` listeners, like in Express

Express lets the developer chain multiple functions as handlers to a single route. From docs:
More than one callback function can handle a route (make sure you
specify the next object). For example:
app.get('/example/b', function (req, res, next) {
console.log('the response will be sent by the next function ...')
next()
}, function (req, res) {
res.send('Hello from B!')
})
This is great if the developer wants to make validations before proceeding to the final function. That's why middlewares are a thing.
Socket.io, on the other hand, only accepts a single handler.
From #types/socket.io:
on( event: string, listener: Function ): Namespace;
It means I can't have middlewares that are event-specific. I know about io.use for global middlewares, and there's an option to have a middleware per namespace too, but all I want is per-event.
My workaround variations
Option 1: try and catch in every event handler.
try {
validateCurrentPlayer(socket);
} catch (e) {
return handleFailedValidation(socket, e);
}
// ... rest of the code
Pro: readable. Con: super repetitive. It means that every relevant entry point with start with the same 5 lines of code that does exactly the same thing every time.
And if the "middleware" returns values, this is how it looks:
let foo: Something;
try {
[foo] = validateCurrentPlayer(socket);
} catch (e) {
return handleFailedValidation(socket, e);
}
// ... rest of the code, use foo
Option 2: Common validation with conditional return
const validation = validate(socket, () => validateCurrentPlayer(socket));
if (validation.error) {
return;
}
const [foo] = validation.result;
This is validate:
export function validate<T extends (...args: any) => any>(socket: Socket, func: T): {
error: boolean;
result: ReturnType<T>;
} {
let result: ReturnType<T> = null;
let error = false;
try {
result = func();
} catch (error) {
handleFailedValidation(socket, error);
}
return {
result,
error,
};
}
As you can see it just wraps the try and catch.
Pro: non-repetitive. Con: Still X lines of code to be copy-pasted into a few handlers.
I don't like these workarounds.
I'm desperately trying to find something similar to the approach of Express, So I could just conditionally call next() if the validation succeeded.
Do you know of any way to do it?
Thanks!

Correct way to unit-test observable stream being fed

I use this to test that the service adds an item to observable stream of errors.
it('can be subscribed for errors', () => {
let testError = new Error('Some error.');
let called = false;
let subscription = service.onError.subscribe(error => {
called = true;
expect(error).toEqual(testError);
});
/// This makes an error to be added to onError stream
service.setError(testError);
expect(called).toEqual(true);
});
I use the called variable to make sure that the subscription callback was actually called. Otherwise the test would pass when it shouldn't. But it doesn't seem right to me. Also, it wouldn't work if the stream was asynchronous.
Is this a good way to test that? If not, how to do it properly?
EDIT: this is the class that's being tested. It's in typescript, actually.
import { ReplaySubject } from 'rxjs/Rx';
export class ErrorService {
private error: Error;
public onError: ReplaySubject<Error> = new ReplaySubject<Error>();
constructor() {
}
public setError = (error: Error) => {
this.error = error;
console.error(error);
this.onError.next(error);
}
public getError() {
return this.error;
}
public hasError() {
return !!this.error;
}
}
The way you are testing is good. You are:
Checking if the value is correct with expect statement.
Checking the fact the expect statement is being executed.
Especially the last part is important otherwise the expect might not be triggered and the test will falsely pass.

Can't set class member variable from callback

I'm trying to set a class member variable from a callback of a function I'm calling from the class constructor.
To be a bit more specific: I need to set the connection ID in the Connection class constructor based on the Redis INCR result (each client has a 'global' connection ID so I can have multiple nodes).
Here's the code.
class Connection {
constructor() {
client.incr('conn_id', (err, reply) => {
this.connID = reply;
});
}
}
var lovely = new Connection();
console.log(`lovely connID is ${ lovely.connID }`);
This is the result: lovely connID is undefined
It seems that client.incr('conn_id' ....) is async , which means the callback will be invoked after your code run .
So
console.log(lovely connID is ${ lovely.connID }); will be called before the callback
(err, reply) => {
self.connID = reply;
}
which is similar to this :
class Connection{
constructor(){
self=this;
setTimeout( function(){self.client='somevalue';
console.log('value1');}, 10)
}
}
var a = new Connection();
console.log(a.client);
running this will result
undefined
value1
As others here have mentioned, the issue seems to be that client.incr is asynchronous and your code does not wait for it to resolve before accessing the properties. To remedy this issue, you could try passing in an onReady callback to Connection to enssure the properties will be there before trying to access them. Something along these lines:
'use strict';
// mock client.incr
var client = { incr: (id, callback) => {
setTimeout(() => callback(null, 'Hello World'), 0)
}};
class Connection {
// receive an "onReady" function
constructor(onReady) {
client.incr('conn_id', (err, reply) => {
this.connID = reply;
// call "onReady" function, and pass it the class
if (typeof onReady === 'function') onReady(this)
});
}
}
new Connection(lovely => { console.log(lovely.connID) })
I hope that helps!
In general putting heavy initialization logic in a constructor, especially if it's asynchronous, is not a good idea. As you've found, the constructor has no way to return the information about when the initialization is finished. An alternative is to create a promise for when the connection is ready. Then, in your outside code, you can hang a then off the property to specify the code you want to trun when it's ready.
class Connection {
constructor() {
this.connId = new Promise((resolve, reject) =>
client.incr('conn_id', (err, reply) => {
if (err) return reject(err);
resolve(reply);
});
}
}
var lovely = new Connection();
lovely.connId . then(connId => console.log(`lovely connID is ${ connID }`);

Categories

Resources