Using Jasmine to spy on a function without an object
Asked Answered
A

10

186

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.

Aceldama answered 1/3, 2012 at 3:17 Comment(0)
P
170

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.

Profusive answered 1/3, 2012 at 6:30 Comment(7)
It worked! I think the error I was making earlier was that I was calling the spyOn with method() instead of method. Thanks!Aceldama
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.Keystroke
jasmine.createSpy() worked perfectly for me. Thanks!Peder
used test = jasmine.createSpy(); to spy on angularJs $anchroScroll worked perfectlyInglis
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?Oresund
I don't think this will work if your function is inside of a closure/other function/IIFE.Parrie
@ndp, can you please help me with this: #65657611Brie
S
104

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();
Sideman answered 21/4, 2017 at 0:40 Comment(10)
Thanks for the TypeScript hint. Should be equally the same for ES6/Babel, but I haven't tried it.Departmentalism
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.Eliaeliades
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 callEliaeliades
Worked like a charm! Thanks, you saved me a lot of time!Alephnull
@RichardMatsen do you find any way to resolve the scenario you mentioned?Charentemaritime
This is not working anymore got a Error: <spyOn> : parseCookie is not declared writable or has no setterAngiosperm
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)Competency
@Competency how is theFunction declared in its source file? I am seeing the above issues that Ling Vu and Richard Matsen are seeing.Bid
@Bid 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.Competency
Damn. That's what we are doing, too. We're on typescript 5.1.3, unsure if that should make a difference, though.Bid
L
52

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

Longboat answered 28/4, 2015 at 15:3 Comment(1)
This makes a little more sense and breaks it out far enough to duplicate with success. +1 from me. Thanks, C§Oresund
I
9

A very simple way:

import * as myFunctionContainer from 'whatever-lib';

const fooSpy = spyOn(myFunctionContainer, 'myFunc');
Imposture answered 27/12, 2017 at 20:25 Comment(0)
Z
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');
Zannini answered 14/10, 2022 at 15:28 Comment(0)
A
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.

Argus answered 18/6, 2018 at 18:31 Comment(1)
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.Ant
T
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();
});
Thumbscrew answered 28/3, 2021 at 16:30 Comment(0)
P
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');
Palmetto answered 15/6, 2020 at 11:33 Comment(1)
I did and it resulted in this error: Error: <spyOn> : default is not declared writable or has no setter. After some research it seem this solution only works on es5Endmost
P
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';
}
Pasta answered 16/3, 2023 at 11:56 Comment(0)
Z
-1

I guess it's the easiest way:

const funcSpy = spyOn(myFunc, 'call');
Zymogen answered 15/12, 2021 at 11:4 Comment(1)
I can't get this to workTarrah

© 2022 - 2024 — McMap. All rights reserved.