jest-auto-spies
v3.0.0
Published
Create automatic spies from classes in Jest tests, also for promises and observables
Downloads
63,696
Maintainers
Readme
jest-auto-spies
Easy and type safe way to write spies for jest tests, for both sync and async (promises, Observables) returning methods.
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
andmustBeCalledWith
✅ 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