jasmine-auto-spies
TypeScript icon, indicating that this package has built-in type declarations

8.0.1 • Public • Published

jasmine-auto-spies

Easy and type safe way to write spies for jasmine tests, for both sync and async (promises, Observables) returning methods.

npm version npm downloads Build codecov Code of Conduct License: MIT All Contributors



IMPORTANT: compatibility

  • Version 2.x and above requires RxJS 6.0 and above.
  • Version 3.x and above requires TypeScript 2.8 and above.


Table of Contents


Installation

pnpm add -D jasmine-auto-spies

or

npm install -D jasmine-auto-spies

THE PROBLEM: writing manual spies is tedious

You've probably seen this type of manual spies in tests:

let mySpy = {
  myMethod: jasmine.createSpy('myMethod'),
};

or even:

let mySpy = jasmine.createSpyObj('mySpy', ['myMethod']);

The problem with that is first -

  • ⛔ You need to repeat the method names in each test.
  • ⛔ Strings are not "type safe" or "refactor friendly"
  • ⛔ You only have synchronous configuration helpers (like returnValue)
  • ⛔ You don't have the ability to write conditional return values

THE SOLUTION: Auto Spies! 💪

If you need to create a spy from any class, just do:

const myServiceSpy = createSpyFromClass(MyService);

THAT'S IT!

If you're using TypeScript, you get EVEN MORE BENEFITS:

const myServiceSpy: Spy<MyService> = createSpyFromClass(MyService);

Now that you have an auto spy you'll be able to:

  • ✅ Have a spy with all of its methods generated automatically as "spy methods".

  • ✅ Rename/refactor your methods and have them change in ALL tests at once

  • ✅ Asynchronous helpers for Promises and Observables.

  • ✅ Conditional return values with calledWith and mustBeCalledWith

  • ✅ Have Type completion for both the original Class and the spy methods

  • ✅ Spy on getters and setters

  • ✅ Spy on Observable properties


Usage (JavaScript)

my-component.js

export class MyComponent {
  constructor(myService) {
    this.myService = myService;
  }
  init() {
    this.compData = this.myService.getData();
  }
}

my-service.js

export class MyService{

  getData{
    return [
      { ...someRealData... }
    ]
  }
}

my-spec.js

import { createSpyFromClass } from 'jasmine-auto-spies';
import { MyService } from './my-service';
import { MyComponent } from './my-component';

describe('MyComponent', () => {
  let myServiceSpy;
  let componentUnderTest;

  beforeEach(() => {
    //                      👇
    myServiceSpy = createSpyFromClass(MyService); // <- THIS IS THE IMPORTANT LINE

    componentUnderTest = new MyComponent(myServiceSpy);
  });

  it('should fetch data on init', () => {
    const fakeData = [{ fake: 'data' }];

    myServiceSpy.getData.and.returnValue(fakeData);

    componentUnderTest.init();

    expect(myServiceSpy.getData).toHaveBeenCalled();
    expect(componentUnderTest.compData).toEqual(fakeData);
  });
});

Usage (TypeScript)

▶ Angular developers - use TestBed.inject<any>(...)

⚠ Make sure you cast your spy with any when you inject it:

import { MyService } from './my-service';
import { Spy, createSpyFromClass } from 'jasmine-auto-spies';

let serviceUnderTest: MyService;

//                 👇
let apiServiceSpy: Spy<ApiService>;

beforeEach(() => {
  TestBed.configureTestingModule({
    providers: [
      MyService,
      //                                       👇
      { provide: ApiService, useValue: createSpyFromClass(ApiService) },
    ],
  });

  serviceUnderTest = TestBed.inject(MyService);

  //                             👇
  apiServiceSpy = TestBed.inject<any>(ApiService);
});

▶ Spying on synchronous methods

// my-service.ts

class MyService{
  getName(): string{
    return 'Bonnie';
  }
}

// my-spec.ts

import { Spy, createSpyFromClass } from 'jasmine-auto-spies';
import { MyService } from './my-service';

//                👇
let myServiceSpy: Spy<MyService>; // <- THIS IS THE IMPORTANT LINE

beforeEach( ()=> {
  //                     👇
  myServiceSpy = createSpyFromClass( MyService );
});

it('should do something', ()=> {
  myServiceSpy.getName.and.returnValue('Fake Name');

  ... (the rest of the test) ...
});

▶ Spying on methods (manually)

For cases that you have methods which are not part of the Class prototype (but instead being defined in the constructor), for example:

class MyClass {
  constructor() {
    this.customMethod1 = function () {
      // This definition is not part of MyClass' prototype
    };
  }
}

You can FORCE the creation of this methods spies like this:

//                                   👇
let spy = createSpyFromClass(MyClass, ['customMethod1', 'customMethod2']);

OR THIS WAY -

let spy = createSpyFromClass(MyClass, {
  //     👇
  methodsToSpyOn: ['customMethod1', 'customMethod2'],
});

▶ Spying on Promises

Use the resolveWith or rejectWith methods.

⚠ You must define a return type : Promise<SomeType> for it to work!

// SERVICE:

