183

I'm using Jasmine and have a library js file with lots of functions which are not associated with any object (i.e. are global). How do I go about spying on these functions?

I tried using window/document as the object, but the spy did not work even though the function was called. I also tried wrapping it in a fake object as follows :

var fakeElement = {};
fakeElement.fakeMethod = myFunctionName;
spyOn(fakeElement, "fakeMethod");

and test with

expect(fakeElement.fakeMethod).toHaveBeenCalled();

This does not work either as the spy did not work.

starball
  • 20,030
  • 7
  • 43
  • 238
Chetter Hummin
  • 6,687
  • 8
  • 32
  • 44

10 Answers10

168

If you are defining your function:

function test() {};

Then, this is equivalent to:

window.test = function() {}  /* (in the browser) */

So spyOn(window, 'test') should work.

If that is not, you should also be able to:

test = jasmine.createSpy();

If none of those are working, something else is going on with your setup.

I don't think your fakeElement technique works because of what is going on behind the scenes. The original globalMethod still points to the same code. What spying does is proxy it, but only in the context of an object. If you can get your test code to call through the fakeElement it would work, but then you'd be able to give up global fns.

ndp
  • 21,546
  • 5
  • 36
  • 52
  • 2
    It worked! I think the error I was making earlier was that I was calling the spyOn with method() instead of method. Thanks! – Chetter Hummin Mar 01 '12 at 08:09
  • 3
    I've had some problems using spyOn(window, 'test') using chutzpah for running the tests as part of our automation due to 'window' not being assigned. Using jasmine.createSpy() got around this. – Henners Sep 24 '13 at 13:43
  • 7
    jasmine.createSpy() worked perfectly for me. Thanks! – dplass Jul 24 '14 at 00:34
  • 1
    used `test = jasmine.createSpy();` to spy on angularJs `$anchroScroll` worked perfectly – Subtubes Mar 17 '15 at 21:20
  • 1
    For some reason I can't get either way to work, but it may be entirely possible that it's because I'm trying to mock up an existing window function; `$window.open(url, '_blank');` with the intention of opening a new tab (or window depending on browser setup). How should I be going about making sure it's calling this function and verifying that it's navigating to the right url regardless of the browser? – CSS Aug 19 '15 at 15:55
  • I don't think this will work if your function is inside of a closure/other function/IIFE. – Jon Gunter Mar 06 '18 at 17:11
  • @ndp, can you please help me with this: https://stackoverflow.com/questions/65657111/how-to-unit-test-an-angular-component-that-is-making-get-call-using-a-service – Tanzeel Jan 10 '21 at 19:30
100

TypeScript users:

I know the OP asked about javascript, but for any TypeScript users who come across this who want to spy on an imported function, here's what you can do.

In the test file, convert the import of the function from this:

import {foo} from '../foo_functions';

x = foo(y);

To this:

import * as FooFunctions from '../foo_functions';

x = FooFunctions.foo(y);

Then you can spy on FooFunctions.foo :)

spyOn(FooFunctions, 'foo').and.callFake(...);
// ...
expect(FooFunctions.foo).toHaveBeenCalled();
Alexander Taylor
  • 16,574
  • 14
  • 62
  • 83
  • 3
    Thanks for the TypeScript hint. Should be equally the same for ES6/Babel, but I haven't tried it. – hgoebl May 29 '17 at 12:09
  • 2
    Seems it only works if calling the function **explicitly with the alias FooFunctions**. I have a function bar() that is a factory returning baz() and want to test that baz() calls foo(). This method doesn't appear to work in that scenario. – Richard Matsen Jul 11 '17 at 22:46
  • 7
    This will work if the alias is taken inside foo_functions `export const FooFunctions = { bar, foo };` and the import in the test becomes `import { FooFunctions } from '../foo_functions'.` However, the alias still needs to be explicitly used within foo_functions private implementation for the spy to work. `const result = FooFunctions.foo(params)` // spy reports call `const result = foo(params)` // spy reports no call – Richard Matsen Jul 11 '17 at 23:28
  • 2
    Worked like a charm! Thanks, you saved me a lot of time! – SrAxi Jan 05 '18 at 13:33
  • @RichardMatsen do you find any way to resolve the scenario you mentioned? – Caxton Aug 03 '18 at 05:02
  • 37
    This is not working anymore got a `Error: : parseCookie is not declared writable or has no setter` – Ling Vu May 14 '20 at 06:58
  • 1
    I successfully used this in a situation where the class under test used `import { theFunction } from '../filewiththefunction'` and the test spec used `import * as prefix from '../filewiththefunction'`, then created a spy with `theFunctionSpy = spyOn(prefix, 'theFunction')`. If this doesn't work for someone then you may have another issue going on. (Typescript 4.3.5) – coppereyecat Jul 23 '21 at 17:02
  • @coppereyecat how is `theFunction` declared in its source file? I am seeing the above issues that Ling Vu and Richard Matsen are seeing. – wilbur Jul 18 '23 at 18:52
  • @wilbur I couldn't honestly tell you for sure now since this was 2 years ago, but most probably `export function theFunction(input: any): ReturnType {.....` since that's typically the syntax I prefer. I use this syntax in jest frequently as well in my current project and it works the same way though. – coppereyecat Jul 18 '23 at 21:23
  • Damn. That's what we are doing, too. We're on typescript 5.1.3, unsure if that should make a difference, though. – wilbur Jul 18 '23 at 21:33
