Reputation: 14185
I've created a script that receives JSON data from a file, parses it, reformats and groups em and then returns an aggregated table, which I am trying to write tests for.
The code structure
├── Readme.md
├── input.json
├── main.ts
├── services
│ ├── calculateTransactionTotals.js
│ └── validateInput.js
└── tests
├── mocks.js
└── tests.test.js
// main.js
import { validateInput, readInputFile } from './services/validateInput.js'
import { calculateTransactionTotals } from './services/calculateTransactionTotals.js'
validateInput()
const content = await readInputFile()
calculateTransactionTotals(content)
// calculateTransactionTotals
import groupby from 'https://esm.sh/lodash.groupby'
import moment from 'https://esm.sh/moment'
export function calculateTransactionTotals(content) {
groupTransactionsByUserAndCurrency(content) // omitting this for brevity, basically does a bunch of `groupby`s
prepareData(groupTransactionsByUserAndCurrency(content), content)
}
function prepareData(transactionsGroupedByUserIdAndCurrency, content) {
const result = {}
// some logic here, doesnt matter
printTable(result)
return result
}
function printTable(result) {
console.table(result, ['GBP', 'EUR', 'USD', 'last-activity'])
}
export const _internals = { prepareData }
My main idea is to run calculateTransactionTotals
and assert prepareData
was run and returned the expected value. But I'm struggling to get the spy
to run.
import { calculateTransactionTotals, _internals } from '../services/calculateTransactionTotals.js'
import data from './mocks.js'
import { assertSpyCalls, spy } from 'https://deno.land/[email protected]/testing/mock.ts'
Deno.test('is the expected result returned', () => {
const prepareDataSpy = spy(_internals, 'prepareData')
calculateTransactionTotals(data)
assertSpyCalls(prepareDataSpy, 1)
})
Any other proposals on how to test this functionality would be welcome and accepted as a correct answer.
Upvotes: 0
Views: 473
Reputation: 33856
First, some contextual observations:
In the module services/calculateTransactionTotals.js
you have defined and exported a function calculateTransactionTotals
. In the same module, you've defined a function prepareData
which is not directly exported. However, you have exported an object named _internals
which references the prepareData
function on a property by the same name (making it a method).
The function calculateTransactionTotals
contains an internal, direct reference to the function prepareData
, which makes it a closure:
export function calculateTransactionTotals(content) {
groupTransactionsByUserAndCurrency(content) // omitting this for brevity, basically does a bunch of `groupby`s
prepareData(groupTransactionsByUserAndCurrency(content), content)
}
This function reference is immutable. Spying on it does not replace it in the ES module graph.
The way that the spy
function works is to create and return a new "spy" function which is essentially a proxy to (or "wrapper" around) the original. In the case of a spied method, it also overwrites the method property on the target object.
The example you provided has quite a bit of code which is unrelated to the problem, so I'll create a minimal, reproducible example below in order to keep the focus on the problematic code:
mod.ts
:
// prepareData
function privateFn() {}
// calculateTransactionTotals
export function publicFn() {
privateFn();
}
export const _internals = { privateFn };
mod.test.ts
:
import {
assertSpyCalls,
spy,
} from "https://deno.land/[email protected]/testing/mock.ts";
import { _internals, publicFn } from "./mod.ts";
Deno.test("private function is invoked once", () => {
const spyFn = spy(_internals, "privateFn");
publicFn();
assertSpyCalls(spyFn, 1);
});
When running the test (copied from the code in your question), this is the output:
% deno test
Check file:///Users/deno/so-73911434/mod.test.ts
running 1 test from ./mod.test.ts
private function is invoked once ... FAILED (10ms)
ERRORS
private function is invoked once => ./mod.test.ts:7:6
error: AssertionError: spy not called as much as expected:
[Diff] Actual / Expected
- 0
+ 1
throw new AssertionError(message);
^
at assertSpyCalls (https://deno.land/[email protected]/testing/mock.ts:489:11)
at file:///Users/deno/so-73911434/mod.test.ts:10:3
FAILURES
private function is invoked once => ./mod.test.ts:7:6
FAILED | 0 passed | 1 failed (60ms)
error: Test failed
When the privateFn
method is spied on in the test, that doesn't update the internal reference to privateFn
in the publicFn
closure, which is your testing goal.
In order to do that, you'll need to reference privateFn
as a method on the _internals
object instead, like this:
mod.ts
:
function privateFn() {}
export const _internals = { privateFn };
export function publicFn() {
// Now invoking a method on `_internals`:
_internals.privateFn();
}
Now, when the test is run, the spy call replaces the method property with the spy, and the spy function is invoked by publicFn
(instead of the original function):
% deno test
Check file:///Users/deno/so-73911434/mod.test.ts
running 1 test from ./mod.test.ts
private function is invoked once ... ok (8ms)
ok | 1 passed | 0 failed (48ms)
One last note:
Be sure to reverse your spy mutation after you're finished, so that the original object is restored to its former state. This means adding one more line in the example test:
mod.test.ts
:
import {
assertSpyCalls,
spy,
} from "https://deno.land/[email protected]/testing/mock.ts";
import { _internals, publicFn } from "./mod.ts";
Deno.test("private function is invoked once", () => {
const spyFn = spy(_internals, "privateFn");
publicFn();
assertSpyCalls(spyFn, 1);
// Restore the original method to its property on the `_internals` object:
spyFn.restore();
});
Upvotes: 1