@hirez_io/observer-spy
Advanced tools
Comparing version 1.3.0 to 1.4.0
@@ -1,11 +0,128 @@ | ||
# Contributor Code of Conduct | ||
# Contributor Covenant Code of Conduct | ||
As contributors and maintainers of jasmine-auto-spies, we pledge to respect everyone who contributes by posting issues, updating documentation, submitting pull requests, providing feedback in comments, and any other activities. | ||
## Our Pledge | ||
Communication through any of channel (GitHub, Gitter, IRC, mailing lists, Google+, Twitter, etc.) must be constructive and never resort to personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. | ||
We as members, contributors, and leaders pledge to make participation in our | ||
community a harassment-free experience for everyone, regardless of age, body | ||
size, visible or invisible disability, ethnicity, sex characteristics, gender | ||
identity and expression, level of experience, education, socio-economic status, | ||
nationality, personal appearance, race, religion, or sexual identity | ||
and orientation. | ||
We promise to extend courtesy and respect to everyone involved in this project regardless of gender, gender identity, sexual orientation, disability, age, race, ethnicity, religion, or level of experience. We expect anyone contributing to jasmine-auto-spies project to do the same. | ||
We pledge to act and interact in ways that contribute to an open, welcoming, | ||
diverse, inclusive, and healthy community. | ||
If any member of the community violates this code of conduct, the maintainers of jasmine-auto-spies may take action, removing issues, comments, and PRs or blocking accounts as deemed appropriate. | ||
## Our Standards | ||
This doc is based on [Angular's code of conduct](https://github.com/angular/code-of-conduct/blob/master/CODE_OF_CONDUCT.md) | ||
Examples of behavior that contributes to a positive environment for our | ||
community include: | ||
- Demonstrating empathy and kindness toward other people | ||
- Being respectful of differing opinions, viewpoints, and experiences | ||
- Giving and gracefully accepting constructive feedback | ||
- Accepting responsibility and apologizing to those affected by our mistakes, | ||
and learning from the experience | ||
- Focusing on what is best not just for us as individuals, but for the | ||
overall community | ||
Examples of unacceptable behavior include: | ||
- The use of sexualized language or imagery, and sexual attention or | ||
advances of any kind | ||
- Trolling, insulting or derogatory comments, and personal or political attacks | ||
- Public or private harassment | ||
- Publishing others' private information, such as a physical or email | ||
address, without their explicit permission | ||
- Other conduct which could reasonably be considered inappropriate in a | ||
professional setting | ||
## Enforcement Responsibilities | ||
Community leaders are responsible for clarifying and enforcing our standards of | ||
acceptable behavior and will take appropriate and fair corrective action in | ||
response to any behavior that they deem inappropriate, threatening, offensive, | ||
or harmful. | ||
Community leaders have the right and responsibility to remove, edit, or reject | ||
comments, commits, code, wiki edits, issues, and other contributions that are | ||
not aligned to this Code of Conduct, and will communicate reasons for moderation | ||
decisions when appropriate. | ||
## Scope | ||
This Code of Conduct applies within all community spaces, and also applies when | ||
an individual is officially representing the community in public spaces. | ||
Examples of representing our community include using an official e-mail address, | ||
posting via an official social media account, or acting as an appointed | ||
representative at an online or offline event. | ||
## Enforcement | ||
Instances of abusive, harassing, or otherwise unacceptable behavior may be | ||
reported to the community leaders responsible for enforcement at | ||
conduct@hirez.io. | ||
All complaints will be reviewed and investigated promptly and fairly. | ||
All community leaders are obligated to respect the privacy and security of the | ||
reporter of any incident. | ||
## Enforcement Guidelines | ||
Community leaders will follow these Community Impact Guidelines in determining | ||
the consequences for any action they deem in violation of this Code of Conduct: | ||
### 1. Correction | ||
**Community Impact**: Use of inappropriate language or other behavior deemed | ||
unprofessional or unwelcome in the community. | ||
**Consequence**: A private, written warning from community leaders, providing | ||
clarity around the nature of the violation and an explanation of why the | ||
behavior was inappropriate. A public apology may be requested. | ||
### 2. Warning | ||
**Community Impact**: A violation through a single incident or series | ||
of actions. | ||
**Consequence**: A warning with consequences for continued behavior. No | ||
interaction with the people involved, including unsolicited interaction with | ||
those enforcing the Code of Conduct, for a specified period of time. This | ||
includes avoiding interactions in community spaces as well as external channels | ||
like social media. Violating these terms may lead to a temporary or | ||
permanent ban. | ||
### 3. Temporary Ban | ||
**Community Impact**: A serious violation of community standards, including | ||
sustained inappropriate behavior. | ||
**Consequence**: A temporary ban from any sort of interaction or public | ||
communication with the community for a specified period of time. No public or | ||
private interaction with the people involved, including unsolicited interaction | ||
with those enforcing the Code of Conduct, is allowed during this period. | ||
Violating these terms may lead to a permanent ban. | ||
### 4. Permanent Ban | ||
**Community Impact**: Demonstrating a pattern of violation of community | ||
standards, including sustained inappropriate behavior, harassment of an | ||
individual, or aggression toward or disparagement of classes of individuals. | ||
**Consequence**: A permanent ban from any sort of public interaction within | ||
the community. | ||
## Attribution | ||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], | ||
version 2.0, available at | ||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. | ||
Community Impact Guidelines were inspired by [Mozilla's code of conduct | ||
enforcement ladder](https://github.com/mozilla/diversity). | ||
[homepage]: https://www.contributor-covenant.org | ||
For answers to common questions about this code of conduct, see the FAQ at | ||
https://www.contributor-covenant.org/faq. Translations are available at | ||
https://www.contributor-covenant.org/translations. |
@@ -1,2 +0,2 @@ | ||
# Contributing to observer-spy | ||
# Contribution Guidelines | ||
@@ -6,65 +6,98 @@ We would love for you to contribute to this project. | ||
## 1. Be Kind - Code of Conduct | ||
## Be Kind - Code of Conduct | ||
Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](CODE_OF_CONDUCT.md) | ||
Please read and follow our [Code of Conduct](CODE_OF_CONDUCT.md) to help us keep this project open and inclusive. | ||
## 2. Submitting an Issue | ||
<br/> | ||
You can file new issues by selecting from our [new issue templates](https://github.com/hirezio/observer-spy/issues/new/choose) and filling out the issue template. | ||
## Found a bug? Want a feature? - Submit an Issue | ||
## 3. Submitting a Pull Request (PR) | ||
[Choose an issue template](https://github.com/hirezio/observer-spy/issues/new/choose) to file a bug report / feature request. | ||
Before you submit your Pull Request (PR) consider the following guidelines: | ||
1. Search [GitHub](https://github.com/hirezio/observer-spy/pulls) for an open or closed PR | ||
that relates to your submission. You don't want to duplicate effort. | ||
1. Be sure that **there is an issue** describes the problem you're fixing, or documents the design for the feature you'd like to add. | ||
Discussing the design up front helps to ensure that we're ready to accept your work. | ||
<br/> | ||
1. Fork the this repo. | ||
1. Make your changes in a new git branch: | ||
## Ready to contribute a Pull Request (PR)? | ||
```shell | ||
git checkout -b my-fix-branch master | ||
``` | ||
<br/> | ||
1. Create your patch, **including appropriate test cases**. | ||
1. Run `yarn test` to check if all the tests are passing. | ||
and ensure that all tests pass. | ||
1. Commit your changes using: | ||
### ▶ 1. First - [Search this repo for existing PRs](https://github.com/hirezio/observer-spy/pulls) ! | ||
```shell | ||
yarn commit | ||
``` | ||
Try to find an open or closed PR that relates to the change you want to introduce. | ||
This will create a descriptive commit message that follows our | ||
[commit message conventions](#commit-message-format). | ||
This is necessary to generate meaningful release notes automatically. | ||
<br/> | ||
1. Push your branch to GitHub: | ||
### ▶ 2. **Before you start coding - [find](https://github.com/hirezio/observer-spy/issues) / [create an issue](https://github.com/hirezio/observer-spy/issues/new/choose)** | ||
```shell | ||
git push origin my-fix-branch | ||
``` | ||
**Make sure there's an issue** describing the problem you're fixing, or documents the design for the feature you'd like to add. | ||
Discussing the design up front helps to ensure that we're ready to accept your work. | ||
1. In GitHub, send a pull request to `observer-spy:master`. | ||
**Don't waste your time working on code before you got a 👍 in an issue comment.** | ||
- If we suggest changes then: | ||
<br/> | ||
- Make the required updates. | ||
- Re-run the tests to ensure tests are still passing. | ||
- Rebase your branch and force push to your GitHub repository (this will update your Pull Request): | ||
```shell | ||
git rebase master -i | ||
git push -f | ||
``` | ||
### ▶ 3. Fork the this repo and create a branch. | ||
That's it! Thank you for your contribution! | ||
Make your changes in a new git branch: | ||
#### After your pull request is merged | ||
```shell | ||
git checkout -b my-fix-branch master | ||
``` | ||
After your pull request is merged, you can safely delete your branch and pull the changes | ||
from the main (upstream) repository: | ||
<br/> | ||
### ▶ 4. Make sure you add / modify tests | ||
Run `yarn test:full` to make sure there aren't any errors | ||
<br/> | ||
### ▶ 5. Commit your changes using commitizen: | ||
Instead of `git commit` use the following command: | ||
```shell | ||
yarn commit | ||
``` | ||
It will then ask you a bunch of questions. | ||
This will create a descriptive commit message that follows the | ||
[Angular commit message convention](#commit-message-format). | ||
This is necessary to generate meaningful release notes / CHANGELOG automatically. | ||
<br/> | ||
### ▶ 6. Push your branch to GitHub: | ||
```shell | ||
git push origin my-fix-branch | ||
``` | ||
### ▶ 7. Create a PR | ||
In GitHub, create a pull request for `hirezio/observer-spy:master`. | ||
If you need to update your PR for some reason - | ||
- Make the required updates. | ||
- Re-run the tests to ensure tests are still passing `yarn test:full` | ||
- Rebase your branch and force push to your GitHub repository (this will update your Pull Request): | ||
```shell | ||
git rebase master -i | ||
git push -f | ||
``` | ||
<br/> | ||
### ▶ 8. After your PR is merged - delete your branches | ||
After your pull request is merged, you can safely delete your branch and pull the changes from the main (upstream) repository: | ||
- Delete the remote branch on GitHub either through the GitHub web UI or your local shell as follows: | ||
@@ -94,9 +127,7 @@ | ||
<hr> | ||
<br/> | ||
This doc is based on [Angular's contributing document](https://github.com/angular/angular/blob/master/CONTRIBUTING.md) | ||
### ▶ 9. That's it! Thank you for your contribution! 🙏💓 | ||
[coc]: CODE_OF_CONDUCT.md | ||
[commit-message-format]: https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit# | ||
[github]: https://github.com/hirezio/observer-spy | ||
[stackblitz]: https://stackblitz.com/ |
export { ObserverSpy } from './observer-spy'; | ||
export { subscribeAndSpyOn, ObserverSpyWithSubscription } from './subscribe-and-spy-on'; | ||
export { ObserverSpyWithSubscription, SubscriberSpy } from './subscriber-spy'; | ||
export { subscribeSpyTo, subscribeAndSpyOn } from './subscribe-spy-to'; | ||
export { fakeTime } from './fake-time'; | ||
export { autoUnsubscribe, queueForAutoUnsubscribe } from './auto-unsubscribe'; | ||
//# sourceMappingURL=index.d.ts.map |
@@ -5,7 +5,13 @@ "use strict"; | ||
exports.ObserverSpy = observer_spy_1.ObserverSpy; | ||
var subscribe_and_spy_on_1 = require("./subscribe-and-spy-on"); | ||
exports.subscribeAndSpyOn = subscribe_and_spy_on_1.subscribeAndSpyOn; | ||
exports.ObserverSpyWithSubscription = subscribe_and_spy_on_1.ObserverSpyWithSubscription; | ||
var subscriber_spy_1 = require("./subscriber-spy"); | ||
exports.ObserverSpyWithSubscription = subscriber_spy_1.ObserverSpyWithSubscription; | ||
exports.SubscriberSpy = subscriber_spy_1.SubscriberSpy; | ||
var subscribe_spy_to_1 = require("./subscribe-spy-to"); | ||
exports.subscribeSpyTo = subscribe_spy_to_1.subscribeSpyTo; | ||
exports.subscribeAndSpyOn = subscribe_spy_to_1.subscribeAndSpyOn; | ||
var fake_time_1 = require("./fake-time"); | ||
exports.fakeTime = fake_time_1.fakeTime; | ||
var auto_unsubscribe_1 = require("./auto-unsubscribe"); | ||
exports.autoUnsubscribe = auto_unsubscribe_1.autoUnsubscribe; | ||
exports.queueForAutoUnsubscribe = auto_unsubscribe_1.queueForAutoUnsubscribe; | ||
//# sourceMappingURL=index.js.map |
import { Observer } from 'rxjs'; | ||
export interface ObserverState { | ||
nextCalled: boolean; | ||
errorCalled: boolean; | ||
completeCalled: boolean; | ||
nextWasCalled: boolean; | ||
errorWasCalled: boolean; | ||
completeWasCalled: boolean; | ||
errorValue: any; | ||
@@ -11,3 +11,3 @@ onCompleteCallback: (() => void) | undefined; | ||
private onNextValues; | ||
private observerState; | ||
private state; | ||
next(value: T): void; | ||
@@ -14,0 +14,0 @@ error(errorVal: any): void; |
@@ -6,6 +6,6 @@ "use strict"; | ||
this.onNextValues = []; | ||
this.observerState = { | ||
nextCalled: false, | ||
errorCalled: false, | ||
completeCalled: false, | ||
this.state = { | ||
nextWasCalled: false, | ||
errorWasCalled: false, | ||
completeWasCalled: false, | ||
errorValue: undefined, | ||
@@ -17,12 +17,12 @@ onCompleteCallback: undefined, | ||
this.onNextValues.push(value); | ||
this.observerState.nextCalled = true; | ||
this.state.nextWasCalled = true; | ||
}; | ||
ObserverSpy.prototype.error = function (errorVal) { | ||
this.observerState.errorValue = errorVal; | ||
this.observerState.errorCalled = true; | ||
this.state.errorValue = errorVal; | ||
this.state.errorWasCalled = true; | ||
}; | ||
ObserverSpy.prototype.complete = function () { | ||
this.observerState.completeCalled = true; | ||
if (this.observerState.onCompleteCallback) { | ||
this.observerState.onCompleteCallback(); | ||
this.state.completeWasCalled = true; | ||
if (this.state.onCompleteCallback) { | ||
this.state.onCompleteCallback(); | ||
} | ||
@@ -32,11 +32,11 @@ }; | ||
var _this = this; | ||
if (this.observerState.completeCalled) { | ||
if (this.state.completeWasCalled) { | ||
return callback ? callback() : Promise.resolve(); | ||
} | ||
if (callback) { | ||
this.observerState.onCompleteCallback = callback; | ||
this.state.onCompleteCallback = callback; | ||
return; | ||
} | ||
return new Promise(function (resolve) { | ||
_this.observerState.onCompleteCallback = resolve; | ||
_this.state.onCompleteCallback = resolve; | ||
}); | ||
@@ -60,12 +60,12 @@ }; | ||
ObserverSpy.prototype.receivedNext = function () { | ||
return this.observerState.nextCalled; | ||
return this.state.nextWasCalled; | ||
}; | ||
ObserverSpy.prototype.getError = function () { | ||
return this.observerState.errorValue; | ||
return this.state.errorValue; | ||
}; | ||
ObserverSpy.prototype.receivedError = function () { | ||
return this.observerState.errorCalled; | ||
return this.state.errorWasCalled; | ||
}; | ||
ObserverSpy.prototype.receivedComplete = function () { | ||
return this.observerState.completeCalled; | ||
return this.state.completeWasCalled; | ||
}; | ||
@@ -72,0 +72,0 @@ return ObserverSpy; |
{ | ||
"name": "@hirez_io/observer-spy", | ||
"version": "1.3.0", | ||
"version": "1.4.0", | ||
"repository": { | ||
@@ -5,0 +5,0 @@ "type": "git", |
475
README.md
# @hirez_io/observer-spy 👀💪 | ||
A simple little class and a helper function that help make Observable testing a breeze | ||
This library makes RxJS Observables testing easy! | ||
@@ -10,4 +10,3 @@ [](https://www.npmjs.org/package/@hirez_io/observer-spy) | ||
[](https://codecov.io/gh/hirezio/observer-spy) <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section --> | ||
[](#contributors-) | ||
[](#contributors-) | ||
<!-- ALL-CONTRIBUTORS-BADGE:END --> | ||
@@ -24,17 +23,7 @@ | ||
## What's the problem? | ||
Testing RxJS observables is usually hard, especially when testing advanced use cases. | ||
This library: | ||
✅ **Is easy to understand** | ||
✅ **Reduces the complexity** | ||
✅ **Makes testing advanced observables easy** | ||
## Installation | ||
``` | ||
```console | ||
yarn add -D @hirez_io/observer-spy | ||
@@ -45,91 +34,189 @@ ``` | ||
``` | ||
```console | ||
npm install -D @hirez_io/observer-spy | ||
``` | ||
## Observer Spies VS Marble Tests | ||
<br/> | ||
[Marble tests](https://rxjs-dev.firebaseapp.com/guide/testing/internal-marble-tests) are very powerful, but at the same time can be very complicated to learn and to reason about for some people. | ||
## THE PROBLEM: Testing RxJS observables is hard! 😓 | ||
Especially when testing advanced use cases. | ||
Until this library, the common way to test observables was to use [Marble tests](https://rxjs-dev.firebaseapp.com/guide/testing/internal-marble-tests) | ||
### What are the disadvantages of Marble Tests? | ||
Marble tests are very powerful, but unfortunately for most tests they are conceptually very complicated to learn and to reason about.. | ||
You need to learn and understand `cold` and `hot` observables, `schedulers` and to learn a new syntax just to test a simple observable chain. | ||
More complex observable chains tests get even harder to read. | ||
More complex observable chains tests get even harder to read and to maintain. | ||
That's why this library was created - to present an alternative to marble tests, which we believe is cleaner and easier to understand and to use. | ||
<br/> | ||
<br/> | ||
### How observer spies are cleaner? | ||
## THE SOLUTION: Observer Spies! 👀💪 | ||
You generally want to test the outcome of your action, not implementation details like exactly how many frames were between each value. | ||
The **Observer-Spy** library was created to present a viable alternative to Marble Tests. | ||
The order of recieved values represents the desired outcome for most production app use cases. | ||
An alternative which we believe is: | ||
Most of the time, if enough (virtual) time passes until the expectation in my test, it should be sufficient to prove whether the expected outcome is valid or not. | ||
* ✅ **Easier** to understand | ||
## Usage | ||
* ✅ **Reduces** the complexity | ||
#### `new ObserverSpy()` | ||
* ✅ Makes observables tests **cleaner** | ||
<br/> | ||
In order to test observables, you can use an `ObserverSpy` instance to "record" all the messages a source observable emits and to get them as an array. | ||
## Why Observer-Spy is easier? | ||
You can also spy on the `error` or `complete` states of the observer. | ||
### 😮 Marble test: | ||
You can use `done` or `async` / `await` to wait for `onComplete` to be called as well. | ||
```js | ||
**Example:** | ||
import { TestScheduler } from 'rxjs/testing'; | ||
let scheduler: TestScheduler; | ||
beforeEach(()=>{ | ||
scheduler = new TestScheduler((actual, expected) => { | ||
expect(actual).toEqual(expected) | ||
}) | ||
}) | ||
it('should filter even numbers and multiply each number by 10', () => { | ||
scheduler.run(({cold, expectObservable}) => { | ||
const sourceValues = { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9, j: 10}; | ||
const source$ = cold('-a-b-c-d-e-f-g-h-i-j|', sourceValues); | ||
const expectedOrder = '-a-b-c-d-e|'; | ||
const expectedValues = { a: 10, b: 30, c: 50, d: 70, e: 90}; | ||
const result$ = source$.pipe( | ||
filter(n => n % 2 !== 0), | ||
map(x => x * 10) | ||
); | ||
expectObservable(result$).toBe(expectedOrder, expectedValues); | ||
}) | ||
}); | ||
``` | ||
### 😎 Observer Spy Test: | ||
```js | ||
// ... other imports | ||
import { ObserverSpy } from '@hirez_io/observer-spy'; | ||
it('should spy on Observable values', () => { | ||
const observerSpy = new ObserverSpy(); | ||
// BTW, if you're using TypeScript you can declare it with a generic: | ||
// const observerSpy: ObserverSpy<string> = new ObserverSpy(); | ||
import { subscribeSpyTo } from '@hirez_io/observer-spy'; | ||
const fakeValues = ['first', 'second', 'third']; | ||
const fakeObservable = of(...fakeValues); | ||
it('should filter even numbers and multiply each number by 10', () => { | ||
const result$ = of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).pipe( | ||
filter(n => n % 2 !== 0), | ||
map(x => x * 10) | ||
); | ||
const subscription = fakeObservable.subscribe(observerSpy); | ||
const observerSpy = subscribeSpyTo(result$); | ||
// DO SOME LOGIC HERE | ||
expect(observerSpy.getValues()).toEqual([10, 30, 50, 70, 90]); | ||
// unsubscribing is optional, it's good for stopping intervals etc | ||
subscription.unsubscribe(); | ||
}) | ||
}); | ||
``` | ||
expect(observerSpy.receivedNext()).toBe(true); | ||
expect(observerSpy.getValues()).toEqual(fakeValues); | ||
You generally want to test the outcome of your action instead of implementation details [like how many frames were between each value]. | ||
expect(observerSpy.getValuesLength()).toEqual(3); | ||
For most production app use cases, if enough (virtual) time passes testing the **received values** or their order should be sufficient. | ||
This library gives you the tool to investigate your spy about the values it received and their order. | ||
(The idea was inspired by [Reactive Programming with RxJava](https://books.google.co.il/books?id=y4Y1DQAAQBAJ)) | ||
<br/> | ||
# Usage | ||
<br/> | ||
## `const observerSpy = subscribeSpyTo(observable)` | ||
In order to test Observables you can use the `subscribeSpyTo` function: | ||
```js | ||
import { subscribeSpyTo } from '@hirez_io/observer-spy'; | ||
it('should immediately subscribe and spy on Observable ', () => { | ||
const fakeObservable = of('first', 'second', 'third'); | ||
// get a special observerSpy of type "SubscriberSpy" (with an additional "unsubscribe" method) | ||
// if you're using TypeScript you can declare it with a generic: | ||
// const observerSpy: SubscriberSpy<string> ... | ||
const observerSpy = subscribeSpyTo(fakeObservable); | ||
// You can unsubscribe if you need to: | ||
observerSpy.unsubscribe(); | ||
// EXPECTATIONS: | ||
expect(observerSpy.getFirstValue()).toEqual('first'); | ||
expect(observerSpy.receivedNext()).toBe(true); | ||
expect(observerSpy.getValues()).toEqual(fakeValues); | ||
expect(observerSpy.getValuesLength()).toEqual(3); | ||
expect(observerSpy.getFirstValue()).toEqual('first'); | ||
expect(observerSpy.getValueAt(1)).toEqual('second'); | ||
expect(observerSpy.getLastValue()).toEqual('third'); | ||
expect(observerSpy.receivedComplete()).toBe(true); | ||
observerSpy.onComplete(() => { | ||
expect(observerSpy.receivedComplete()).toBe(true); | ||
})); | ||
// -------------------------------------------------------- | ||
// You can also use this shorthand version: | ||
expect(subscribeSpyTo(fakeObservable).getFirstValue()).toEqual('first'); | ||
// -------------------------------------------------------- | ||
}); | ||
``` | ||
it('should support async await for onComplete()', async ()=>{ | ||
const observerSpy = new ObserverSpy(); | ||
<br/> | ||
### Wait for `onComplete` before expecting the result (using `async` + `await`) | ||
```js | ||
it('should support async await for onComplete()', async () => { | ||
const fakeObservable = of('first', 'second', 'third'); | ||
fakeObservable.subscribe(observerSpy); | ||
const observerSpy = subscribeSpyTo(fakeObservable); | ||
await observerSpy.onComplete(); | ||
await observerSpy.onComplete(); // <-- the test will pause here until the observable is complete | ||
expect(observerSpy.receivedComplete()).toBe(true); | ||
// If you don't want to use async await you could pass a callback: | ||
// | ||
// observerSpy.onComplete(() => { | ||
// expect(observerSpy.receivedComplete()).toBe(true); | ||
// })); | ||
}); | ||
``` | ||
<br/> | ||
### Spy on errors with `receivedError` and `getError` | ||
```js | ||
it('should spy on Observable errors', () => { | ||
const observerSpy = new ObserverSpy(); | ||
const fakeObservable = throwError('FAKE ERROR'); | ||
fakeObservable.subscribe(observerSpy); | ||
const observerSpy = subscribeSpyTo(fakeObservable); | ||
@@ -140,100 +227,169 @@ expect(observerSpy.receivedError()).toBe(true); | ||
}); | ||
``` | ||
## Quick Usage with `subscribeAndSpyOn(observable)` | ||
<br/> | ||
You can also create an `ObserverSpy` and immediately subscribe to an observable with this simple helper function. | ||
Observer spies generated that way will provide an additional `unsubscribe()` method that you might want to call | ||
if your source observable does not complete or does not get terminated by an error while testing. | ||
## Manually using `new ObserverSpy()` | ||
**Example:** | ||
You can create an `ObserverSpy` instance manually: | ||
```js | ||
import { subscribeAndSpyOn } from '@hirez_io/observer-spy'; | ||
// ... other imports | ||
import { ObserverSpy } from '@hirez_io/observer-spy'; | ||
it('should immediately subscribe and spy on Observable ', () => { | ||
const fakeObservable = of('first', 'second', 'third'); | ||
it('should spy on Observable values', () => { | ||
const fakeValues = ['first', 'second', 'third']; | ||
const fakeObservable = of(...fakeValues); | ||
// get an "ObserverSpyWithSubscription" | ||
const observerSpy = subscribeAndSpyOn(fakeObservable); | ||
// and optionally unsubscribe | ||
observerSpy.unsubscribe(); | ||
// BTW, if you're using TypeScript you can declare it with a generic: | ||
// const observerSpy: ObserverSpy<string> = new ObserverSpy(); | ||
const observerSpy = new ObserverSpy(); | ||
expect(observerSpy.getFirstValue()).toEqual('first'); | ||
// This type of ObserverSpy doesn't have a built in "unsubscribe" method | ||
// only the "SubscriberSpy" has it, so we need to create a separate "Subscription" variable. | ||
const subscription = fakeObservable.subscribe(observerSpy); | ||
// or use the shorthand version: | ||
expect(subscribeAndSpyOn(fakeObservable).getFirstValue()).toEqual('first'); | ||
// ...DO SOME LOGIC HERE... | ||
// unsubscribing is optional, it's good for stopping intervals etc | ||
subscription.unsubscribe(); | ||
expect(observerSpy.getValuesLength()).toEqual(3); | ||
}); | ||
``` | ||
# Testing Async Observables | ||
<br/> | ||
#### `it('should do something', fakeTime((flush) => { ... flush(); });` | ||
You can use the `fakeTime` utility function and call `flush()` to simulate the passage of time if you have any async operators like `delay` or `timeout` in your tests. | ||
# Auto Unsubscribing | ||
### [SEE AN EXAMPLE HERE](#-for-time-based-rxjs-code-timeouts--intervals--animations---use-faketime) | ||
### ⚠ PAY ATTENTION: | ||
--- | ||
* This works **only with subscriptions created** using either `subscribeSpyTo()` or `queueForAutoUnsubscribe()`. | ||
## Now, let's see some use cases and their solutions: | ||
* Requires a global `afterEach` function, so **it only works** with frameworks like **Jasmine**, **Mocha** and **Jest**. | ||
### ▶ For _Angular_ code - just use `fakeAsync` | ||
You can control time in a much more versatile way and clear the microtasks queue (for promises) without using the `done()` which is much more convenient. | ||
## `autoUnsubscribe()` | ||
Just use `fakeAsync` (and `tick` if you need it). | ||
In order to save you the trouble of calling `unsubscribe` in each test, you can configure the library to auto unsubscribe from every observer you create with `subscribeSpyTo()`. | ||
Example: | ||
### Configuring Jest with `autoUnsubscribe` | ||
Add this to your jest configuration (i.e `jest.config.js`): | ||
```js | ||
// ... other imports | ||
import { ObserverSpy } from '@hirez_io/observer-spy'; | ||
import { fakeAsync, tick } from '@angular/core/testing'; | ||
{ | ||
setupFilesAfterEnv: ['node_modules/@hirez_io/observer-spy/dist/setup-auto-unsubscribe.js'], | ||
} | ||
``` | ||
it('should test Angular code with delay', fakeAsync(() => { | ||
### Configuring Angular with `autoUnsubscribe` | ||
Add this to your `test.ts` | ||
```ts | ||
import { autoUnsubscribe } from '@hirez_io/observer-spy'; | ||
autoUnsubscribe(); | ||
``` | ||
### Manually adding a subscription with `queueForAutoUnsubscribe` | ||
## | ||
If you configured `autoUnsubscribe()` in your environment and want your manually created spies (via `new ObserverSpy()`) to be "auto unsubscribed" you can use `queueForAutoUnsubscribe(subscription)`. | ||
It accepts any `Unsubscribable` object which has an `unsubscribe()` method - | ||
```js | ||
import { queueForAutoUnsubscribe } from '@hirez_io/observer-spy'; | ||
it('should spy on Observable values', () => { | ||
const fakeValues = ['first', 'second', 'third']; | ||
const fakeObservable = of(...fakeValues); | ||
const observerSpy = new ObserverSpy(); | ||
const subscription = fakeObservable.subscribe(observerSpy) | ||
// This will auto unsubscribe this subscription after the test ends | ||
// (if you configured "autoUnsubscribe()" in your environment) | ||
queueForAutoUnsubscribe(subscription); | ||
const fakeObservable = of('fake value').pipe(delay(1000)); | ||
// ... rest of the test | ||
const sub = fakeObservable.subscribe(observerSpy); | ||
}); | ||
``` | ||
<br/> | ||
tick(1000); | ||
# Testing Sync Logic | ||
sub.unsubscribe(); | ||
### ▶ Synchronous RxJS | ||
expect(observerSpy.getLastValue()).toEqual('fake value'); | ||
})); | ||
RxJS - without delaying operators or async execution contexts - will run synchronously. This is the simplest use case; where our `it()` does not need any special asynchronous plugins. | ||
```ts | ||
it('should run synchronously', () => { | ||
const observerSpy = subscribeSpyTo(from(['first', 'second', 'third'])); | ||
expect(spy.getValuesLength()).toBe(3); | ||
}); | ||
``` | ||
### ▶ For microtasks related code (promises, but no timeouts / intervals) - just use `async` `await` or `done()` | ||
<br/> | ||
You can use the `onComplete` method to wait for a completion before checking the outcome. | ||
Chose between `async` + `await` or `done`, both work. | ||
<br/> | ||
Example: | ||
# Testing Async Logic | ||
If you're **not using Angular** and have RxJS async operators like `delay` or `timeout` | ||
Use `fakeTime` with `flush()` to simulate the passage of time ([detailed explanation](#-for-time-based-rxjs-code-timeouts--intervals--animations---use-faketime)) - | ||
[](#-for-time-based-rxjs-code-timeouts--intervals--animations---use-faketime) | ||
<br/> | ||
### ▶ RxJS + Angular: use `fakeAsync` | ||
With Angular, you can control time in a much more versatile way. | ||
Just use `fakeAsync` (and `tick` if you need it): | ||
```js | ||
// ... other imports | ||
import { ObserverSpy } from '@hirez_io/observer-spy'; | ||
import { subscribeSpyTo } from '@hirez_io/observer-spy'; | ||
import { fakeAsync, tick } from '@angular/core/testing'; | ||
it('should work with observables', async () => { | ||
const observerSpy: ObserverSpy<string> = new ObserverSpy(); | ||
it('should test Angular code with delay', fakeAsync(() => { | ||
const fakeObservable = of('fake value').pipe(delay(1000)); | ||
const fakeService = { | ||
getData() { | ||
return defer(() => of('fake data')); | ||
}, | ||
}; | ||
const fakeObservable = of('').pipe(switchMap(() => fakeService.getData())); | ||
const observerSpy = subscribeSpyTo(fakeObservable); | ||
fakeObservable.subscribe(observerSpy); | ||
tick(1000); | ||
await observerSpy.onComplete(); | ||
expect(observerSpy.getLastValue()).toEqual('fake value'); | ||
})); | ||
``` | ||
expect(observerSpy.getLastValue()).toEqual('fake data'); | ||
}); | ||
<br/> | ||
### ▶ RxJS + Promises: use `async` + `await` | ||
Since Promise(s) are [MicroTasks](https://javascript.info/microtask-queue), we should consider them to resolve asynchronously. | ||
For code using _Promise(s)_ **without timeouts or intervals**, just use `async` + `await` with the `onComplete()` method: | ||
```js | ||
// ... other imports | ||
import { subscribeSpyTo } from '@hirez_io/observer-spy'; | ||
it('should work with promises', async () => { | ||
const observerSpy: ObserverSpy<string> = new ObserverSpy(); | ||
@@ -247,3 +403,3 @@ const fakeService = { | ||
fakeObservable.subscribe(observerSpy); | ||
const observerSpy = subscribeSpyTo(fakeObservable); | ||
@@ -255,29 +411,16 @@ await observerSpy.onComplete(); | ||
it('should work with promises and "done()"', (done) => { | ||
const observerSpy: ObserverSpy<string> = new ObserverSpy(); | ||
``` | ||
const fakeService = { | ||
getData() { | ||
return Promise.resolve('fake data'); | ||
}, | ||
}; | ||
const fakeObservable = defer(() => fakeService.getData()); | ||
<br/> | ||
fakeObservable.subscribe(observerSpy); | ||
### ▶ RxJS Timers / Animations: use `fakeTime` | ||
observerSpy.onComplete(() => { | ||
expect(observerSpy.getLastValue()).toEqual('fake data'); | ||
done(); | ||
}); | ||
}); | ||
``` | ||
RxJS code that has time-based logic (e.g using timeouts / intervals / animations) will emit asynchronously. | ||
### ▶ For _time based_ rxjs code (timeouts / intervals / animations) - use `fakeTime` | ||
`fakeTime()` is a custom utility function that wraps the test callback which is perfect for most of these use-cases. | ||
`fakeTime` is a utility function that wraps the test callback. | ||
It does the following things: | ||
1. Changes the `AsyncScheduler` delegate to use `VirtualTimeScheduler` (which gives you the ability to use "virtual time" instead of having long tests) | ||
2. Passes a `flush` function you can call to `flush()` the virtual time (pass time forward) | ||
1. Changes the RxJS `AsyncScheduler` delegate to use `VirtualTimeScheduler` and use "virtual time". | ||
2. Passes a `flush()` function you can call whenever you want to virtually pass time forward. | ||
3. Works well with `done` if you pass it as the second parameter (instead of the first) | ||
@@ -289,14 +432,12 @@ | ||
// ... other imports | ||
import { ObserverSpy, fakeTime } from '@hirez_io/observer-spy'; | ||
import { subscribeSpyTo, fakeTime } from '@hirez_io/observer-spy'; | ||
it( | ||
'should handle delays with a virtual scheduler', | ||
fakeTime((flush) => { | ||
it('should handle delays with a virtual scheduler', fakeTime((flush) => { | ||
const VALUES = ['first', 'second', 'third']; | ||
const observerSpy: ObserverSpy<string> = new ObserverSpy(); | ||
const delayedObservable: Observable<string> = of(...VALUES).pipe(delay(20000)); | ||
const sub = delayedObservable.subscribe(observerSpy); | ||
flush(); | ||
sub.unsubscribe(); | ||
const observerSpy = subscribeSpyTo(delayedObservable); | ||
flush(); // <-- passes the "virtual time" forward | ||
@@ -307,12 +448,11 @@ expect(observerSpy.getValues()).toEqual(VALUES); | ||
it( | ||
'should handle be able to deal with done functionality as well', | ||
fakeTime((flush, done) => { | ||
// =============================================================================== | ||
it('should handle done functionality as well', fakeTime((flush, done) => { | ||
const VALUES = ['first', 'second', 'third']; | ||
const observerSpy: ObserverSpy<string> = new ObserverSpy(); | ||
const delayedObservable: Observable<string> = of(...VALUES).pipe(delay(20000)); | ||
const sub = delayedObservable.subscribe(observerSpy); | ||
const observerSpy = subscribeSpyTo(delayedObservable); | ||
flush(); | ||
sub.unsubscribe(); | ||
@@ -327,10 +467,35 @@ observerSpy.onComplete(() => { | ||
### ▶ For _ajax_ calls (http) - they shouldn't be tested in a unit / micro test anyway... 😜 | ||
<br/> | ||
Yeah. Test those in an integration test! | ||
### ▶ RxJS + _AJAX_ calls: | ||
# Wanna learn more? | ||
Asynchronous REST calls (using axios, http, fetch, etc.) should not be tested in a unit / micro test... Test those in an integration test! 😜 | ||
## In my [class testing In action course](http://testangular.com/?utm_source=github&utm_medium=link&utm_campaign=observer-spy) I go over all the differences and show you how to use this library to test stuff like `switchMap`, `interval` etc... | ||
<br/> | ||
<br/> | ||
# 🧠 Wanna become a PRO Observables tester? | ||
In [Angular Class Testing In action](http://testangular.com/?utm_source=github&utm_medium=link&utm_campaign=observer-spy) course Shai Reznik goes over all the differences and show you how to use observer spies to test complex Observable chains with `switchMap`, `interval` etc... | ||
<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 ✨ | ||
@@ -348,2 +513,4 @@ | ||
<td align="center"><a href="https://github.com/burkybang"><img src="https://avatars0.githubusercontent.com/u/927886?v=4" width="100px;" alt=""/><br /><sub><b>Adam Smith</b></sub></a><br /><a href="https://github.com/hirezio/observer-spy/commits?author=burkybang" title="Documentation">📖</a></td> | ||
<td align="center"><a href="https://github.com/katharinakoal"><img src="https://avatars3.githubusercontent.com/u/17751573?v=4" width="100px;" alt=""/><br /><sub><b>Katharina Koal</b></sub></a><br /><a href="https://github.com/hirezio/observer-spy/commits?author=katharinakoal" title="Code">💻</a> <a href="https://github.com/hirezio/observer-spy/commits?author=katharinakoal" title="Tests">⚠️</a> <a href="https://github.com/hirezio/observer-spy/commits?author=katharinakoal" title="Documentation">📖</a> <a href="#ideas-katharinakoal" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/hirezio/observer-spy/issues?q=author%3Akatharinakoal" title="Bug reports">🐛</a></td> | ||
<td align="center"><a href="http://www.linkedin.com/in/thomasburleson"><img src="https://avatars3.githubusercontent.com/u/210413?v=4" width="100px;" alt=""/><br /><sub><b>Thomas Burleson</b></sub></a><br /><a href="https://github.com/hirezio/observer-spy/commits?author=ThomasBurleson" title="Code">💻</a> <a href="https://github.com/hirezio/observer-spy/commits?author=ThomasBurleson" title="Tests">⚠️</a> <a href="https://github.com/hirezio/observer-spy/commits?author=ThomasBurleson" title="Documentation">📖</a> <a href="#ideas-ThomasBurleson" title="Ideas, Planning, & Feedback">🤔</a></td> | ||
</tr> | ||
@@ -354,5 +521,15 @@ </table> | ||
<!-- 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 | ||
export { ObserverSpy } from './observer-spy'; | ||
export { subscribeAndSpyOn, ObserverSpyWithSubscription } from './subscribe-and-spy-on'; | ||
export { ObserverSpyWithSubscription, SubscriberSpy } from './subscriber-spy'; | ||
export { subscribeSpyTo, subscribeAndSpyOn } from './subscribe-spy-to'; | ||
export { fakeTime } from './fake-time'; | ||
export { autoUnsubscribe, queueForAutoUnsubscribe } from './auto-unsubscribe'; |
@@ -6,5 +6,5 @@ import { Observable, of, throwError } from 'rxjs'; | ||
describe('ObserverSpy', () => { | ||
describe(`GIVEN observable emits 3 values and completes | ||
describe(`GIVEN the observable emits 3 values and completes | ||
WHEN subscribing`, () => { | ||
function getObservableWith3Values() { | ||
function getSpyAndObservableWith3Values() { | ||
const observerSpy: ObserverSpy<string> = new ObserverSpy(); | ||
@@ -22,3 +22,3 @@ const fakeValues: any[] = ['first', 'second', 'third']; | ||
it('should set receivedNext to true', () => { | ||
const { observerSpy, fakeObservable } = getObservableWith3Values(); | ||
const { observerSpy, fakeObservable } = getSpyAndObservableWith3Values(); | ||
@@ -31,3 +31,7 @@ fakeObservable.subscribe(observerSpy).unsubscribe(); | ||
it('should return the right values', () => { | ||
const { observerSpy, fakeObservable, fakeValues } = getObservableWith3Values(); | ||
const { | ||
observerSpy, | ||
fakeObservable, | ||
fakeValues, | ||
} = getSpyAndObservableWith3Values(); | ||
@@ -40,3 +44,3 @@ fakeObservable.subscribe(observerSpy).unsubscribe(); | ||
it('should return the values length of 3', () => { | ||
const { observerSpy, fakeObservable } = getObservableWith3Values(); | ||
const { observerSpy, fakeObservable } = getSpyAndObservableWith3Values(); | ||
@@ -49,3 +53,3 @@ fakeObservable.subscribe(observerSpy).unsubscribe(); | ||
it('should be able to return the correct first value', () => { | ||
const { observerSpy, fakeObservable } = getObservableWith3Values(); | ||
const { observerSpy, fakeObservable } = getSpyAndObservableWith3Values(); | ||
@@ -58,3 +62,3 @@ fakeObservable.subscribe(observerSpy).unsubscribe(); | ||
it('should be able to return the correct value at any index', () => { | ||
const { observerSpy, fakeObservable } = getObservableWith3Values(); | ||
const { observerSpy, fakeObservable } = getSpyAndObservableWith3Values(); | ||
@@ -67,3 +71,3 @@ fakeObservable.subscribe(observerSpy).unsubscribe(); | ||
it('should be able to return the correct last value', () => { | ||
const { observerSpy, fakeObservable } = getObservableWith3Values(); | ||
const { observerSpy, fakeObservable } = getSpyAndObservableWith3Values(); | ||
@@ -76,3 +80,3 @@ fakeObservable.subscribe(observerSpy).unsubscribe(); | ||
it('should know whether it got a "complete" notification', () => { | ||
const { observerSpy, fakeObservable } = getObservableWith3Values(); | ||
const { observerSpy, fakeObservable } = getSpyAndObservableWith3Values(); | ||
@@ -85,3 +89,3 @@ fakeObservable.subscribe(observerSpy).unsubscribe(); | ||
it('should be able to call a callback when it completes synchronously', (done) => { | ||
const { observerSpy, fakeObservable } = getObservableWith3Values(); | ||
const { observerSpy, fakeObservable } = getSpyAndObservableWith3Values(); | ||
@@ -97,3 +101,3 @@ fakeObservable.subscribe(observerSpy); | ||
it('should return a resolved promise when it completes synchronously', async () => { | ||
const { observerSpy, fakeObservable } = getObservableWith3Values(); | ||
const { observerSpy, fakeObservable } = getSpyAndObservableWith3Values(); | ||
@@ -107,3 +111,3 @@ fakeObservable.subscribe(observerSpy); | ||
it('should be able to call a callback when it completes asynchronously', (done) => { | ||
const { observerSpy, fakeObservable } = getObservableWith3Values(); | ||
const { observerSpy, fakeObservable } = getSpyAndObservableWith3Values(); | ||
@@ -119,3 +123,3 @@ fakeObservable.pipe(delay(1)).subscribe(observerSpy); | ||
it('should return a resolved promise when it completes asynchronously', async () => { | ||
const { observerSpy, fakeObservable } = getObservableWith3Values(); | ||
const { observerSpy, fakeObservable } = getSpyAndObservableWith3Values(); | ||
@@ -122,0 +126,0 @@ fakeObservable.pipe(delay(1)).subscribe(observerSpy); |
import { Observer } from 'rxjs'; | ||
export interface ObserverState { | ||
nextCalled: boolean; | ||
errorCalled: boolean; | ||
completeCalled: boolean; | ||
nextWasCalled: boolean; | ||
errorWasCalled: boolean; | ||
completeWasCalled: boolean; | ||
errorValue: any; | ||
@@ -14,6 +14,6 @@ onCompleteCallback: (() => void) | undefined; | ||
private observerState: ObserverState = { | ||
nextCalled: false, | ||
errorCalled: false, | ||
completeCalled: false, | ||
private state: ObserverState = { | ||
nextWasCalled: false, | ||
errorWasCalled: false, | ||
completeWasCalled: false, | ||
errorValue: undefined, | ||
@@ -25,14 +25,14 @@ onCompleteCallback: undefined, | ||
this.onNextValues.push(value); | ||
this.observerState.nextCalled = true; | ||
this.state.nextWasCalled = true; | ||
} | ||
error(errorVal: any): void { | ||
this.observerState.errorValue = errorVal; | ||
this.observerState.errorCalled = true; | ||
this.state.errorValue = errorVal; | ||
this.state.errorWasCalled = true; | ||
} | ||
complete(): void { | ||
this.observerState.completeCalled = true; | ||
if (this.observerState.onCompleteCallback) { | ||
this.observerState.onCompleteCallback(); | ||
this.state.completeWasCalled = true; | ||
if (this.state.onCompleteCallback) { | ||
this.state.onCompleteCallback(); | ||
} | ||
@@ -44,3 +44,3 @@ } | ||
onComplete(callback?: () => void) { | ||
if (this.observerState.completeCalled) { | ||
if (this.state.completeWasCalled) { | ||
return callback ? callback() : Promise.resolve(); | ||
@@ -50,3 +50,3 @@ } | ||
if (callback) { | ||
this.observerState.onCompleteCallback = callback; | ||
this.state.onCompleteCallback = callback; | ||
return; | ||
@@ -56,3 +56,3 @@ } | ||
return new Promise((resolve) => { | ||
this.observerState.onCompleteCallback = resolve; | ||
this.state.onCompleteCallback = resolve; | ||
}); | ||
@@ -82,16 +82,16 @@ } | ||
receivedNext(): boolean { | ||
return this.observerState.nextCalled; | ||
return this.state.nextWasCalled; | ||
} | ||
getError(): any { | ||
return this.observerState.errorValue; | ||
return this.state.errorValue; | ||
} | ||
receivedError(): boolean { | ||
return this.observerState.errorCalled; | ||
return this.state.errorWasCalled; | ||
} | ||
receivedComplete(): boolean { | ||
return this.observerState.completeCalled; | ||
return this.state.completeWasCalled; | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
283430
57
731
525