Giter Site home page Giter Site logo

hirezio / observer-spy Goto Github PK

View Code? Open in Web Editor NEW
368.0 368.0 13.0 3 MB

This library makes RxJS Observables testing easy!

License: MIT License

JavaScript 5.38% TypeScript 93.98% Shell 0.64%
angular jasmine jest marble-tests microtest mock mocks observables observer-spies rxjs spies testing unit-tests

observer-spy's People

Contributors

allcontributors[bot] avatar armanozak avatar burkybang avatar dependabot[bot] avatar edbzn avatar jasonlandbridge avatar katharinakoal avatar petrzjunior avatar shairez avatar thomasburleson avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

observer-spy's Issues

Jest matchers

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();

});

is there a method to count the number of times the source observable has emitted?

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?

Promise support for `onComplete`

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?

Requires ESM module transformation

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.

import { AsyncScheduler } from 'rxjs/internal/scheduler/AsyncScheduler';

Support for RxJS 7

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'

Await .onFirstEmitted() and .onEmitted(n); of observable values and then continue

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!

DISCUSSION 3: `subscribeAndSpyOn` + `unsubscribe` VS `spyOn` and `dispose`

CONTEXT:

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.

The suggested API changes to 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$];
}

Thomas' points:

  • spyOn is shorter and cleaner
  • dispose() relates to the spy, not the subscription. (you shouldn't care about what happens behind the scenes)
  • tuples are better because they let you name your own variables during the array destructuring.
  • You separate responsibilities and leave the spy to handle spying alone.

Shai's points:

  • just the name 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.
  • Because of jasmine's spyOn function, when scanning the test, people might confuse the two and not realize there's a subscription involved here.
  • I always prefer to return properties over tuples, especially if I want to keep things consistent between lots of tests. It also helps to understand the API better IMO.
  • I prefer the name 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.

SO... WHAT DO YOU THINK?

Improve .receivedNext()

Use Case / Problem

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.)

Expected Behaviour

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.)

Possible Solutions

  1. Change the behaviour of .receivedNext()
  2. Add a optional parameter. I.e. .receivedNext({ sinceLastTimeChecked: true }) - It would be nice if I could configure the lib in a way that this is the default.
  3. Make it a new function .receivedNextSinceLastTimeChecked() - This is my least favorite option

Work Around

I 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.

Additional context

I am using angular v12. RxJS v6 and @hirez_io/observer-spy v2.2.

Simplify usage with factory function and internal subscription handling

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

The automated release is failing 🚨

🚨 The automated release from the 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.


Invalid npm token.

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 📦🚀

Make .expectErrors() return this

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);

subscribeSpyTo<T> returns SubscriberSpy<unknown> instead of SubscriberSpy<T>

Describe the bug
subscribeSpyTo<T> returns SubscriberSpy<unknown> instead of SubscriberSpy<T>

To Reproduce

  1. Create a new angular project
  2. Install @hirez_io/observer-spy
  3. Use subscribeSpyTo to test a string observable, for example - of('str')
  4. See that it returns SubscriberSpy<unknown> instead of SubscriberSpy<string>

Expected behavior
It should return SubscriberSpy<string> instead of SubscriberSpy<unknown>

Screenshots
Bug -
image
Source code for subscribeSpyTo -
image

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

Package Version

@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

subscribeSpyTo is causing the ''ERROR TypeError: You provided 'undefined' where a stream was expected'' with the RxJS 7 upgrade

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?

DISCUSSION 2: ObserverSpy API - passing `OnComplete` callback in the constructor?

CONTEXT:

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.

The suggested API changes to 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);
	  }

...

The upsides (IMO) -

  • You define it one time and cannot change it afterwards (might reduce confusion?)

The downsides (IMO) -

  • It forces you to write your expectations before you trigger your action, meaning your test could end up looking like 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.
});
  • It introduces 2 ways to do 1 thing, which is something I'd like to avoid to keep the library as simple as possible so people won't have to guess "what's the best way".
    Plus, if you keep the 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.

SO... WHAT DO YOU THINK?

DISCUSSION 1: ObserverSpy API - Methods vs getters

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.

The suggested API changes to 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;
  }

The upsides (IMO) -

  • 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...)

The downsides (IMO) -

  • State object usage might make the expectation longer and requires remembering more of api.
    For example:
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) -

WHAT ARE YOUR THOUGHTS?

`ObserverSpy` swallows errors

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:

  1. throw an error in an observable
  2. See no trace of it in the logs

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.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.