jasmine-auto-spies
Easy and type safe way to write spies for jasmine tests, for both sync and async (promises, Observables) returning methods.
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);
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
class MyService{
getName(): string{
return 'Bonnie';
}
}
import { Spy, createSpyFromClass } from 'jasmine-auto-spies';
import { MyService } from './my-service';
let myServiceSpy: Spy<MyService>;
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 () {
};
}
}
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!
class MyService {
getItems(): Promise<Item[]> {
return http.get('/items');
}
}
import { Spy, createSpyFromClass } from 'jasmine-auto-spies';
let myServiceSpy: Spy<MyService>;
beforeEach(() => {
myServiceSpy = createSpyFromClass(MyService);
});
it(() => {
myServiceSpy.getItems.and.resolveWith(fakeItemsList);
myServiceSpy.getItems.and.rejectWith(fakeError);
myServiceSpy.getItems.and.resolveWithPerCall([
{ value: fakeItemsList },
{ 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!
class MyService {
getItems(): Observable<Item[]> {
return http.get('/items');
}
}
import { Spy, createSpyFromClass } from 'jasmine-auto-spies';
let myServiceSpy: Spy<MyService>;
beforeEach(() => {
myServiceSpy = createSpyFromClass(MyService);
});
it(() => {
myServiceSpy.getItems.and.nextWith(fakeItemsList);
myServiceSpy.getItems.and.nextOneTimeWith(fakeItemsList);
myServiceSpy.getItems.and.nextWithValues([
{ value: fakeItemsList },
{ value: fakeItemsList, delay: 1000 },
{ errorValue: someError },
{ complete: true },
]);
const subjects = myServiceSpy.getItems.and.nextWithPerCall([
{ value: fakeItemsList },
{ value: someOtherItemsList, delay: 2000 },
{ value: someOtherItemsList, doNotComplete: true },
]);
subjects[2].next('yet another emit');
subjects[2].complete();
myServiceSpy.getItems.and.throwWith(fakeError);
myServiceSpy.getItems.and.complete();
const subject = myServiceSpy.getItems.and.returnSubject();
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']
}
);
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);
myServiceSpy.getProducts$.calledWith(1).nextWith(true);
βΆ 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":
MyClass{
private _myProp: number;
get myProp(){
return _myProp;
}
set myProp(value: number){
_myProp = value;
}
}
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 getResultsObservable(): Observable<number> {
return of(1, 2, 3);
}
it('should ...', () => {
const functionSpy =
createFunctionSpy<typeof getResultsObservable>('getResultsObservable');
functionSpy.nextWith(4);
});
βΆ 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',
]);
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 },
{ complete: true },
]);
});
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 },
{ complete: true },
],
{ 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