class MyService {
  // (you must define a return type)
  //             👇
  getItems(): Promise<Item[]> {
    return http.get('/items');
  }
}

// TEST:

import { Spy, createSpyFromClass } from 'jasmine-auto-spies';

let myServiceSpy: Spy<MyService>;

beforeEach(() => {
  myServiceSpy = createSpyFromClass(MyService);
});

it(() => {
  //                            👇
  myServiceSpy.getItems.and.resolveWith(fakeItemsList);

  // OR
  //                            👇
  myServiceSpy.getItems.and.rejectWith(fakeError);

  // OR
  //                              👇
  myServiceSpy.getItems.and.resolveWithPerCall([
    // 👇 return this promise for the FIRST getItems() call
    { value: fakeItemsList },
    // 👇 return this promise with a delay of 2 seconds (2000ms) for the SECOND getItems() call
    { value: someOtherItemsList, delay: 2000 },
  ]);
});

▶ Spying on Observables

Use the nextWith or throwWith and other helper methods.

⚠ You must define a return type : Observable<SomeType> for it to work!

// SERVICE:

class MyService {
  // (you must define a return type)
  //             👇
  getItems(): Observable<Item[]> {
    return http.get('/items');
  }
}

// TEST:

import { Spy, createSpyFromClass } from 'jasmine-auto-spies';

let myServiceSpy: Spy<MyService>;

beforeEach(() => {
  myServiceSpy = createSpyFromClass(MyService);
});

it(() => {
  //                           👇
  myServiceSpy.getItems.and.nextWith(fakeItemsList);

  // OR
  //                              👇
  myServiceSpy.getItems.and.nextOneTimeWith(fakeItemsList); // emits one value and completes

  // OR
  //                            👇
  myServiceSpy.getItems.and.nextWithValues([
    { value: fakeItemsList },
    { value: fakeItemsList, delay: 1000 },
    { errorValue: someError }, // <- will throw this error, you can also add a "delay"
    { complete: true }, // <- you can add a "delay" as well
  ]);

  // OR
  //                              👇
  const subjects = myServiceSpy.getItems.and.nextWithPerCall([
    // 👇 return this observable for the FIRST getItems() call
    { value: fakeItemsList },

    // 👇 return this observable after 2 seconds for the SECOND getItems call()
    { value: someOtherItemsList, delay: 2000 },

    // 👇 by default, the observable completes after 1 value
    // set "doNotComplete" if you want to keep manually emit values
    { value: someOtherItemsList, doNotComplete: true },
  ]);
  subjects[2].next('yet another emit');
  subjects[2].complete();

  // OR
  //                            👇
  myServiceSpy.getItems.and.throwWith(fakeError);

  // OR
  //                            👇
  myServiceSpy.getItems.and.complete();

  // OR

  // "returnSubject" is good for cases where you want
  // to separate the Spy Observable creation from it's usage.

  //                                             👇
  const subject = myServiceSpy.getItems.and.returnSubject(); // create and get a ReplaySubject
  subject.next(fakeItemsList);
});

▶ Spying on observable properties

If you have a property that extends the Observable type, you can create a spy for it as follows:

MyClass{
  myObservable: Observable<any>;
  mySubject: Subject<any>;
}

it('should spy on observable properties', ()=>{

  let classSpy = createSpyFromClass(MyClass, {
      //         👇
      observablePropsToSpyOn: ['myObservable', 'mySubject']
    }
  );

  // and then you could configure it with methods like `nextWith`:
  //                      👇
  classSpy.myObservable.nextWith('FAKE VALUE');

  let actualValue;
  classSpy.myObservable.subscribe((value) => actualValue = value )

  expect(actualValue).toBe('FAKE VALUE');

})

calledWith() - conditional return values

You can setup the expected arguments ahead of time by using calledWith like so:

//                           👇
myServiceSpy.getProducts.calledWith(1).returnValue(true);

and it will only return this value if your subject was called with getProducts(1).

Oh, and it also works with Promises / Observables:

//                                  👇             👇
myServiceSpy.getProductsPromise.calledWith(1).resolveWith(true);

// OR

myServiceSpy.getProducts$.calledWith(1).nextWith(true);

// OR ANY OTHER ASYNC CONFIGURATION METHOD...

mustBeCalledWith() - conditional return values that throw errors (Mocks)

//                              👇
myServiceSpy.getProducts.mustBeCalledWith(1).returnValue(true);

is the same as:

myServiceSpy.getProducts.and.returnValue(true);

expect(myServiceSpy.getProducts).toHaveBeenCalledWith(1);

But the difference is that the error is being thrown during getProducts() call and not in the expect(...) call.


▶ Create accessors spies (getters and setters)

If you have a property that extends the Observable type, you can create a spy for it.

You need to configure whether you'd like to create a "SetterSpy" or a "GetterSpy" by using the configuration settersToSpyOn and GettersToSpyOn.

This will create an object on the Spy called accessorSpies and through that you'll gain access to either the "setter spies" or the "getter spies":

// CLASS:

MyClass{
  private _myProp: number;
  get myProp(){
    return _myProp;
  }
  set myProp(value: number){
    _myProp = value;
  }
}

// TEST:

