jasmine-auto-spies
Version:
Create automatic spies from classes in jasmine tests, also for promises and observables
792 lines (552 loc) β’ 23.6 kB
Markdown
# jasmine-auto-spies
Easy and type safe way to write spies for jasmine tests, for both sync and async (promises, Observables) returning methods.
[](https://www.npmjs.org/package/jasmine-auto-spies)
[](http://npm-stat.com/charts.html?package=jasmine-auto-spies&from=2017-07-26)

[](https://codecov.io/gh/hirezio/auto-spies)
[](../../CODE_OF_CONDUCT.md)
[](https://opensource.org/licenses/MIT) <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
<br/>
<div align="center">
<a href="https://hirez.io?utm_medium=Open_Source&utm_source=Github&utm_campaign=Lead_Generation&utm_content=jasmine_auto_spies_readme_banner">
<img src="../../for-readme/test-angular.jpg"
alt="TestAngular.com - Free Angular Testing Workshop - The Roadmap to Angular Testing Mastery"
width="600"
/>
</a>
</div>
<br/>
## IMPORTANT: compatibility
- Version `2.x` and above requires **RxJS 6.0** and above.
- Version `3.x` and above requires **TypeScript 2.8** and above.
<br/>
<div>
<a href="https://twitter.com/angular/status/1318492030016884736">
<img src="https://user-images.githubusercontent.com/1430726/97088487-c9d41b00-1639-11eb-814f-2c2c7258448e.png"
alt="Angular Tweet about Jasmine-auto-spies"
width="500"
/>
</a>
</div>
<br/>
# Table of Contents
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [Installation](#installation)
- [THE PROBLEM: writing manual spies is tedious](#the-problem-writing-manual-spies-is-tedious)
- [THE SOLUTION: Auto Spies! πͺ](#the-solution-auto-spies-)
- [Usage (JavaScript)](#usage-javascript)
- [Usage (TypeScript)](#usage-typescript)
- [βΆ Angular developers - use `TestBed.inject<any>(...)`](#-angular-developers---use-testbedinjectany)
- [βΆ Spying on synchronous methods](#-spying-on-synchronous-methods)
- [βΆ Spying on methods (manually)](#-spying-on-methods-manually)
- [βΆ Spying on Promises](#-spying-on-promises)
- [βΆ Spying on Observables](#-spying-on-observables)
- [βΆ Spying on observable properties](#-spying-on-observable-properties)
- [βΆ `calledWith()` - conditional return values](#-calledwith---conditional-return-values)
- [βΆ `mustBeCalledWith()` - conditional return values that throw errors (Mocks)](#-mustbecalledwith---conditional-return-values-that-throw-errors-mocks)
- [βΆ Create accessors spies (getters and setters)](#-create-accessors-spies-getters-and-setters)
- [βΆ Spying on a function](#-spying-on-a-function)
- [βΆ Spying on abstract classes](#-spying-on-abstract-classes)
- [βΆ `createObservableWithValues()` - Create a pre-configured standalone observable](##-createobservablewithvalues---create-a-pre-configured-standalone-observable)
- [Contributing](#contributing)
- [Code Of Conduct](#code-of-conduct)
- [Contributors β¨](#contributors-)
- [License](#license)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
<br/>
## Installation
```console
pnpm add -D jasmine-auto-spies
```
or
```console
npm install -D jasmine-auto-spies
```
<br/>
## THE PROBLEM: writing manual spies is tedious
You've probably seen this type of manual spies in tests:
```js
let mySpy = {
myMethod: jasmine.createSpy('myMethod'),
};
```
or even:
```js
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
<br>
# THE SOLUTION: Auto Spies! πͺ
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<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**
<br/>
## Usage (JavaScript)
### `my-component.js`
```js
export class MyComponent {
constructor(myService) {
this.myService = myService;
}
init() {
this.compData = this.myService.getData();
}
}
```
### `my-service.js`
```js
export class MyService{
getData{
return [
{ ...someRealData... }
]
}
}
```
### `my-spec.js`
```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);
});
});
```
<br/>
## Usage (TypeScript)
### βΆ Angular developers - use `TestBed.inject<any>(...)`
β Make sure you cast your spy with `any` when you inject it:
```ts
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);
});
```
<br/>
### βΆ Spying on synchronous methods
```ts
// 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) ...
});
```
<br/>
### βΆ 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:
```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'],
});
```
<br/>
### βΆ Spying on Promises
Use the `resolveWith` or `rejectWith` methods.
β You **must define a return type** `: Promise<SomeType>` for it to work!
```ts
// 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 },
]);
});
```
<br/>
### βΆ Spying on Observables
Use the `nextWith` or `throwWith` and other helper methods.
β You **must define a return type** `: Observable<SomeType>` for it to work!
```ts
// 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);
});
```
<br/>
### βΆ Spying on observable properties
If you have a property that extends the `Observable` type, you can create a spy for it as follows:
```ts
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');
})
```
<br/>
### βΆ `calledWith()` - conditional return values
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...
```
<br/>
### βΆ `mustBeCalledWith()` - conditional return values that throw errors (Mocks)
```ts
// π
myServiceSpy.getProducts.mustBeCalledWith(1).returnValue(true);
```
is the same as:
```ts
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.
<br/>
### βΆ 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":
```ts
// 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);
});
```
<br/>
### βΆ Spying on a function
You can create an "auto spy" for a function using:
```ts
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 -
```ts
// 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
});
```
<br/>
### βΆ Spying on abstract classes
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<MyAbstractClass>(MyAbstractClass as any);
abstractClassSpy.getName.and.returnValue('Evil Baboon');
});
```
And if you have **abstract methods** on that abstract class -
```ts
abstract class MyAbstractClass {
// π
abstract getAnimalName(): string;
}
describe('...', () => {
// π
abstractClassSpy = createSpyFromClass<MyAbstractClass>(MyAbstractClass as any, [
'getAnimalName',
]);
// OR
abstractClassSpy.getAnimalName.and.returnValue('Evil Badger');
});
```
<br/>
### βΆ `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:**
```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);
});
```
<br/>
### βΆ `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:**
```ts
TestBed.configureTestingModule({
providers: [
MyComponent,
provideAutoSpy(MyService)
];
})
myServiceSpy = TestBed.inject<any>(MyService);
```
## <br/>
<br/>
## Contributing
Want to contribute? Yayy! π
Please read and follow our [Contributing Guidelines](../../CONTRIBUTING.md) to learn what are the right steps to take before contributing your time, effort and code.
Thanks π
<br/>
## Code Of Conduct
Be kind to each other and please read our [code of conduct](../../CODE_OF_CONDUCT.md).
<br/>
## Contributors β¨
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="http://www.hirez.io/"><img src="https://avatars1.githubusercontent.com/u/1430726?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Shai Reznik</b></sub></a><br /><a href="https://github.com/hirezio/auto-spies/commits?author=shairez" title="Code">π»</a> <a href="https://github.com/hirezio/auto-spies/commits?author=shairez" title="Documentation">π</a> <a href="#ideas-shairez" title="Ideas, Planning, & Feedback">π€</a> <a href="#infra-shairez" title="Infrastructure (Hosting, Build-Tools, etc)">π</a> <a href="#maintenance-shairez" title="Maintenance">π§</a> <a href="#mentoring-shairez" title="Mentoring">π§βπ«</a> <a href="https://github.com/hirezio/auto-spies/pulls?q=is%3Apr+reviewed-by%3Ashairez" title="Reviewed Pull Requests">π</a> <a href="https://github.com/hirezio/auto-spies/commits?author=shairez" title="Tests">β οΈ</a></td>
<td align="center"><a href="https://github.com/Bnaya"><img src="https://avatars0.githubusercontent.com/u/1304862?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Bnaya Peretz</b></sub></a><br /><a href="https://github.com/hirezio/auto-spies/commits?author=Bnaya" title="Code">π»</a> <a href="#ideas-Bnaya" title="Ideas, Planning, & Feedback">π€</a> <a href="#tool-Bnaya" title="Tools">π§</a></td>
<td align="center"><a href="https://github.com/shuebner"><img src="https://avatars0.githubusercontent.com/u/1770684?v=4?s=100" width="100px;" alt=""/><br /><sub><b>shuebner</b></sub></a><br /><a href="https://github.com/hirezio/auto-spies/commits?author=shuebner" title="Code">π»</a> <a href="#ideas-shuebner" title="Ideas, Planning, & Feedback">π€</a> <a href="https://github.com/hirezio/auto-spies/commits?author=shuebner" title="Tests">β οΈ</a></td>
<td align="center"><a href="https://dev-front.herokuapp.com/"><img src="https://avatars0.githubusercontent.com/u/5671930?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Meksi</b></sub></a><br /><a href="https://github.com/hirezio/auto-spies/commits?author=meksof" title="Code">π»</a> <a href="https://github.com/hirezio/auto-spies/commits?author=meksof" title="Tests">β οΈ</a></td>
<td align="center"><a href="https://github.com/taylor-ben"><img src="https://avatars0.githubusercontent.com/u/37868849?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Taylor Ben</b></sub></a><br /><a href="#ideas-taylor-ben" title="Ideas, Planning, & Feedback">π€</a></td>
<td align="center"><a href="https://yonatankra.com/"><img src="https://avatars0.githubusercontent.com/u/6459899?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Yonatan Kra</b></sub></a><br /><a href="https://github.com/hirezio/auto-spies/commits?author=YonatanKra" title="Code">π»</a></td>
<td align="center"><a href="http://www.treestructure.net/"><img src="https://avatars3.githubusercontent.com/u/780083?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Martin Baum</b></sub></a><br /><a href="https://github.com/hirezio/auto-spies/commits?author=treestructure" title="Code">π»</a> <a href="https://github.com/hirezio/auto-spies/commits?author=treestructure" title="Tests">β οΈ</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/GuilleEneas"><img src="https://avatars0.githubusercontent.com/u/5407478?v=4" width="100px;" alt=""/><br /><sub><b>Guille Eneas TimΓ³n Grau</b></sub></a><br /><a href="https://github.com/hirezio/auto-spies/commits?author=GuilleEneas" title="Code">π»</a> <a href="https://github.com/hirezio/auto-spies/commits?author=GuilleEneas" title="Tests">β οΈ</a></td>
<td align="center"><a href="https://www.coding-academy.ca/"><img src="https://avatars0.githubusercontent.com/u/2942116?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Laurent Duveau </b></sub></a><br /><a href="https://github.com/hirezio/auto-spies/commits?author=ldex" title="Documentation">π</a></td>
<td align="center"><a href="https://www.rainerhahnekamp.com"><img src="https://avatars.githubusercontent.com/u/5721205?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Rainer Hahnekamp</b></sub></a><br /><a href="#maintenance-rainerhahnekamp" title="Maintenance">π§</a></td>
<td align="center"><a href="https://github.com/WynieCronje"><img src="https://avatars.githubusercontent.com/u/4537265?v=4?s=100" width="100px;" alt=""/><br /><sub><b>WynieCronje</b></sub></a><br /><a href="https://github.com/hirezio/auto-spies/commits?author=WynieCronje" title="Code">π»</a> <a href="https://github.com/hirezio/auto-spies/issues?q=author%3AWynieCronje" title="Bug reports">π</a> <a href="#maintenance-WynieCronje" title="Maintenance">π§</a></td>
</tr>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
<br/>
## License
MIT