Create automatic spies from classes in Jest tests, also for promises and observables
npm install jest-auto-spiesEasy and type safe way to write spies for jest tests, for both sync and async (promises, Observables) returning methods.


!Build




- THE PROBLEM: writing manual spies is tedious
- THE SOLUTION: Auto Spies! πͺ
- βΆ Angular developers - use TestBed.inject
- βΆ Spying on synchronous methods
- βΆ Spying on methods (manually)
- βΆ Spying on Promises
- βΆ Spying on Observables
- βΆ Spying on observable properties
- βΆ calledWith() - conditional return values
- βΆ mustBeCalledWith() - conditional return values that throw errors (Mocks)
- βΆ Create accessors spies (getters and setters)
- βΆ Spying on a function
- βΆ Spying on abstract classes
- βΆ createObservableWithValues() - Create a pre-configured standalone observable
- Contributing
- Code Of Conduct
- Contributors β¨
- License
``console`
pnpm add -D jest-auto-spies
or
`console`
npm install -D jest-auto-spies
You've probably seen this type of manual spies in tests:
`js`
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
If you need to create a spy from any class, just do:
`js`
const myServiceSpy = createSpyFromClass(MyService);
THAT'S IT!
If you're using TypeScript, you get EVEN MORE BENEFITS:
`ts`
const myServiceSpy: Spy
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
`js`
export class MyComponent {
constructor(myService) {
this.myService = myService;
}
init() {
this.compData = this.myService.getData();
}
}
`js
export class MyService{
getData{
return [
{ ...someRealData... }
]
}
}
`
`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);
});
});
`
β Make sure you cast your spy with any when you inject it:
`ts
import { MyService } from './my-service';
import { Spy, createSpyFromClass } from 'jest-auto-spies';
let serviceUnderTest: MyService;
// π
let apiServiceSpy: Spy
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
MyService,
// π
{ provide: ApiService, useValue: createSpyFromClass(ApiService) },
],
});
serviceUnderTest = TestBed.inject(MyService);
// π
apiServiceSpy = TestBed.inject
});
`
`ts
// 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
beforeEach( ()=> {
// π
myServiceSpy = createSpyFromClass( MyService );
});
it('should do something', ()=> {
myServiceSpy.getName.mockReturnValue('Fake Name');
... (the rest of the test) ...
});
`
For cases that you have methods which are not part of the Class prototype (but instead being defined in the constructor), for example:
`ts`
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 -
`ts`
let spy = createSpyFromClass(MyClass, {
// π
methodsToSpyOn: ['customMethod1', 'customMethod2'],
});
Use the resolveWith or rejectWith methods.
β You must define a return type : Promise for it to work!
`ts
// SERVICE:
class MyService {
// (you must define a return type)
// π
getItems(): Promise
return http.get('/items');
}
}
// TEST:
import { Spy, createSpyFromClass } from 'jest-auto-spies';
let myServiceSpy: Spy
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 -
`ts
import { Spy, createSpyFromClass } from 'jest-auto-spies';
let myServiceSpy: Spy
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.
Use the nextWith or throwWith and other helper methods.
β You must define a return type : Observable for it to work!
`ts
// SERVICE:
class MyService {
// (you must define a return type)
// π
getItems(): Observable
return http.get('/items');
}
}
// TEST:
import { Spy, createSpyFromClass } from 'jest-auto-spies';
let myServiceSpy: Spy
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);
});
`
If you have a property that extends the Observable type, you can create a spy for it as follows:
`ts
MyClass{
myObservable: Observable
mySubject: Subject
}
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');
})
`
You can setup the expected arguments ahead of time
by using calledWith like so:
`ts`
// π
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:
`ts
// π π
myServiceSpy.getProductsPromise.calledWith(1).resolveWith(true);
// OR
myServiceSpy.getProducts$.calledWith(1).nextWith(true);
// OR ANY OTHER ASYNC CONFIGURATION METHOD...
`
`ts`
// π
myServiceSpy.getProducts.mustBeCalledWith(1).returnValue(true);
is the same as:
`ts
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.
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":
`ts
// CLASS:
MyClass{
private _myProp: number;
get myProp(){
return _myProp;
}
set myProp(value: number){
_myProp = value;
}
}
// TEST:
let classSpy: Spy
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);
});
`
You can create an "auto spy" for a function using:
`ts
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
functionSpy.mockReturnValue(4);
expect(functionSpy()).toBe(4);
});
});
`
Could also be useful for Observables -
`ts
// FUNCTION:
function getResultsObservable(): Observable
return of(1, 2, 3);
}
// TEST:
it('should ...', () => {
const functionSpy =
createFunctionSpy
functionSpy.nextWith(4);
// ... rest of the test
});
`
Here's a nice trick you could apply in order to spy on abstract classes -
`ts
// π
abstract class MyAbstractClass {
getName(): string {
return 'Bonnie';
}
}
describe(() => {
// π
abstractClassSpy = createSpyFromClass
abstractClassSpy.getName.mockReturnValue('Evil Baboon');
});
`
And if you have abstract methods on that abstract class -
`ts
abstract class MyAbstractClass {
// π
abstract getAnimalName(): string;
}
describe(() => {
// π
abstractClassSpy = createSpyFromClass
'getAnimalName',
]);
// OR
abstractClassSpy.getAnimalName.mockReturnValue('Evil Badger');
});
`
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:
`ts
//
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.
`ts
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);
});
`
This will save you the need to type:
{ provide: MyService, useValue: createSpyFromClass(MyService, config?) }
INTERFACE: provideAutoSpy(Class, config?)
USAGE EXAMPLE:
`ts
TestBed.configureTestingModule({
providers: [
MyComponent,
provideAutoSpy(MyService)
];
})
myServiceSpy = TestBed.inject
``
---
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 π
Be kind to each other and please read our code of conduct.
Thanks goes to these wonderful people (emoji key):
Shai Reznik π» π π€ π π§ π§βπ« π β οΈ | Guille Eneas TimΓ³n Grau π» β οΈ | Rainer Hahnekamp π§ |
This project follows the all-contributors specification. Contributions of any kind welcome!
MIT