let classSpy: Spy<MyClass>;

beforeEach(()=>{
  classSpy = createSpyFromClass(MyClass, {

    //      👇
    gettersToSpyOn: ['myProp'],

    //      👇
    settersToSpyOn: ['myProp']
  });
})

it('should return the fake value', () => {

    //            👇          👇     👇
    classSpy.accessorSpies.getters.myProp.and.returnValue(10);

    expect(classSpy.myProp).toBe(10);
});

it('allow spying on setter', () => {

  classSpy.myProp = 2;

  //                  👇          👇     👇
  expect(classSpy.accessorSpies.setters.myProp).toHaveBeenCalledWith(2);
});

▶ Spying on a function

You can create an "auto spy" for a function using:

import { createFunctionSpy } from 'jasmine-auto-spies';

describe('Testing a function', () => {
  it('should be able to spy on a function', () => {
    function addTwoNumbers(a, b) {
      return a + b;
    }
    //                         👇           👇
    const functionSpy = createFunctionSpy<typeof addTwoNumbers>('addTwoNumbers');

    functionSpy.and.returnValue(4);

    expect(functionSpy()).toBe(4);
  });
});

Could also be useful for Observables -

// FUNCTION:

function getResultsObservable(): Observable<number> {
  return of(1, 2, 3);
}

// TEST:

it('should ...', () => {
  const functionSpy =
    createFunctionSpy<typeof getResultsObservable>('getResultsObservable');

  functionSpy.nextWith(4);

  // ... rest of the test
});

▶ Spying on abstract classes

Here's a nice trick you could apply in order to spy on abstract classes -

//  👇
abstract class MyAbstractClass {
  getName(): string {
    return 'Bonnie';
  }
}

describe(() => {
  //                                                    👇
  abstractClassSpy = createSpyFromClass<MyAbstractClass>(MyAbstractClass as any);
  abstractClassSpy.getName.and.returnValue('Evil Baboon');
});

And if you have abstract methods on that abstract class -

abstract class MyAbstractClass {
  // 👇
  abstract getAnimalName(): string;
}

describe('...', () => {
  //                                                                     👇
  abstractClassSpy = createSpyFromClass<MyAbstractClass>(MyAbstractClass as any, [
    'getAnimalName',
  ]);
  // OR

  abstractClassSpy.getAnimalName.and.returnValue('Evil Badger');
});

createObservableWithValues() - Create a pre-configured standalone observable

MOTIVATION: You can use this in order to create fake observable inputs with delayed values (instead of using marbles).

Accepts the same configuration as nextWithValues but returns a standalone observable.

EXAMPLE:

//
import { createObservableWithValues } from 'jasmine-auto-spies';

it('should emit the correct values', () => {
  //                                      👇
  const observableUnderTest = createObservableWithValues([
    { value: fakeItemsList },
    { value: secondFakeItemsList, delay: 1000 },
    { errorValue: someError }, // <- will throw this error, you can also add a "delay" to the error
    { complete: true }, // <- you can also add a "delay" to the complete
  ]);
});

And if you need to emit more values, you can set returnSubject to true and get the subject as well.

it('should emit the correct values', () => {
  //        👇       👇
  const { subject, values$ } = createObservableWithValues(
    [
      { value: fakeItemsList },
      { value: secondFakeItemsList, delay: 1000 },
      { errorValue: someError }, // <- will throw this error, you can also add a "delay" to the error
      { complete: true }, // <- you can also add a "delay" to the complete
    ],
    //      👇
    { returnSubject: true }
  );

  subject.next(moreValues);
});

provideAutoSpy() - Small Utility for Angular Tests

This will save you the need to type:

{ provide: MyService, useValue: createSpyFromClass(MyService, config?) }

INTERFACE: provideAutoSpy(Class, config?)

USAGE EXAMPLE:

TestBed.configureTestingModule({

  providers: [
    MyComponent,
    provideAutoSpy(MyService)
  ];
})

myServiceSpy = TestBed.inject<any>(MyService);



Contributing

Want to contribute? Yayy! 🎉

Please read and follow our Contributing Guidelines to learn what are the right steps to take before contributing your time, effort and code.

Thanks 🙏


Code Of Conduct

Be kind to each other and please read our code of conduct.


Contributors ✨

Thanks goes to these wonderful people (emoji key):


Shai Reznik

💻 📖 🤔 🚇 🚧 🧑‍🏫 👀 ⚠️

Bnaya Peretz

💻 🤔 🔧

shuebner

💻 🤔 ⚠️

Meksi

💻 ⚠️

Taylor Ben

🤔

Yonatan Kra

💻

Martin Baum

💻 ⚠️

Guille Eneas Timón Grau

💻 ⚠️

Laurent Duveau

📖

Rainer Hahnekamp

🚧

WynieCronje

💻 🐛 🚧

This project follows the all-contributors specification. Contributions of any kind welcome!


License

MIT

Package Sidebar

Install

npm i jasmine-auto-spies

Weekly Downloads

11,481

Version

8.0.1

License

MIT

Unpacked Size

168 kB

Total Files

39

Last publish

Collaborators

  • hirez.io