I'm working on a library that throws errors using event emitter. I subscribe to receive the "error" event from my library and, when it happens, I want to make my tests fail. If no error is emitted, then I expect that the test is passed.
// Imports and Globals
import {IListener} from "typescript.events";
import { expect } from 'chai';
import 'mocha';
import * as assert from "assert"
describe('Public functions', () => {
describe('getMessage(error : string) : any', () => {
let responseGiver;
beforeEach(function() {
responseGiver = require("../index");
responseGiver.setLoggerLevel("none");
});
// 1
it('should throw when the dictionary is not set', async () => {
const handler : IListener = async function(e){
await responseGiver.removeListener("error", handler);
// This test should fail here
};
await responseGiver.on('error', handler);
await responseGiver.getMessage("A");
});
// 2
it('should throw when the dictionary is set, but there is not the key we are looking for', async () => {
const handler : IListener = async function(e){
await responseGiver.removeListener("error", handler);
// This test should fail here
};
await responseGiver.on('error', handler);
await responseGiver.setDictionary( { "A" : "B" } )
await responseGiver.getMessage("C");
});
});
});
Is the code to be tested actually using both promises and an error-handling event? Either the one or the other would be more straightforward to test on its own:
it("tests a promise-returning library", async function() {
const x = await getSomePromise()
return x.someAdditionalOperation()
// If x became rejected at any point, a rejected promise will be returned and Mocha treats that as failure.
})
it("tests an event-emitting library", function(done) {
const x = getSomeEventEmitter()
x.on("error", done) // Will pass the error to Mocha's `done`, which fails the test; if this emitter is going to hang around and needs you to clean up the event handler you can `x.on("error", function(error) { x.removeListener(done); done(error) })`
x.on(<whatever is success>, function(successValue) {
assert.stuff.about(successValue)
done()
})
})
It's the combination of two different asynchronous paradigms that makes things difficult. Mocha specifically disallows returning a promise (as async functions do behind the scenes) and using both the done callback in the same test, because we haven't figured out how to guarantee correct behavior. But you might be able to adapt the event emitter into a promise interface:
it("is really hacky, but should work", async function() {
const x = getSomeEventEmitter()
const potentialFailure = new Promise(function(resolve, reject) {
x.on("error", reject)
})
await x.doWhatever()
return potentialFailure
})
@ScottFreeCode , first of all, thanks for the brillian answer. It was super useful.
I would like to precise that my functions don't emit events when they succeed. I don't know if it's a good practise, but this shouldn't impede my tests. In the worst case, it times out.
I tried all your approaches, but I "fail" every time I try to call _removeListener()_. In fact, if I get "Error: done() called multiple times"
This is the result of my logs:
// Test initialization
AAAAA
BBBBB
// Test 1
1
CCCCC
2
// Test initialization
AAAAA
BBBBB
// Test 2
3
4
DDDDD
CCCCC
5
This is my code:
describe('Public functions', () => {
describe('getMessage(error : string) : any', () => {
let responseGiver;
beforeEach(function() {
console.log("AAAAA");
responseGiver = require("../index");
responseGiver.setLoggerLevel("none");
console.log("BBBBB");
});
// 1
it('should throw when the dictionary is not set', (done) => {
const handler : IListener = function(error) {
console.log("CCCCC")
responseGiver.removeListener(handler);
done()
}
responseGiver.on("error", handler)
console.log(1);
responseGiver.getMessage("A");
console.log(2);
});
// 2
it('should throw when the dictionary is set, but there is not the key we are looking for', (done) => {
const handler = function(error) {
console.log("DDDDD")
responseGiver.removeListener(handler);
done()
}
responseGiver.on("error", handler)
console.log(3);
responseGiver.setDictionary({"key" : "value"})
console.log(4);
responseGiver.getMessage("inexistantKey");
console.log(5);
});
});
});
As you can see, when I execute _responseGiver.setDictionary({"key" : "value"})_, I shouldn't print both "CCCCC" and "DDDDD", but only "DDDDD".
I mean, I could remove all listeners all at once, but I think that I shouldn't expose a function like that.
Thanks again for the support.
Hmm. The test structure looks right, but removeListener isn't working? Maybe it needs to be removeListener("error", handler) instead of just removeListener(handler)? I sometimes forget which way that is supposed to work.
@ScottFreeCode , you're right. My bad. But it still doesn't work. I made some change to my code to understand where's the error. I also listen to node "removeListener" which, according to the docs:
The 'removeListener' event is emitted after the listener is removed.
But you know what? This event is never emitted. But in the end the function is called, since I log after I call _removeListener()_.
These are my tests:
describe('Public functions', () => {
describe('getMessage(error : string) : any', () => {
let responseGiver;
beforeEach(function() {
console.log("AAAAA");
responseGiver = require("../index");
responseGiver.setLoggerLevel("none");
responseGiver.on("removeListener", function() {
console.log("ONE EVENT REMOVED")
});
console.log("BBBBB");
});
// 1
it('should throw when the dictionary is not set', (done) => {
const handler : IListener = function(error) {
console.log("CCCCC")
responseGiver.removeListener("error", handler);
done()
}
responseGiver.on("error", handler)
console.log(1);
responseGiver.getMessage("A");
console.log(2);
});
// 2
it('should throw when the dictionary is set, but there is not the key we are looking for', (done) => {
const handler = function(error) {
console.log("DDDDD")
responseGiver.removeListener("error", handler);
done()
}
responseGiver.on("error", handler)
console.log(3);
responseGiver.setDictionary({"key" : "value"})
console.log(4);
responseGiver.getMessage("inexistantKey");
console.log(5);
});
});
});
It seems that _handler_ cannot "remove" itself.
I did another test: instead of using _emitter.removeListener(eventName, listener)_, I used _emitter.removeAllListeners([eventName])_, but It didn't work. It seems to be a problem with Typescript/Javascript. Can someone confirm me to be a bug?
I partially solved my problem. In a nutshell:
1) I couldn't explain why I wasn't able to not remove the listener. Maybe it could be because we try to detach _handler_ inside _handler_ itself. With a "middleware" function didn't work as well. However, I found out that excluding this specific use-case, it's impossible in general to remove a listener for some reason. This could be a bug on node.
2) But in the end, I adjusted my tests to make them work. Basically I asked myself: "if I require the module before each test, why are old events emitted anyway?". I figured out that, even if we do responseGiver = null; responseGiver = responseGiver = require("../index");, node cached the old module, so that if we tried to require two times the same module, we'll get an old module.
For those who could have a similar problem, here there is my working source code:
describe('Public functions', () => {
describe('getMessage(error : string) : any', () => {
let responseGiver;
beforeEach(function() {
// Require a fresh version of the module
delete require.cache[require.resolve('../index')]
responseGiver = require("../index");
// Do not log
responseGiver.setLoggerLevel("none");
});
// 1
it('should throw when the dictionary is not set', (done) => {
// If an error is thrown, then the test passes
const handler : IListener = function(error) {
done()
}
responseGiver.on("error", handler)
// Cause a throw
responseGiver.getMessage("A");
});
// 2
it('should throw when the dictionary is set, but there is not the key we are looking for', (done) => {
// If an error is thrown, then the test passes
const handler = function(error) {
done()
}
responseGiver.on("error", handler)
// Cause a throw
responseGiver.setDictionary({"key" : "value"})
responseGiver.getMessage("inexistantKey");
});
});
});
Is it possible this particular EventEmitter is subclassed, monkey-patched or otherwise changed from the default EventEmitter behavior?
I almost mentioned this earlier -- in general, unless you really want one instance of a thing to exist in your entire Node process, you shouldn't make that thing your module export; instead, if you typically want to be able to create multiple instances of it or otherwise avoid sharing state between multiple other modules that import it, you'd want to export a constructor or factory that returns a new instance. So, for example, instead of something like this (dumb names in this example, just trying to show the difference between exporting an instance and exporting the non-state-retaining means of creating an instance):
// index.js
module.exports = new MyEventEmitterVariationOrUsage()
// other files
var emitter = require("./index.js")
...you'd do something like this:
// index.js
module.exports = MyEventEmitterVariationOrUsage
// or
module.exports = function() { return new MyEventEmitterVariationOrUsage() }
// other files
var makeEmitter = require("./index.js")
var emitter = makeEmitter()
I'm going to close this for now since you isolated the issue to module system usage patterns and the behavior of the remove listener methods (i.e. not Mocha issues), but feel free to continue discussion here; if we do find any problem in this situation that's directly Mocha-related we can always reopen!
If someone is still looking for a way to do that, I'm using:
it("test case", done => {
(async () => {
const successListener = msg => {
done(); // wil pass
};
const errorListener = error => {
done(error); // will fail
};
responseGiver.on("success", successListener);
responseGiver.on("error", errorListener);
try {
responseGiver.willThrow();
}
catch(error) {
done(error); // will fail
}
})();
});
Most helpful comment
If someone is still looking for a way to do that, I'm using: