Passing context to Sinon stub's fake function - javascript

I'm using the mochawesome test reporter for my mocha tests. I'd like it to record my logging as well, and attach it to whatever test was running when the log was written. This can be achieved with addContext(). However, I also want the logs to output to the console, so I can monitor them as the test is running, rather than wait til it all finishes and the report is generated.
I've got 98% of a solution, based on this answer, but am struggling based on the fact that mochawesome's addContext() requires you to pass it a test object. Here's what I've got:
beforeEach(`Spy on logger`, function() {
const origLogInfo = logger.info.bind(logger),
origLogError = logger.error.bind(logger),
testContext = this
sinon.stub(logger, 'info').callsFake(function(message) {
addContext(testContext, `INFO: ${message}`)
origLogInfo.call(testContext, message)
})
sinon.stub(logger, 'error').callsFake(function(message) {
addContext(testContext, `ERROR: ${message}`)
origLogError.call(testContext, message)
})
})
afterEach(`Remove stubs`, function() {
logger.info.restore()
logger.error.restore()
})
it('counts approved hours', async function() {
logger.info(`Approving timesheets...`)
...
So before each it(), I get a reference to the original logger.info() and logger.error() functions, then I stub them. The stub calls a function which calls mochawesome's addContext() function, passing it a reference to the beforeEach() and whatever string I've sent to logger.info(). Then the original is called.
My problem is that every time addContext() is called, it attaches the logs to the beforeEach() hook. I can see why it's happening, too. Stepping through the code reveals that when beforeEach() is executing, this has two properties: currentTest, and test. The former refers to the it() which is about to be called, and the latter is the beforeEach() hook itself. This is great! If it stayed like this, addContext() would pick up currentTest and attach the logs to it (link to source).
By the time the argument to callsFake() is called, however, that same object has lost its currentTest property, so addContext() instead attaches the logs to the beforeEach() hook itself. Resulting in a test report with all the logs attached to the beforeEach hook instead of the relevant tests.
Is there any way I can write this so the callsFake() argument has a reference to the test that logger.info was called from? The following works, but requires extra code inside each it():
boundLoggerInfo = logger.info.bind(this);
boundLoggerInfo(`Approving timesheets...`)
Cheers! Hopefully I've provided enough info without being too verbose...

Update: I managed to work around the issue and achieve what I was after, after a good night's sleep:
testContext = {
currentTest: this.currentTest
}
Now a reference to currentTest is maintained even after beforeEach() loses it (still not sure why that happens).

Related

How to apply expect over softAssertAll or show explicity in the test result that the softAssertAll passed (Error: Expected Object to be a function)

I'm using 'soft-assert' library (soft-assert library) to apply assertion on my test steps without stopping the test if any of them fail.
According to the documentation, all soft-assert is verified at the end of the test by using softAssertAll() command. And this works very well. However, if nothing fails, I don't see any explicit message in my test result as when I use expect command.
So, I'm trying to apply expect over the softAssertAll() command, as it's seen below, but I'm getting the error message:
"expected { Object (userInvocationStack, specWindow, ...) } to be a function"
What I'm trying to do:
expect(cy.softAssertAll()).not.throw(Error)
Does anyone know how can I do this or solve the error in the image below?
Thanks in advance.
See the Chai example for throw
var badFn = function () { throw new TypeError('Illegal salmon!'); };
expect(badFn).to.throw();
Note you pass in the function name without invoking it. I think this allows chai to wrap the function invocation in a try-catch and gracefully report the failure.
You can't do the same with a Cypress custom command as it will not raise errors in the same way as badFn above. Internally it swallows any error and sets the state of the test to "failed".
You could reasonably expect this to work
expect(jsonAssertion.softAssertAll).not.throw()
however there's an internal error in jsonAssertion that seems to be related to the this reference inside it's class.
TypeError: Cannot read properties of undefined (reading '_softThrowJsonDiffArray')
To fix, use an arrow function
const testSoftAssertAll = () => jsonAssertion.softAssertAll();
expect(testSoftAssertAll).not.throw()
or shorter
expect(() => jsonAssertion.softAssertAll()).not.throw()
Check the diff array
This is cleaner and clearer
// expect all soft-assert to be passing
expect(jsonAssertion.jsonDiffArray).to.eq(undefined)
// expect first soft-assert to fail with a certain message
expect(jsonAssertion.jsonDiffArray[0].error.message).to.contain('Illegal salmon')

How is the value of 'this' different in concrete implementation than on playground?

I am developing an app using a Node-Express stack using Socket.io and I found something weird. I have the following in one of my files:
const server = require('./server')
const io = require('socket.io').listen(server)
const Game = require('./service/game')
const game = new Game()
io.on('connection', (socket) => {
...
game.addPlayer(socket)
socket.on('increaseTime', game.increaseTime) // I know this is wrong
})
I have read about how you have to bind this if you want to use a callback as a handler, so in this specific case I know that on the commented line one of the solutions is the following to actually bind 'this' to the game instance, instead of the socket:
socket.on('increaseTime', game.increaseTime.bind(game))
What I do not understand is not this issue, but related to this. If I leave the line as is, so in the 'wrong' version I would still like to know how is the value of 'this' the socket. That is not what I would expect, because if I try to simulate this in a playground file, the value of this would be the the global object:
const socket = {
on(label, callback) {
callback()
},
}
const game = {
increaseTime() {
console.log(this)
}
}
socket.on('increaseTime', game.increaseTime) // global object
My guess is that the reason that it is the global object is that the value of this is lost, because when we use the 'this' keyword in a function inside of another function, it loses it's value and falls back to the global object (https://spin.atomicobject.com/2014/10/20/javascript-scope-closures/). My main question is how is it possible that the value of 'this' is the socket if I leave the 'wrong' implementation, how is it not the same as in the playground file?
I also tried instantiating dummy classes to have something resembling the actual implementation, but then the value of 'this' would be undefined, which I do not understand either (maybe it could be that the class keyword uses strict mode implicitely, so the fallback is not the global object, I don't know).
Any help would be greatly appreciated!
The value of this depends on how the function is called.
game.increaseTime.bind(game) creates a function which calls increaseTime with game as the this value.
callback() calls the function passed to the callback argument (and copied from game) without any explicit context (so this is the global object).
The code underlying socket.on clearly calls the function passed to it with the socket as the this value. There are several ways it could do that, you'd need to look at its source code to determine which one it uses.

Make a scope inside a function global

Im tryin to access my $rootScope.newBloodneeded but I cant access it outside the function, I tried rootscope so I can call it as global but still it gives me undefined
.controller('editbloodrequestCtrl', function($scope,Bloodrequest,$rootScope,$routeParams) {
$rootScope.newBloodneeded;
Bloodrequest.getBloodrequest($routeParams.id).then(function(data) {
if (data.data.success) {
$scope.newBloodneeded = data.data.bloodrequest.blood_component;
$rootScope.newBloodneeded = $scope.newBloodneeded;
//gives me output when I console here
} else {
app.errorMsg = data.data.message; // Set error message
}
});
console.log($rootScope.newBloodneeded); //gives me undefined
}
Assuming $rootScope is working correctly, this is a problem of asynchronicity NOT scope—when you try to run:
console.log($rootScope.newBloodneeded); //gives me undefined
...getBloodRequest has not necessarily finished. You set the $rootScope.newBloodneeded in the then, which is ONLY run after getBloodRequest resolves, which may be far, far after your console log finishes.
Bloodrequest.getBloodrequest($routeParams.id).then(function(data) {
...
$rootScope.newBloodneeded = $scope.newBloodneeded;
...
One fun test you can try is to wrap that console log in setTimeout for a LONG time (when you are guaranteed/sure that getBloodRequest has finished). That should prove to you that timing is the issue, not one of function scoping.
Basically:
setTimeout(() => console.log($rootScope.newBloodneeded), 10000000000) // or whatever timing guarantees completion
The solution here is to also chain whatever logic that requires $rootScope.newBloodneeded in .then. If THAT doesn't work, you might want to create a Promise that you access from elsewhere. (That's beyond the scope of this question, and we'd need more detail to figure out best implementation).

How to create test doubles with Sinon that prevents execution of the actual function

I am doing javascript unit testing with Mocha and Sinon. I want to test whether under certain circumstances, a certain method is called.
However, so far I couldn't manage only to test if the method was called. To make it clearer, I want a fake method to replace the actual method, because I don't want to simulate my whole application state to make this simple test pass.
This is my actual test code:
it('calls the handleResults method when its model syncs', function () {
var spy = sinon.stub( this.appview, 'handleResults' );
this.appview.model.fetch();
server.requests[0].respond( 200,
{ "Content-Type": "application/json" },
JSON.stringify( [ { id: "casa", text: "Something" } ] )
);
spy.should.have.been.called;
});
The real this.appview.handleResults method is being called, while I would want to call a fake version which does nothing else than checking whether it was called or not.
What am I doing wrong?
The docs are very clear that by using stub, the original function will be replaced by mock and will not be called:
As spies, stubs can be either anonymous, or wrap existing functions.
When wrapping an existing function with a stub, the original function
is not called.
and
var stub = sinon.stub(object, "method");
Replaces object.method with a
stub function. The original function can be restored by calling
object.method.restore(); (or stub.restore();). An exception is thrown
if the property is not already a function, to help avoid typos when
stubbing methods.

Unit testing with Jasmine: code in beforeEach() not seen by test's spyOn()

New to unit testing in general and Jasmine in particular.
I've set a variable in a beforeEach() callback, but it doesn't seem to work on the second test. It's supposed to fire initialization stuff in advance of every test within its context, right? I'm sure my spyOn() call is to blame, but I don't know how to fix it.
Comments explain the passes and fails:
describe("Test suite for my library", function () {
var html,
body,
play,
...
// custom matcher...
beforeEach(function () {
this.addMatchers({
toBeInstanceOf : function (constructr) {
return this.actual instanceof constructr;
});
});
});
describe("Within the Button object", function () {
beforeEach(function () {
play = new Button("play", false);
});
describe("play", function () {
// This test passes, as expected...
it("should be an instance of the Button object", function () {
expect(play).toBeInstanceOf(Button);
});
});
describe("play.name", function () {
// This test failed with the message
// "Expected spy Button to have been called
// with [ 'play', false ] but it was never called."
it("should be the first argument passed to the Button constructor", function () {
spyOn(window, "Button");
play = new Button("play", false); // ...until I added this line. Now it passes.
expect(window.Button).toHaveBeenCalledWith("play", false);
});
// This test passes, even if the one above fails.
it("should be 'play'", function () {
expect(play.name).toBe("play");
});
});
});
});
The documentation explains the usage, but not the context, of spyOn(), so I can't tell if I've created a bug or if I'm unknowingly taking advantage of a feature.
I can post the constructor if anyone thinks it makes any difference in the diagnosis, but I can assure you it's dead simple.
I'm sure it's a straightforward fix using some basic unit testing concept I'm having to learn the hard way. Thanks in advance.
P.S. I realize what I'm testing for in that failing spec isn't what I've described. I'm working my way through the API guide, looking for a way to get to the arguments array within a function call, so I can do a specific test on arguments[0]. Hints are appreciated, but not necessary. I'll figure it out.
Short answer: No, Before each and spies are not incompatible
You must Spy before you call if you want the spy to know about the call. You can use spyOn(object, 'function').andCallThrough() if you do not wish to interfere with its default behavior.
Long answer: The way faking/mocking/stubbing/spying frameworks often work is by replacing the method you are calling with a method that the mocking framework can control. Any calls to that function before it is replaced with the spy cannot be observed. This is a good thing, though mildly inconvenient,
Its cause you spy on window.Button after you have called. Im not totally sure what spy does, but after all it displaced the function you spy on with another function where it can check the function was called and whats arguments was passed. When you create your Button before your start your test, the original window.button function called. Then you replaces the function with the spy and test that the spy was called, so your test must fail.
Seems either create your Button in the test itself or create your spy before you call new Button in your beforeEach function.

Categories