UNPKG

jasmine-auto-spies

Version:

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

792 lines (552 loc) β€’ 23.6 kB
# jasmine-auto-spies Easy and type safe way to write spies for jasmine tests, for both sync and async (promises, Observables) returning methods. [![npm version](https://img.shields.io/npm/v/jasmine-auto-spies.svg?style=flat-square)](https://www.npmjs.org/package/jasmine-auto-spies) [![npm downloads](https://img.shields.io/npm/dm/jasmine-auto-spies.svg?style=flat-square)](http://npm-stat.com/charts.html?package=jasmine-auto-spies&from=2017-07-26) ![Build](https://github.com/hirezio/auto-spies/workflows/Build/badge.svg) [![codecov](https://img.shields.io/codecov/c/github/hirezio/auto-spies.svg?flags=jasmine-auto-spies)](https://codecov.io/gh/hirezio/auto-spies) [![Code of Conduct](https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square)](../../CODE_OF_CONDUCT.md) [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section --> [![All Contributors](https://img.shields.io/badge/all_contributors-8-green.svg?style=flat-square)](#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