npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

jest-auto-spies

v3.0.0

Published

Create automatic spies from classes in Jest tests, also for promises and observables

Downloads

55,497

Readme

jest-auto-spies

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

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

Table of Contents

Installation

pnpm add -D jest-auto-spies

or

npm install -D jest-auto-spies

THE PROBLEM: writing manual spies is tedious

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

let mySpy = {
  myMethod: jest.fn(),
};

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 don't have the ability to write conditional return values
  • ⛔ You don't have helper methods for Observables

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 'jest-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.mockReturnValue(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 'jest-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 'jest-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.mockReturnValue('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 'jest-auto-spies';

let myServiceSpy: Spy<MyService>;

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

it(() => {
  //                       👇
  myServiceSpy.getItems.resolveWith(fakeItemsList);
  // OR
  //                       👇
  myServiceSpy.getItems.rejectWith(fakeError);

  // OR
  //                              👇
  myServiceSpy.getItems.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 },
  ]);
});

although with jest you don't really have to do that as you have the native mockResolvedValue and mockRejectedValue -

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

let myServiceSpy: Spy<MyService>;

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

it(() => {
  //                           👇
  myServiceSpy.getItems.mockResolvedValue(fakeItemsList);
  // OR
  //                           👇
  myServiceSpy.getItems.mockRejectedValue(fakeError);
});

So the resolveWith and rejectWith are useful for backward compatibility if you're migrating from jasmine-auto-spies.

▶ 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 'jest-auto-spies';

let myServiceSpy: Spy<MyService>;

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

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

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

  // OR
  //                         👇
  myServiceSpy.getItems.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.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.throwWith(fakeError);

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

  // OR

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

  //                                         👇
  const subject = myServiceSpy.getItems.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.mockReturnValue(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.mockReturnValue(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 'jest-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.mockReturnValue(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.mockReturnValue('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.mockReturnValue('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):

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

License

MIT