51

There is 2 alternative which I use (for jasmine 2)

This one is not quite explicit because it seems that the function is actually a fake.

test = createSpy().and.callFake(test); 

The second more verbose, more explicit, and "cleaner":

test = createSpy('testSpy', test).and.callThrough();

-> jasmine source code to see the second argument

IxDay
  • 3,667
  • 2
  • 22
  • 27
  • This makes a little more sense and breaks it out far enough to duplicate with success. +1 from me. Thanks, C§ – CSS Aug 19 '15 at 20:00
9

A very simple way:

import * as myFunctionContainer from 'whatever-lib';

const fooSpy = spyOn(myFunctionContainer, 'myFunc');
FlavorScape
  • 13,301
  • 12
  • 75
  • 117
3

Solution with TypeScript

Replace your import:

import { yourGlobalFunction } from '../your-global-functions';

with:

import * as Functions from '../your-global-functions';

Then:

spyOnProperty(Functions, 'yourGlobalFunction').and.returnValue(() => 'mock return value');
Francesco Borzi
  • 56,083
  • 47
  • 179
  • 252
1
import * as saveAsFunctions from 'file-saver';
..........
....... 
let saveAs;
            beforeEach(() => {
                saveAs = jasmine.createSpy('saveAs');
            })
            it('should generate the excel on sample request details page', () => {
                spyOn(saveAsFunctions, 'saveAs').and.callFake(saveAs);
                expect(saveAsFunctions.saveAs).toHaveBeenCalled();
            })

This worked for me.

Sushil
  • 21
  • 1
  • 4
    Please add explanation to your answer, code by itself is not that helpful for the person asking the question if they cannot understand what is going on. – chevybow Jun 18 '18 at 18:56
1

The approach we usually follow, is as follows:

utils.ts file for all global utilities:

function globalUtil() {
  // some code
}

abc.component.ts:

function foo {
  // some code
  globalUtil();  // calls global function from util.ts
}

While writing a Jasmine test for function foo (), you can spy on the globalUtil function as follows:

abc.component.spec.ts:

import * as SharedUtilities from 'util.ts';

it('foo', () =>
{
  const globalUtilSpy = jasmine.createSpy('globalUtilSpy');
  spyOnProperty(SharedUtilities, "globalUtilSpy").and.returnValue(globalUtilSpy);
  
  foo();
  expect(globalUtilSpy).toHaveBeenCalled();
});
thinkOfaNumber
  • 2,581
  • 3
  • 27
  • 46
AkshAy Agrawal
  • 163
  • 1
  • 8
0

My answer differs slightly to @FlavorScape in that I had a single (default export) function in the imported module, I did the following:

import * as functionToTest from 'whatever-lib';

const fooSpy = spyOn(functionToTest, 'default');
JackDev
  • 4,891
  • 1
  • 39
  • 48
  • 1
    I did and it resulted in this error: Error: : default is not declared writable or has no setter. After some research it seem this solution only works on es5 – Royer Adames Nov 02 '22 at 00:48
0

I found a new way because the suggested solutions don't work for me :( So you can do it like this:

import * as FooFunctions from 'foo-functions';

spyOnProperty(FooFunctions, 'foo').and.returnValue(jasmine.createSpy());

If you want do callThrough:

import * as FooFunctions from 'foo-functions';

const originalFn = FooFunctions.foo;
spyOnProperty(FooFunctions, 'foo').and.returnValue(
    jasmine.createSpy().and.callFake(originalFn)
);

To make it more convenient, I made a helper. You can use it like this:

import * as FooFunctions from 'foo-functions';

spyOnFunction(FooFunctions, 'foo'); // to call through
spyOnFunction(FooFunctions, 'foo').and.callFake(...) // to call fake
spyOnFunction(FooFunctions, 'foo').and... // to do something else

Here is the helper code:

function spyOnFunction<T, K extends keyof T>(source: T, originalFnKey: K): jasmine.Spy {
    const originalFn: T[K] = source[originalFnKey];

    if (!isFunction(originalFn)) {
        throw new Error('[spyOnFunction] spy target must be a function');
    }

    const spy: jasmine.Spy = jasmine.createSpy().and.callFake(originalFn);

    spyOnProperty(source, originalFnKey).and.returnValue(spy);

    return spy;
}

function isFunction(item: unknown): item is (...args: unknown[]) => unknown {
    return typeof item === 'function';
}
-1

I guess it's the easiest way:

const funcSpy = spyOn(myFunc, 'call');
onurbaysan
  • 1,248
  • 8
  • 27
Ludwig
  • 328
  • 1
  • 6