I know with sinon.js you can test that a spy was called a certain number of times:
sinon.assert.calledTwice(mySpy.someMethod);
And you can test that a spy was called with certain arguments:
sinon.assert.calledWith(mySpy.someMethod, 1, 2);
But how to you combine them to test that a method was called a specific number of times with specific arguments? Something, theoretically, like this:
sinon.assert.calledTwiceWith(mySpy.someMethod, 1, 2);
A spy provides access to the calls made to it using getCall() and getCalls(). Each Spy call can be tested using methods like calledWithExactly():
import * as sinon from 'sinon';
test('spy', () => {
const spy = sinon.spy();
spy(1, 2);
spy(3, 4);
expect(spy.callCount).toBe(2);
expect(spy.getCall(0).calledWithExactly(1, 2)).toBe(true);
expect(spy.getCall(1).calledWithExactly(3, 4)).toBe(true);
});
Unfortunately, Sinon doesn't have a function that checks what you're looking for, especially if you don't know or care about the order of calls ahead of time. However, it does let you inspect each time that a function was called individually. As a result, although it's a bit inelegant, you can count the number of times the function was called with the expected arguments yourself.
Use spy.getCalls() to get an array of the spy's calls, which are instances of spy call objects. Each call lets you access an array of the arguments passed to the spy with call.args (not call.args()).
test('spy', () => {
const spy = sinon.spy();
spy(1, 2);
spy(1, 2);
spy(3, 4);
const wantedCalls = spy.getCalls().filter(
(call) => call.args.length === 2 && call.args[0] === 1 && call.args[1] === 2
);
expect(wantedCalls.length).toEqual(2);
});
A bit more cleanly, as Brian Adams points out, you can call call.calledWithExactly() to check the arguments a specific call received:
test('spy', () => {
const spy = sinon.spy();
spy(1, 2);
spy(1, 2);
spy(3, 4);
const wantedCalls = spy.getCalls().filter(
(call) => call.calledWithExactly(1, 2)
);
expect(wantedCalls.length).toEqual(2);
});
Related
I've been learning jest and been doing ok so far but I've come up with something that I don't know how to resolve. I need to mock the #actions/github module and I think I've mocked the methods of the module the right way I think:
const githubListCommentsMock = jest.fn().mockReturnValue(
{
id: 1,
user: {login: 'github-actions[bot]'},
body: 'Code quality reports: Mock value'
})
const githubDeleteCommentMock = jest.fn()
const githubCreateCommentMock = jest.fn()
const githubIssuesMock = { listComments: githubListCommentsMock,
deleteComment: githubDeleteCommentMock,
createComment: githubCreateCommentMock
}
const githubContextMock = {repo:'Mocked Repository'}
jest.mock('#actions/github', () => ({
Github:jest.fn().mockImplementation(() => (
{issues: githubIssuesMock, context: githubContextMock}))
}))
But I have a piece of code on the file I'm testing that instances the github module like this:
const octokit = new github.GitHub(githubToken)
And my test fails when trying to execute the file with the following error:
TypeError: github.GitHub is not a constructor
Also been learning Jest and was having similar challenges. It is a little different to what you describe (and inline with the hydrated github client mentioned by M Mansour in the comments).
// Setup stub Octokit to return from getOctokit.
const StubOctokit = {
search: {
code: jest.fn()
}
}
// Setup fake response from call to Octokit.search.code.
const fakeResponse = {
data: {
total_count: 4
}
}
// Code search should return fakeResponse.
StubOctokit.search.code.mockReturnValueOnce(fakeResponse);
// getOctokit should return the Octokit stub.
github.getOctokit.mockReturnValueOnce(StubOctokit);
I'm sure there are likely much cleaner ways of doing it. After going round in circles for a while trying different approaches I ended up going step by step. Asserting getOctokit to have been called, using mockReturnValueOnce to set it an empty object, using toHaveLastReturnedWith and building up from there until getting the expected result.
I have an observable which might throw an error. When it throws, I want to resubscribe to that observable and try again. For example with the retry() operator.
To test this retry-logic I would need to create a test-observable which will throw an error the first 2 times it's subscribed to, and only on 3rd time would produce a value.
I tried the following:
import { Observable } from 'rxjs';
import { TestScheduler } from 'rxjs/testing';
import { retry } from 'rxjs/operators';
// Setup for TestScheduler
function basicTestScheduler() {
return new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
}
// The function we're going to test
function retryMultipleTimes(observable$) {
return observable$.pipe(retry(2));
}
describe('retryMultipleTimes()', () => {
it('retries twice when observable throws an error', () => {
basicTestScheduler().run(({ hot, cold, expectObservable }) => {
const observable$ = hot('--#--#--Y');
const expected = ' --------Y'; // This is what I want to get
const unexpected = ' --# '; // This is what I get instead
expectObservable(retryMultipleTimes(observable$)).toBe(expected);
});
});
});
Seems that with a hot() observable it always resubscribes to the same frame that produced the error, resulting in immediately throwing again.
I also tried with cold() observable, in which case I get ------# - that is, at each retry the observable starts again from the beginning, resulting in --#, --#, --# - never reaching --Y.
It seems that there isn't a way to do such a thing with RxJS TestScheduler. Or perhaps there is?
If the hot() and cold() observable-creators aren't up to the task, perhaps I can create my own... but how?
I also tried adding a little delay between retries, so I wouldn't resubscribe immediately to the current frame by implementing the retry-logic using retryWhen:
function retryMultipleTimes(observable$) {
return observable$.pipe(
retryWhen(errors => errors.pipe(
delayWhen(() => timer(2)), // wait 2 frames before each retry
take(2), // do maximum of 2 retries
concat(throwError('error')), // finish with error when no success after 2 retries
)),
);
}
But this didn't work either. Looks like the resubscription still happens to the same frame as before.
How could I make this test pass?
Figured out a solution for this. I can use iif() to create an observable which chooses at subscription time between two observables:
describe('retryMultipleTimes()', () => {
it('retries twice when observable throws an error', () => {
basicTestScheduler().run(({ cold, expectObservable }) => {
let count = 0;
const observable$ = iif(
() => ++count <= 2,
cold('--#'),
cold('--Y'),
);
// --#
// --#
// --Y
expectObservable(retryMultipleTimes(observable$)).toBe('------Y');
});
});
});
I'm currently testing a Fibonacci algorithm that uses memoization+recursion.
function memoization(num, hash = {'0': 0, '1':1}) {
if (!hash.hasOwnProperty(num)) {
hash[num] = memoization(num-1,hash) + memoization(num-2,hash);
}
return hash[num];
}
I want to test the memoization aspect of the function in Jest to ensure that the function is properly using the hash and not doing redundant work:
test('is never run on the same input twice', ()=>{
fib.memoization = jest.fn(fib.memoization);
fib.memoization(30);
expect(allUniqueValues(fib.memoization.mock.calls)).toBeTruthy();
});
However, the mock.calls only reports this function being called once with the initial parameter value and doesn't keep track of the additional recursive calls. Any ideas?
Spies in JavaScript depend on the function being the property of an object.
They work by replacing the object property with a new function that wraps and tracks calls to the original.
If a recursive function calls itself directly it is not possible to spy on those calls since they refer directly to the function.
In order to spy on recursive calls they must refer to functions that can be spied on. Fortunately, this is possible and can be done in one of two ways.
The first solution is to wrap the recursive function in an object and refer to the object property for the recursion:
fib.js
const wrappingObject = {
memoization: (num, hash = { '0':0, '1':1 }) => {
if (hash[num-1] === undefined) {
hash[num-1] = wrappingObject.memoization(num-1, hash);
}
return hash[num-1] + hash[num-2];
}
};
export default wrappingObject;
fib.test.js
import fib from './fib';
describe('memoization', () => {
it('should memoize correctly', () => {
const mock = jest.spyOn(fib, 'memoization');
const result = fib.memoization(50);
expect(result).toBe(12586269025);
expect(mock).toHaveBeenCalledTimes(49);
mock.mockRestore();
});
});
The second solution is to import a recursive function back into its own module and use the imported function for the recursion:
fib.js
import * as fib from './fib'; // <= import the module into itself
export function memoization(num, hash = { '0':0, '1':1 }) {
if (hash[num-1] === undefined) {
hash[num-1] = fib.memoization(num-1, hash); // <= call memoization using the module
}
return hash[num-1] + hash[num-2];
}
fib.test.js
import * as fib from './fib';
describe('memoization', () => {
it('should memoize correctly', () => {
const mock = jest.spyOn(fib, 'memoization');
const result = fib.memoization(50);
expect(result).toBe(12586269025);
expect(mock).toHaveBeenCalledTimes(49);
mock.mockRestore();
});
});
The tests above are using Jest, but the ideas extend to other testing frameworks. For example, here is the test for the second solution using Jasmine:
// ---- fib.test.js ----
import * as fib from './fib';
describe('memoization', () => {
it('should memoize correctly', () => {
const spy = spyOn(fib, 'memoization').and.callThrough();
const result = fib.memoization(50);
expect(result).toBe(12586269025);
expect(spy.calls.count()).toBe(49);
});
});
(I optimized memoization to require the minimum number of calls)
I've found out that it works as expected if you turn memoization() into an arrow function:
fib.js
// Don't import module in itself
export const memoization = (num, hash = { '0': 0, '1': 1 }) => {
if (hash[num - 1] === undefined) {
hash[num - 1] = memoization(num - 1, hash); // <= call memoization as you would, without module
}
return hash[num - 1] + hash[num - 2];
};
fib.test.js
describe('memoization', () => {
it('should memoize correctly', () => {
const mock = jest.spyOn(fib, 'memoization');
const result = fib.memoization(50);
expect(result).toBe(12586269025);
expect(mock).toHaveBeenCalledTimes(49);
mock.mockRestore();
});
});
For this case, I would suggest taking a Functional Programming approach.
Build your function to accept as 1st parameter the recursive function
Create a Factory function to bind the recursive to itself
Use the Factory function to produce your function
function memoizationRecursive(self, num, hash = {'0': 0, '1':1}) {
if (!hash.hasOwnProperty(num)) {
hash[num] = self(self, num-1,hash) + self(self, num-2,hash);
}
return hash[num];
}
const memoizationFactory = (recursiveFn) => recursiveFn.bind(null, recursiveFn);
const memoization = memoizationFactory(memoizationRecursive);
Then in your test file, wrap the recursive function with a jest mock and use the Factory to produce the same function.
test('is never run on the same input twice', ()=>{
const memoizationRecursiveSpy = jest.fn(fib.memoizationRecursive);
const memoization = fib.memoizationFactory(memoizationRecursiveSpy);
memoization(30);
expect(allUniqueValues(memoizationRecursiveSpy.mock.calls)).toBeTruthy();
});
Man, this firebase unit testing is really kicking my butt.
I've gone through the documentation and read through the examples that they provide, and have gotten some of my more basic Firebase functions unit tested, but I keep running into problems where I'm not sure how to verify that the transactionUpdated function passed along to the refs .transaction is correctly updating the current object.
My struggle is probably best illustrated with their child-count sample code and a poor attempt I made at writing a unit test for it.
Let's say my function that I want to unit test does the following (taken straight from that above link):
// count.js
exports.countlikechange = functions.database.ref('/posts/{postid}/likes/{likeid}').onWrite(event => {
const collectionRef = event.data.ref.parent;
const countRef = collectionRef.parent.child('likes_count');
// ANNOTATION: I want to verify the `current` value is incremented
return countRef.transaction(current => {
if (event.data.exists() && !event.data.previous.exists()) {
return (current || 0) + 1;
}
else if (!event.data.exists() && event.data.previous.exists()) {
return (current || 0) - 1;
}
}).then(() => {
console.log('Counter updated.');
});
});
Unit Test Code:
const chai = require('chai');
const chaiAsPromised = require("chai-as-promised");
chai.use(chaiAsPromised);
const assert = chai.assert;
const sinon = require('sinon');
describe('Cloud Functions', () => {
let myFunctions, functions;
before(() => {
functions = require('firebase-functions');
myFunctions = require('../count.js');
});
describe('countlikechange', () => {
it('should increase /posts/{postid}/likes/likes_count', () => {
const event = {
// DeltaSnapshot(app: firebase.app.App, adminApp: firebase.app.App, data: any, delta: any, path?: string);
data: new functions.database.DeltaSnapshot(null, null, null, true)
}
const startingValue = 11
const expectedValue = 12
// Below code is misunderstood piece. How do I pass along `startingValue` to the callback param of transaction
// in the `countlikechange` function, and spy on the return value to assert that it is equal to `expectedValue`?
// `yield` is almost definitely not the right thing to do, but I'm not quite sure where to go.
// How can I go about "spying" on the result of a stub,
// since the stub replaces the original function?
// I suspect that `sinon.spy()` has something to do with the answer, but when I try to pass along `sinon.spy()` as the yields arg, i get errors and the `spy.firstCall` is always null.
const transactionStub = sinon.stub().yields(startingValue).returns(Promise.resolve(true))
const childStub = sinon.stub().withArgs('likes_count').returns({
transaction: transactionStub
})
const refStub = sinon.stub().returns({ parent: { child: childStub }})
Object.defineProperty(event.data, 'ref', { get: refStub })
assert.eventually.equals(myFunctions.countlikechange(event), true)
})
})
})
I annotated the source code above with my question, but I'll reiterate it here.
How can I verify that the transactionUpdate callback, passed to the transaction stub, will take my startingValue and mutate it to expectedValue and then allow me to observe that change and assert that it happened.
This is probably a very simple problem with an obvious solution, but I'm very new to testing JS code where everything has to be stubbed, so it's a bit of a learning curve... Any help is appreciated.
I agree that unit testing in the Firebase ecosystem isn't as easy as we'd like it to be. The team is aware of it, and we're working to make things better! Fortunately, there are some good ways forward for you right now!
I suggest taking a look at this Cloud Functions demo that we've just published. In that example we use TypeScript, but this'll all work in JavaScript too.
In the src directory you'll notice we've split out the logic into three files: index.ts has the entry-logic, saythat.ts has our main business-logic, and db.ts is a thin abstraction layer around the Firebase Realtime Database. We unit-test only saythat.ts; we've intentionally kept index.ts and db.ts really simple.
In the spec directory we have the unit tests; take a look at index.spec.ts. The trick that you're looking for: we use mock-require to mock out the entire src/db.ts file and replace it with spec/fake-db.ts. Instead of writing to the real database, we now store our performed operations in-memory, where our unit test can check that they look correct. A concrete example is our score field, which is updated in a transaction. By mocking the database, our unit test to check that that's done correctly is a single line of code.
I hope that helps you do your testing!
I am trying to mock my repository's Get() method to return an object in order to fake an update on that object, but my setup is not working:
Here is my Test:
[Test]
public void TestUploadDealSummaryReportUploadedExistingUpdatesSuccessfully()
{
var dealSummary = new DealSummary {FileName = "Test"};
_mockRepository.Setup(r => r.Get(x => x.FileName == dealSummary.FileName))
.Returns(new DealSummary {FileName = "Test"}); //not working for some reason...
var reportUploader = new ReportUploader(_mockUnitOfWork.Object, _mockRepository.Object);
reportUploader.UploadDealSummaryReport(dealSummary, "", "");
_mockRepository.Verify(r => r.Update(dealSummary));
_mockUnitOfWork.Verify(uow => uow.Save());
}
Here is the method that is being tested:
public void UploadDealSummaryReport(DealSummary dealSummary, string uploadedBy, string comments)
{
dealSummary.UploadedBy = uploadedBy;
dealSummary.Comments = comments;
// method should be mocked to return a deal summary but returns null
var existingDealSummary = _repository.Get(x => x.FileName == dealSummary.FileName);
if (existingDealSummary == null)
_repository.Insert(dealSummary);
else
_repository.Update(dealSummary);
_unitOfWork.Save();
}
And here is the error that I get when I run my unit test:
Moq.MockException :
Expected invocation on the mock at least once, but was never performed: r => r.Update(.dealSummary)
No setups configured.
Performed invocations:
IRepository1.Get(x => (x.FileName == value(FRSDashboard.Lib.Concrete.ReportUploader+<>c__DisplayClass0).dealSummary.FileName))
IRepository1.Insert(FRSDashboard.Data.Entities.DealSummary)
at Moq.Mock.ThrowVerifyException(MethodCall expected, IEnumerable1 setups, IEnumerable1 actualCalls, Expression expression, Times times, Int32 callCount)
at Moq.Mock.VerifyCalls(Interceptor targetInterceptor, MethodCall expected, Expression expression, Times times)
at Moq.Mock.Verify(Mock mock, Expression1 expression, Times times, String failMessage)
at Moq.Mock1.Verify(Expression`1 expression)
at FRSDashboard.Test.FRSDashboard.Lib.ReportUploaderTest.TestUploadDealSummaryReportUploadedExistingUpdatesSuccessfully
Through debugging I have found that the x => x.FileName is returning null, but even if i compare it to null I still get a null instead of the Deal Summary I want returned. Any ideas?
I'm guessing your setup isn't matching the call you make because they're two different anonymous lambdas. You may needs something like
_mockRepository.Setup(r => r.Get(It.IsAny<**whatever your get lambda is defined as**>()).Returns(new DealSummary {FileName = "Test"});
You could verify by setting a breakpoint in the Get() method of your repository and seeing if it is hit. It shouldn't be.