Security News
Research
Data Theft Repackaged: A Case Study in Malicious Wrapper Packages on npm
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
@hirez_io/observer-spy
Advanced tools
A simple little class that helps making Observable testing a breeze
This library makes RxJS Observables testing easy!
yarn add -D @hirez_io/observer-spy
or
npm install -D @hirez_io/observer-spy
Especially when testing advanced use cases.
Until this library, the common way to test observables was to use 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 and to maintain.
The Observer-Spy library was created to present a viable alternative to Marble Tests.
An alternative which we believe is:
✅ Easier to understand
✅ Reduces the complexity
✅ Makes observables tests cleaner
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);
})
});
import { subscribeSpyTo } from '@hirez_io/observer-spy';
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 observerSpy = subscribeSpyTo(result$);
expect(observerSpy.getValues()).toEqual([10, 30, 50, 70, 90]);
})
});
You generally want to test the outcome of your action instead of implementation details [like how many frames were between each value].
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)
const observerSpy = subscribeSpyTo(observable)
In order to test Observables you can use the subscribeSpyTo
function:
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);
// --------------------------------------------------------
// You can also use this shorthand version:
expect(subscribeSpyTo(fakeObservable).getFirstValue()).toEqual('first');
// --------------------------------------------------------
});
onComplete
before expecting the result (using async
+ await
)it('should support async await for onComplete()', async () => {
const fakeObservable = of('first', 'second', 'third');
const observerSpy = subscribeSpyTo(fakeObservable);
// 👇
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);
// }));
});
receivedError
and getError
expectErrors
to catch errors!This 👆 is to avoid swallowing unexpected errors (more details here)
it('should spy on Observable errors', () => {
const fakeObservable = throwError('FAKE ERROR');
const observerSpy = subscribeSpyTo(fakeObservable, {expectErrors: true});
expect(observerSpy.receivedError()).toBe(true);
expect(observerSpy.getError()).toEqual('FAKE ERROR');
});
onError
before expecting the result (using async
+ await
)it('should support async await for onError()', async () => {
const fakeObservable = throwError('FAKE ERROR');
const observerSpy = subscribeSpyTo(fakeObservable, {expectErrors: true});
// 👇
await observerSpy.onError(); // <-- the test will pause here until the observer receive the error
expect(observerSpy.getError()).toEqual('FAKE ERROR');
});
new ObserverSpy()
You can create an ObserverSpy
instance manually:
// ... other imports
import { ObserverSpy } from '@hirez_io/observer-spy';
it('should spy on Observable values', () => {
const fakeValues = ['first', 'second', 'third'];
const fakeObservable = of(...fakeValues);
// BTW, if you're using TypeScript you can declare it with a generic:
// const observerSpy: ObserverSpy<string> = new ObserverSpy();
const observerSpy = new ObserverSpy();
// 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);
// ...DO SOME LOGIC HERE...
// unsubscribing is optional, it's good for stopping intervals etc
subscription.unsubscribe();
expect(observerSpy.getValuesLength()).toEqual(3);
});
If you need to spy on errors, make sure to set the expectErrors
property:
it('should spy on Observable errors', () => {
const fakeObservable = throwError('OOPS');
const observerSpy = new ObserverSpy({expectErrors: true}); // <-- IMPORTANT
// OR
const observerSpy = new ObserverSpy().expectErrors(); // <-- ALTERNATIVE WAY TO SET IT
// Or even...
observerSpy.expectErrors(); // <-- ALTERNATIVE WAY TO SET IT
fakeObservable.subscribe(observerSpy);
expect(observerSpy.receivedError()).toBe(true);
});
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()
.
You should only call autoUnsubscribe()
once per environment (not manually after each test!). You do it in your testing environment setup files (like jest.config.js
or test.ts
in Angular).
This works only with subscriptions created using either subscribeSpyTo()
or queueForAutoUnsubscribe()
.
Currently it only works with frameworks like Jasmine, Mocha and Jest (because they have a global afterEach
function)
This library provide helper functions to help you configure this behavior -
setup-auto-unsubscribe.js
This requires Jest to be loaded and then calls autoUnsubscribe()
which sets up a global / root afterEach
function that unsubscribes from your observer spies.
Add this to your jest configuration (i.e jest.config.js
):
{
setupFilesAfterEnv: ['node_modules/@hirez_io/observer-spy/dist/setup-auto-unsubscribe.js'],
}
autoUnsubscribe
This will add a root level afterEach()
once that auto unsubscribes observer spies.
Add this to your test.ts
-
// test.ts
// ~~~~~~~
import { autoUnsubscribe } from '@hirez_io/observer-spy';
autoUnsubscribe();
queueForAutoUnsubscribe
If you configured your environment to "autoUnsubscribe" and want your manually created spies (via new ObserverSpy()
) to be "auto unsubscribed" as well, you can use queueForAutoUnsubscribe(subscription)
.
It accepts any Unsubscribable
object which has an unsubscribe()
method -
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);
// ... rest of the test
});
This will ensure your manually created spies are auto unsubscribed at the end of each test.
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.
it('should run synchronously', () => {
const observerSpy = subscribeSpyTo(from(['first', 'second', 'third']));
expect(spy.getValuesLength()).toBe(3);
});
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) -
fakeAsync
With Angular, you can control time in a much more versatile way.
Just use fakeAsync
(and tick
if you need it):
// ... other imports
import { subscribeSpyTo } from '@hirez_io/observer-spy';
import { fakeAsync, tick } from '@angular/core/testing';
it('should test Angular code with delay', fakeAsync(() => {
const fakeObservable = of('fake value').pipe(delay(1000));
const observerSpy = subscribeSpyTo(fakeObservable);
tick(1000);
expect(observerSpy.getLastValue()).toEqual('fake value');
}));
async
+ await
Since Promise(s) are MicroTasks, we should consider them to resolve asynchronously.
For code using Promise(s) without timeouts or intervals, just use async
+ await
with the onComplete()
method:
// ... other imports
import { subscribeSpyTo } from '@hirez_io/observer-spy';
it('should work with promises', async () => {
const fakeService = {
getData() {
return Promise.resolve('fake data');
},
};
const fakeObservable = defer(() => fakeService.getData());
const observerSpy = subscribeSpyTo(fakeObservable);
await observerSpy.onComplete();
expect(observerSpy.getLastValue()).toEqual('fake data');
});
fakeTime
RxJS code that has time-based logic (e.g using timeouts / intervals / animations) will emit asynchronously.
fakeTime()
is a custom utility function that wraps the test callback which is perfect for most of these use-cases.
It does the following things:
AsyncScheduler
delegate to use VirtualTimeScheduler
and use "virtual time".flush()
function you can call whenever you want to virtually pass time forward.done
if you pass it as the second parameter (instead of the first)Example:
// ... other imports
import { subscribeSpyTo, fakeTime } from '@hirez_io/observer-spy';
it('should handle delays with a virtual scheduler', fakeTime((flush) => {
const VALUES = ['first', 'second', 'third'];
const delayedObservable: Observable<string> = of(...VALUES).pipe(delay(20000));
const observerSpy = subscribeSpyTo(delayedObservable);
flush(); // <-- passes the "virtual time" forward
expect(observerSpy.getValues()).toEqual(VALUES);
})
);
// ===============================================================================
it('should handle done functionality as well', fakeTime((flush, done) => {
const VALUES = ['first', 'second', 'third'];
const delayedObservable: Observable<string> = of(...VALUES).pipe(delay(20000));
const observerSpy = subscribeSpyTo(delayedObservable);
flush();
observerSpy.onComplete(() => {
expect(observerSpy.getValues()).toEqual(VALUES);
done();
});
})
);
Asynchronous REST calls (using axios, http, fetch, etc.) should not be tested in a unit / micro test... Test those in an integration test! 😜
In Angular Class Testing In action 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...
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 🙏
Be kind to each other and please read our code of conduct.
Thanks goes to these wonderful people (emoji key):
Shai Reznik 💻 ⚠️ 🚇 📖 🚧 👀 🤔 | Edouard Bozon 💻 ⚠️ 📖 🤔 | Adam Smith 📖 | Katharina Koal 💻 ⚠️ 📖 🤔 🐛 | Thomas Burleson 💻 ⚠️ 📖 🤔 | Levent Arman Özak 📖 |
This project follows the all-contributors specification. Contributions of any kind welcome!
MIT
FAQs
A simple little class that helps making Observable testing a breeze
The npm package @hirez_io/observer-spy receives a total of 45,948 weekly downloads. As such, @hirez_io/observer-spy popularity was classified as popular.
We found that @hirez_io/observer-spy demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Research
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
Research
Security News
Attackers used a malicious npm package typosquatting a popular ESLint plugin to steal sensitive data, execute commands, and exploit developer systems.
Security News
The Ultralytics' PyPI Package was compromised four times in one weekend through GitHub Actions cache poisoning and failure to rotate previously compromised API tokens.