hirezio / observer-spy Goto Github PK
View Code? Open in Web Editor NEWThis library makes RxJS Observables testing easy!
License: MIT License
This library makes RxJS Observables testing easy!
License: MIT License
Is your feature request related to a problem? Please describe.
The extra isEqual
and etc can make the test longer, if there were a custom matchers it will be much cleaner.
Describe the solution you'd like
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:
// .getFirstValue()
expect(observerSpy).toFirstEmit('first');
// .receivedNext()
expect(observerSpy).toHaveBeenReceivedNext();
// .getValues()
expect(observerSpy).toEmitValues(fakeValues);
// .getValuesLength()
expect(observerSpy).toEmitValuesOfSize(3);
// .getValueAt()
expect(observerSpy).toEmitValueAt(1, 'second');
// .getLastValue()
expect(observerSpy).toLastEmit('third');
// .receivedComplete()
expect(observerSpy).toComplete();
});
For example in the testing debounce,
I would like to know how many times the source observable has emitted.
i have this example:
const observerSpy = subscribeSpyTo(component.posts$ as Observable<Post[]>);
component.searchTermSubject.next('t');
tick(100);
component.searchTermSubject.next('te');
tick(300);
component.searchTermSubject.next('test');
tick(300);
observerSpy.unsubscribe();
// sample method
expect(observableSpy.getEmissionCount()).toEqual(2);
Would that be possible?
Hi,
It could simplify the code if we can use the async await syntax with onComplete
method instead of a callback.
it('should work with promises', async () => {
const observerSpy: ObserverSpy<string> = new ObserverSpy();
const fakeService = {
getData() {
return Promise.resolve('fake data');
},
};
const fakeObservable = of('').pipe(switchMap(() => fakeService.getData()));
fakeObservable.subscribe(observerSpy);
await observerSpy.onComplete();
expect(observerSpy.getLastValue()).toEqual('fake data');
});
It removes the need to use Jest done
fn and looks more synchronous, what do you think?
When trying to import this into a @nrwl/nx
configured workspace I get the following error:
● Test suite failed to run
Jest encountered an unexpected token
Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript syntax, or when Jest is not configured to support such syntax.
Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel configuration.
By default "node_modules" folder is ignored by transformers.
Here's what you can do:
• If you are trying to use ECMAScript Modules, see https://jestjs.io/docs/ecmascript-modules for how to enable it.
• To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config.
• If you need a custom transformation specify a "transform" option in your config.
• If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option.
You'll find more details and examples of these config options in the docs:
https://jestjs.io/docs/configuration
For information about custom transformations, see:
https://jestjs.io/docs/code-transformation
Details:
/Volumes/MacOS-Data/Home/wsedlacek/Devolpement/angular/brandingbrand/node_modules/rxjs/dist/types/internal/scheduler/AsyncScheduler.d.ts:1
({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,jest){import { Scheduler } from '../Scheduler';
^^^^^^
SyntaxError: Cannot use import statement outside a module
at Runtime.createScriptFromCode (../../../node_modules/jest-runtime/build/index.js:1679:14)
at Object.<anonymous> (../../../node_modules/@hirez_io/observer-spy/src/fake-time.ts:2:1)
It appears that using this internal rxjs
import results in an invalid import in some enviorments.
Line 2 in 00ee48a
Is your feature request related to a problem? Please describe.
Support for RxJS 7, currently we have the following error message when trying to use the lib:
Cannot find module 'rxjs/internal/scheduler/AsyncScheduler' from 'node_modules/@hirez_io/observer-spy/dist/fake-time.js'
First off, huge fan of observer-spy! What a life-saver!
Is your feature request related to a problem? Please describe.
Comparable to .onComplete()
and .onError()
, it would be very useful to have something that awaits the first or n value emitted by an observable before continuing. This is because not all observables should be completing and need to stay alive. Observable Store is a good example of this where I would like a (Vue) component to keep being updated when ever the store is updated, which means I don't want to complete the observable.
Describe the solution you'd like
Something like this maybe:
const test$ = of([0,1,2,3,4,5,6,7,8,9,10]);
const _test= subscribeSpyTo(test$);
await _test.onFirstEmitted();
expect(_test.getFirstValue()).toEqual(0);
await _test.onEmitted(5);
expect(_test.getValues()).toEqual([0,1,2,3,4]);
Describe alternatives you've considered
Continuing from the example above:
const test$ = of([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
const testTakeOne$ = test$.pipe(take(1));
const _testTakeOne = subscribeSpyTo(testTakeOne$);
await _testTakeOne.onComplete();
expect(_testTakeOne.getFirstValue()).toEqual(0);
const testTakeFive$ = test$.pipe(take(5));
const _testTakeFive = subscribeSpyTo(testTakeFive$);
await _testTakeFive.onComplete();
expect(_testTakeFive.getValues()).toEqual([0, 1, 2, 3, 4]);
Additional context
So before making this post I thought this couldn't be done but by rubber ducking I realized that it could be done, albeit in a convoluted way. If nothing else, I could most likely write an extension method for it.
Thanks for reading and please like and subscribe if you want more feature requests!
Following #12 (by the amazing @ThomasBurleson) we had a discussion and realized this PR could be separated into several PRs, and before that we need to discuss them.
This one is about: changing the factory method and functionality of ObserverSpyWithSubscription
.
ObserverSpy
-export type SpyOnResults<T> = [ObserverSpy<T>, () => void, Observable<any>];
export function spyOn<T>(
source$: Observable<T>,
completionCallback?: CompletionCallback
): SpyOnResults<T> {
const spy = new ObserverSpy<T>(completionCallback);
const subscription = source$.subscribe(spy);
const dispose = () => {
subscription.unsubscribe();
removeSpyFromWatchList(spy);
};
subscriptions.push([spy, dispose, source$]);
return [spy, dispose, source$];
}
spyOn
is shorter and cleanerdispose()
relates to the spy, not the subscription. (you shouldn't care about what happens behind the scenes)spyOn
hides the fact that there is a subscription happening behind the scenes, and because the subscription IS the action of the test, it's an important information to convey. subscribeAndSpyOn
shows what actually happens behind the scenes better IMO.spyOn
function, when scanning the test, people might confuse the two and not realize there's a subscription involved here.unsubscribe
over dispose
because it's a term + functionality people already know.I'd love to hear your thoughts / corrections @ThomasBurleson @katharinakoal @edbzn @burkybang @yjaaidi (and whoever is interested to share their opinion), before we decide whether to break it into its own PR.
I am using jest to write a test for a angular 12 component. I would like to use receivedNext()
in a specific way, but it doesn't do what I would expect.
Here is an example:
// I create a spy to a observable, which emits some values when submit$ emits but
// ONLY WHEN service returns something.
jest.spyOn(mockService, 'load').mockReturnValue(of(values));
const valuesSpy = subscribeSpyTo(component.values$);
// submit
component.submit$.next();
tick();
expect(valuesSpy.receivedNext()).toBe(true); // works until here
// now I want to submit again, but with no values being returned
jest.spyOn(mockService, 'load').mockReturnValue(of(null));
// now submit again
component.submit$.next();
tick();
expect(valueSpy.receivedNext()).toBe(false); // fails, but I would expect that it passes, since it did not emit since the last tick() or the last time I checked.
(The component which is tested here, is implemented declaratively.)
I would expect that receivedNext()
would indicate if the observable was "nexted" again AFTER the last time I checked.
So once receivedNext()
is called, it resets it. For example when called directly after I would assume that it means "received next two times in a row":
expect(valueSpy.receivedNext()).toBe(true); // passes
expect(valueSpy.receivedNext()).toBe(true); // fails
It would also be awesome if it would "reset" when using tick
(inside fakeAsync()
). "Received next since the last time something changed". For exmaple:
// check/emit something but don't use receivedNext here
// then do something again
tick(); // tick to forward in time which should reset receivedNext
expect(valueSpy.receivedNext()).toBe(false); // works, since it did not emit since the last tick()
(I could use this in my use case example, to only check at the end that it was not emitted.)
.receivedNext()
.receivedNext({ sinceLastTimeChecked: true })
- It would be nice if I could configure the lib in a way that this is the default..receivedNextSinceLastTimeChecked()
- This is my least favorite optionI know it is easily possible to use .getValues()
or .getValuesLength()
to check if something emitted again but for that I need to know how often it already emitted, which can change while updating the test. Therefore it would be nicer to use .receivedNext()
instead.
I am using angular v12. RxJS v6 and @hirez_io/observer-spy v2.2.
Hey Shai!
Thanks for this! It makes basic observable testing a lot easier!
I replaced almost all marble tests in my current project using observer spies, and I do love them. When you don't have to make use of the complexity which comes with marble testing, observer spies are a real improvement in terms of clarity and readability.
However, I found the few lines to use them still a bit too implementation heavy ;) I know this is a matter of taste, so please consider this more like a feature proposal than a feature request.
Is your feature request related to a problem? Please describe.
Letting test authors handle subscriptions on their own leads to a bit too much boilerplate code.
Describe the solution you'd like
I ended up using observer spies like this:
const fakeObservable = of('first', 'second', 'third');
const spy = spyOnObservable(fakeObservable);
// if the observable under test will not complete
// or we just want the spy to stop listening
// this will internally unsubscribe
spy.stop();
// simplified shorthand usage
expect(spyOnObservable(fakeObservable).getFirstValue()).toBe('first');
For that to work, I extended the ObserverSpy Class to handle the subscription itself and added a stop method to give test authors the possibility to clear subscriptions:
class StoppableObserverSpy<T> extends ObserverSpy<T> {
private observationStopped = new AsyncSubject<never>();
constructor(observableUnderTest: Observable<T>) {
super();
observableUnderTest
.pipe(
finalize(() => this.stop()),
takeUntil(this.observationStopped)
)
.subscribe(this);
}
public stop(): void {
if (!this.observationStopped.isStopped) {
this.observationStopped.complete();
this.observationStopped.unsubscribe();
}
}
}
Additionally I use this simple factory function:
function spyOnObservable<T>(observableUnderTest: Observable<T>) {
return new StoppableObserverSpy<T>(observableUnderTest);
}
Describe alternatives you've considered
Maybe the usage of observer spies could be simplified even more by creating custom jasmine/jest matchers, but I have no experience there.
Additional context
I guess the proposed modifications could be applied to this library without introducing breaking changes. If you think that these changes would be useful, I'd love to create a PR for discussing implementation details.
Cheers,
kat
master
branch failed. 🚨I recommend you give this issue a high priority, so other packages depending on you could benefit from your bug fixes and new features.
You can find below the list of errors reported by semantic-release. Each one of them has to be resolved in order to automatically publish your package. I’m sure you can resolve this 💪.
Errors are usually caused by a misconfiguration or an authentication problem. With each error reported below you will find explanation and guidance to help you to resolve it.
Once all the errors are resolved, semantic-release will release your package the next time you push a commit to the master
branch. You can also manually restart the failed CI job that runs semantic-release.
If you are not sure how to resolve this, here is some links that can help you:
If those don’t help, or if this issue is reporting something you think isn’t right, you can always ask the humans behind semantic-release.
The npm token configured in the NPM_TOKEN
environment variable must be a valid token allowing to publish to the registry https://registry.npmjs.org/
.
If you are using Two-Factor Authentication, make configure the auth-only
level is supported. semantic-release cannot publish with the default auth-and-writes
level.
Please make sure to set the NPM_TOKEN
environment variable in your CI with the exact value of the npm token.
Good luck with your project ✨
Your semantic-release bot 📦🚀
Is your feature request related to a problem? Please describe.
I really enjoy the concise way of writing single-expect tests
expect(subscribeSpyTo(observable, { expectErrors: true }).getError()).toStrictEqual(error);
and this method mentioned is README is also very useful
// BTW, this could also be set like this:
observerSpy.expectErrors(); // <-- ALTERNATIVE WAY TO SET IT
so I was thinking, why not to combine them?
Describe the solution you'd like
I suggest that ObserverSpy.expectErrors()
would return this
expectErrors(): this {
this.state.errorIsExpected = true;
return this;
}
so that it is possible to write
expect(subscribeSpyTo(observable).expectErrors().getError()).toStrictEqual(error);
Describe the bug
subscribeSpyTo<T>
returns SubscriberSpy<unknown>
instead of SubscriberSpy<T>
To Reproduce
of('str')
SubscriberSpy<unknown>
instead of SubscriberSpy<string>
Expected behavior
It should return SubscriberSpy<string>
instead of SubscriberSpy<unknown>
Screenshots
Bug -
Source code for subscribeSpyTo -
Desktop (please complete the following information):
Angular CLI: 13.1.2
Node: 14.18.2
Package Manager: npm 6.14.15
OS: linux x64
Angular: 13.1.1
... animations, common, compiler, compiler-cli, core, forms
... platform-browser, platform-browser-dynamic, router
@angular-devkit/architect 0.1301.2
@angular-devkit/build-angular 13.1.2
@angular-devkit/core 13.1.2
@angular-devkit/schematics 13.1.2
@angular/cli 13.1.2
@schematics/angular 13.1.2
rxjs 7.4.0
typescript 4.5.4
I am getting the following error with the subscribeSpyTo import with the RxJS 7
import { subscribeSpyTo } from '@hirez_io/observer-spy';
ERROR TypeError: You provided 'undefined' where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.
ERROR TypeError: You provided 'undefined' where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.
at Object.createInvalidObservableTypeError (/node_modules/rxjs/src/internal/util/throwUnobservableError.ts:7:10)
Can you please advise on this?
Following #12 (by the amazing @ThomasBurleson) we had a discussion and realized this PR could be separated into several PRs, and before that we need to discuss them.
This one is about: the possible changes to the ObserverSpy API to make it more "getters" focused.
ObserverSpy
-export type CompletionCallback = (spy: ObserverSpy<any>) => void;
const NOOP = () => {};
export class ObserverSpy<T> implements Observer<T> {
constructor(private onCompleteCallback: CompletionCallback = NOOP) {}
...
complete(): void {
this._state.called.complete = true;
this.onCompleteCallback(this);
}
...
it('should do something', ()=>{
// SETUP
const observerSpy = new ObserverSpy((spy)=>{
// OUTCOME
expect(spy.getValuesLength()).toBe(2);
})
// ACTION
observableUnderTest.subscribe(observerSpy);
// I personally don't like this style, I prefer to follow the "setup, action, outcome" structure in that order
// because it keeps the tests more readable and predictable IMO.
});
onComplete
method, you then have another discussion to have - "what happens when you define both callbacks? do they co-exist or not?"I'd love to hear your thoughts / corrections @ThomasBurleson @katharinakoal @edbzn @burkybang @yjaaidi (and whoever is interested), before we decide whether to break it into its own PR.
Following #12 by the amazing @ThomasBurleson we had a discussion and realized this PR could be separated into several PRs, and before that we need to discuss them.
This discussion is about the possible changes to the ObserverSpy API to make it more "getters" focused.
ObserverSpy
-private _state: ObserverState<T> = {
values: [],
errorValue: undefined,
called: {
next: false,
error: false,
complete: false,
},
};
get state(): ObserverState<T> {
return {
...this._state,
values: [...this._state.values],
};
}
get values(): T[] {
return this._state.values;
}
get hasValues(): boolean {
return this._state.values.length > 0;
}
get isComplete(): boolean {
return this._state.called.complete;
}
readFirst(): T | undefined {
return this.hasValues ? this._state.values[0] : undefined;
}
readLast(): T | undefined {
return this.hasValues ? this._state.values[this._state.values.length - 1] : undefined;
}
Read only values (that's a good idea to implement regardless of the api)
Reduces the need to call a method which might make it shorter and more straightforward to read the state of the spy
(probably more that I'm missing...)
State
object usage might make the expectation longer and requires remembering more of api.expect(observerSpy.state.called.next).toBeTruthy();
// VS
expect(observerSpy.receivedNext()).toBeTruthy();
// AND
expect(observerSpy.getError()).toBe('OMG AN ERROR!');
// VS
expect(observerSpy.state.errorValue).toBe('OMG AN ERROR!');
I prefer to reduce the nesting, so If we'll decide to go for getters, I would suggest flattening the api of "state" to exposed from the root, something like observerSpy.errorValue
etc)
Intuitively (for me), methods feel (at first glance) more "Read only" than properties, but maybe it's just me.
It's true that with TypeScript you'll see a compile time error if you try to assign values
some other value, but with JS you'll get a run time error which is no fun.
It might be tempting for developers in some scenarios to try and do observerSpy.values[2] = 'some other value';
, but then again, maybe I'm wrong here.
Because this is a BREAKING_CHANGE, I wanted to get as much opinions as possible about it, before we decide whether we should break it into it's own PR.
So @ThomasBurleson @katharinakoal @edbzn @burkybang @yjaaidi (and anyone else who'd like to chime in) -
Describe the bug
When an error is thrown unexpectedly, the error
method on the ObserverSpy
catches it and basically "hides" it from the user.
And by that making debugging an unexpected error much more difficult.
To Reproduce
Steps to reproduce the behavior:
Expected behavior
We should be able to configure the observer spy (on creation or via subscribeSpyTo
) to flag whether an error is expected or not.
The default should be that the error is not expected and should be re-thrown so it would be shown in the console.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.