Unit testing rxjs in Angular [duplicate] - javascript

This question already has answers here:
Angular unit test combineLatest
(2 answers)
Closed last year.
Im having a problem writing unit tests for observable in Angular... Im trying to test cases if displayPaymentsLineItem$ will be true or false depending on the values of
mobileAccountBalance$, and selectedMobileDigitalBill$... Can anyone help?
public selectedMobileDigitalBill$: Observable<MobileDigitalBill>;
public mobileAccountBalance$: Observable<MobileAccountBalance>;
private expandedLinesMap: object = {};
private expandedTaxesAndFees: boolean = false;
private devices: MobileDevice[] = [];
private destroy$: Subject<void> = new Subject();
constructor(]
private mobileStatementsTabFacade: MobileStatementsTabFacade,
private billingMobileFacade: BillingMobileFacade,
) {}
ngOnInit() {
this.mobileAccountBalance$ = this.mobileStatementsTabFacade.mobileAccountBalance$;
this.displayPaymentsLineItem$ = combineLatest([
this.mobileAccountBalance$,
this.selectedMobileDigitalBill$,
]).pipe(
map(([mobileAccountBalance, selectedMobileDigitalBill]: [MobileAccountBalance, MobileDigitalBill]) => {
const isPastDue: boolean = mobileAccountBalance?.pastdue > 0;
const hasPayments: boolean = selectedMobileDigitalBill?.payments?.length > 0;
return isPastDue && hasPayments;
})
);
}
});

You can take(1) (take one value, then complete) and subscribe to test if the emitted value is falsy. Observables need to be completed if you test them this way.
describe('The display payment line items observable', () => {
it('should emit truthy', () => {
displayPaymentsLineItem$
.pipe(take(1))
.subscribe(value =>{
expect(value).toBeTruthy();
});
}
}
That being said, displayPaymentsLineItem$ won't emit anything if the two observables inside combineLatest() aren't defined in your test. Since they come from two facades, they may need to be provided before starting your test.
Also, about your code example:
displayPaymentsLineItem$ isn't declared before the constructor.
selectedMobileDigitalBill$ is declared but is never defined before it is referenced inside combineLatest().

Related

Avoid subscribing in Angular Service using RxJS?

I am working on an angular project that needs to load up pages and then display them one by one / two by two.
As per this article and some other sources, subscribing in services is almost never necessary. So is there a way to rewrite this in pure reactive style using RxJS operators?
Here's what I have (simplified) :
export class NavigationService {
private pages: Page[] = [];
private mode = Mode.SinglePage;
private index = 0;
private currentPages = new BehaviorSubject<Page[]>([]);
constructor(
private pageService: PageService,
private view: ViewService,
) {
this.pageService.pages$.subscribe(pages => {
this.setPages(pages);
});
this.view.mode$.subscribe(mode => {
this.setMode(mode);
});
}
private setPages(pages: Page[]) {
this.pages = pages;
this.updateCurrentPages();
}
private setMode(mode: Mode) {
this.mode = mode;
this.updateCurrentPages();
}
private updateCurrentPages() {
// get an array of current pages depending on pages array, mode & index
this.currentPages.next(...);
}
public goToNextPage() {
this.index += 1;
this.updateCurrentPages();
}
public get currentPages$() {
return this.currentPages.asObservable();
}
}
I've tried multiple solutions and didn't manage to get it right. The closest I got was using scan(), but it always reset my accumulated value when the outer observables (pages, mode) got updated.
Any help is appreciated, thanks !
You can use merge to create reducer functions from observables. These functions will update part of a state maintained by the service. They are past along to the scan operator which will update the prior state from the reducer. After the reducer is run, currentPages is set on the new state and that new state is returned.
export class NavigationService {
private readonly relativePageChangeSubject = new Subject<number>();
readonly state$ = merge(
this.pageService.pages$.pipe(map((pages) => (vm) => ({ ...vm, pages }))),
this.relativePageChangeSubject.pipe(map((rel) => (vm) => ({ ...vm, index: vm.index + rel }))),
this.view.mode$.pipe(map((mode) => (vm) => ({ ...vm, mode })))
).pipe(
startWith((s) => s), // if necessary, force an initial value to be emitted from the initial value in scan.
scan((s, reducer) => {
const next = reducer(s);
// update currentPages on the next state here.
return next;
}, { currentPages: [], index: 0, mode: Mode.SinglePage, pages: [] }),
shareReplay(1)
)
readonly currentPages$ = this.state$.pipe(
map(x => x.currentPages),
distinctUntilChanged()
);
constructor(private pageService: PageService, private view: ViewService) { }
goToNextPage() {
this.relativePageChangeSubject.next(1);
}
}
Notes:
Instead of having a nextPage Subject, a more flexible relative change subject is used that will modify the index from the value in the prior state.
The currentPage$ observable isn't necessary, as a consumer could just attach to the main state$ and map as needed. Feel free to make state$ private or remove currentPage$.
Let's first detail what you are doing:
You subscribe to pageService.pages$ and view.mode$
Those subscriptions take the values and put them in a private variabkle
Then fire a function to use those two variables
Finally, trigger a value push.
All this can be done in a simple pipeline. You'd need to include the index as an observable (behaviour subject in our case) to react to that change too.
Use combineLatest, this will subscribe to all observables we want, and trigger the pipe WHEN ALL have fired once, and every time one changes afterwards. You may want to use .pipe(startWith("something")) on observables that should have a default value so your observable pipe triggers asap.
CombineLatest will then provide an object as value, with each value in the object key passed when created. Here pages, mode and index. I've used a switchMap to demo here if updateCurrentPages passes an observable, but you could use a map if there is no async task to be done.
export class NavigationService {
readonly currentPages$:Observable<Pages[]>;
constructor(
private pageService: PageService,
private view: ViewService,
) {
this.paginator = new Paginator(this.pageService.pages$);
this.currentPages$ = combineLatest({
pages:this.pageService.pages$,
mode:this.view.mode$,
index:this.this.paginator.pageChange$
}).pipe(
switchMap(({pages,mode,index})=>{
return this.updateCurrentPages(pages,mode);
}),
);
}
private updateCurrentPages() {
// get an array of current pages depending on pages array, mode & index
this.currentPages.next(...);
}
public goToNextPage() {
this.paginator.next();
}
}
class Paginator{
pageChange$ = combineLatest({
total:this.pages$.pipe(map(pages=>pages.length)),
wanted:this.pageMove$}).pipe(map({total,wanted}=>{
// Make sure it is between 0 and maximum according to pages.
return Math.max(Math.min(total-1,wanted),0);
}),
// Do not emit twice the same page (pressing next when already at last)
dinstinctUntilChanged());
);
pageMove$ = new BehaviorSubject<number>(0);
constructor(pages$: Observable<Pages[]>){
}
next(){
this.pageMove$.next(this.pageMove$.value()+1);
}
previous(){
this.pageMove$.next(this.pageMove$.value()-1);
}
to(i:number){
this.pageMove$.next(i);
}
}
Beware tough about stating that subscribe is never needed. You may want to subscribe to some events for some reason. It is just that combining everything in a pipeline makes things easier to handle... In the example above, the observables will be unsubscribed to when the consumer of your service unsubscribes to currentPages$. So one thing less to handle.
Also, note that if multiple consumers subscribe to this service's currentPages$ the pipeline will be duplicated and unecessary work will be done, once for each subscriber. While this MAY be good, you might want to have everyone subscribe to the same "final" observable. This is easily do-able by adding share() or shareReplay(1) at the end of your pipeline. Share will make sure the same observable pipeline will be used for the new subscriber, and they will receive new values starting from then. Using shareReplay(1), will do the same but also emit the latest value directly on subscribe (just like BehaviourSubject) the 1 as parameter is indeed the number of replays to send out...
Hope this helps! When you master the RxJS you'll see that things will get easier and easier (see the difference in code amount!) but getting the hang of it take a little bit of time. Do not worry, just perseverate you'll get there. (Hint to get better, using outside variables/properties are the evil of handling pipelines)

How to mock an encapsulated dependency with Jest?

I am trying to mock the return value of a method of a class which is instantiated inside of the class I am testing, without it persisting the mocked value across the rest of the tests.
Here is a very basic demo I've put together:
Mystery.ts
export class Mystery {
private max = 10;
public getNumber(): number {
return Math.random() * this.max;
}
}
TestedClass.ts
import { Mystery } from './Mystery';
export class TestedClass {
public someMethod() {
const numberGenerator = new Mystery();
return numberGenerator.getNumber();
}
}
TestedClass.test.ts
import { Mystery } from './Mystery';
import { TestedClass } from './TestedClass';
jest.mock('./Mystery');
describe('TestedClass', () => {
beforeEach(jest.clearAllMocks);
it('should return the number 1000', () => {
// I want to have Mystery.getNumber() return 1000 for this test only.
Mystery.prototype.getNumber = jest.fn().mockReturnValue(1000);
// jest.spyOn(Mystery.prototype, 'getNumber').mockReturnValue(1000);
// Mystery.prototype.getNumber = jest.fn().mockReturnValueOnce(1000);
const provider = new TestedClass();
const result = provider.someMethod();
expect(result).toEqual(1000);
});
it('should return undefined', () => {
const provider = new TestedClass();
const result = provider.someMethod();
// Expects: undefined, Received: 1000
expect(result).toEqual(undefined);
});
});
As you can see from the test, I am seeing 1000 appear in the second test which I would like to avoid. In the real scenario all of the tests will likely need to mock the return value of Mystery.getNumber() (in different ways) but I want to ensure that one test is not affecting another test as it could be now.
This current behaviour does make sense as I am changing the Mystery.prototype but I'm not sure how to mock the value for individual test in any other way. I am also aware of using composition, which would help tremendously here, but I would like to avoid this for the sake of this question.
If you're trying to mock the getNumber method for a single test case only and not for all test cases, the jest.fn().mockReturnValueOnce method should do the trick.
So instead of
Mystery.prototype.getNumber = jest.fn().mockReturnValue(1000);
use
Mystery.prototype.getNumber = jest.fn().mockReturnValueOnce(1000);

How to set two Async Pipe functions in Angular 7? [duplicate]

This question already has answers here:
*ngIf with multiple async pipe variables
(3 answers)
Closed 3 years ago.
I have 2 functions that checks a specific condition, I would both like both of them to be true.
How would you use *ngIf in this case? Currently if I set one of them it works, but I need both of them.
HTML
<p *ngIf="(isFree$ | async) && (order$ | async)">Free Plan</p>
TS
public order(data) {
const order = data.externalOrderId;
if (order.substring(0, 4) === 'asdad') {
return true;
} else {
return false;
}
}
public isFree$ = this.planType$.pipe(
map((data) => {
return (data.plan === 'Free');
}
));
Create a new dumb component that has (2) Inputs: [isFree], [order]
In your new component use an *ngIf statement to show the <p> if both Inputs are available.
You can use rxjs forkJoin
It will wait until both the observables streams return the response. It returns single observable array of responses to which you can subscribe or use async pipe to resolve
HTML
<p *ngIf="resp$ | async">Free Plan</p>
TS
public isFree$ = this.planType$.pipe(
map((data) => {
return (data.plan === 'Free');
}
));
public isOrder$ = of({resp:[]}) // stream 2
public resp$ = forkJoin(this.isFree$, this.isOrder$)
Fork join example with ngIf and async

Property always undefined when accessing it in Event Handler from SignalR

I have built an angular application and I am using signalR to get notifications.
For the signalR stuff i have installed "#aspnet/signalr": "^1.0.4".
In the Angular App I have built a service that handles all notifications that are sent over signalR:
private _hubConnection: HubConnection;
numberX: number = 0;
constructor() {
this.createConnection();
this.registerOnServerEvents();
this.startConnection();
}
private createConnection() {
this._hubConnection = new HubConnectionBuilder().withUrl(someURL).build();
}
private startConnection(): void {
this._hubConnection.start();
}
private registerOnServerEvents(): void {
this._hubConnection.on('Notify', (data: any) => {
this.numberX++;
});
}
When I get an event from signalR the event handler is actually called, when I send something to the console output it is actually written to the console.
But when I try to access a member property (in this case numberX), I always get an exception telling me that numberX is undefined, even I define it at the very start.
Could this be some kind of scope thing?
EDIT:
The solution from Kenzk447 actually works for my given scenario. But this would not work when I go one step further by not just increasing a number, but using an EventEmitter that can be consumed by components:
In the service:
someEvent: EventEmitter<any> = new EventEmitter();
private registerOnServerEvents(): void {
const $self = this;
this._hubConnection.on('ErrorMessage', (data: any) => {
$self.someEvent.emit(data);
});
}
In the component:
notificationCount: number = 0;
this.notificationService.someEvent.subscribe(this.onSomeEvent);
onSomeEvent(data: any) {
this.notificationCount++;
}
When doing this, 'this' in the component is undefined and I cannot access properties of the component.
EDIT 2:
Ok I got it working with
this solution
The trick is to bind the eventHandler.
Nevertheless I will accept the answer because it worked in my given example and pointed me to the right direction.
I was researched on SignalR repo, and i found:
methods.forEach((m) => m.apply(this, invocationMessage.arguments));
Issue here: m.apply(this,..., SignalR trying to impose 'this' as HubConnection class instance. So, my solution:
private registerOnServerEvents(): void {
const $self = this;
this._hubConnection.on('Notify', (data: any) => {
$self.numberX++;
});
}

Run action once per Observable's value emission

Suppose we have the following code (Stackblitz):
public counter: number = 0; // Some arbitrary global state
public ngOnInit(): void {
const subj: Subject<number> = new Subject<number>();
const obs: Observable<number> = subj.asObservable().pipe(
tap((): void => {
++this.counter;
}));
// this.counter increments for each subscription.
// But supposed to increment once per obs new value emition.
obs.subscribe();
obs.subscribe();
obs.subscribe();
subj.next(1);
// Now this.counter === 3
}
The result is that this.counter equals 3 after executing the above, it has been incremented one time per each subscription.
But how can we increment it once per Observable's new value emission in a Reactive way?
I know that we should avoid state outside the Reactive flow, but still it is needed time to time in real world apps.
The reason why you see number 3 is a little different. When you use of(1) it emits one next notification and right after that the complete notification. It does this every time you call obs.subscribe().
So if you want to see 1 you need make an intermediate Subject where you make all subscriptions and then this intermediate Subject subscribe to the source Observable (with publish() operator most easily that creates the Subject for you):
const obs: ConnectableObservable<number> = of(1).pipe(
tap((): void => {
++this.counter;
}),
publish(),
) as ConnectableObservable<number>;
obs.subscribe();
obs.subscribe();
obs.subscribe();
obs.connect();
Your updated demo: https://stackblitz.com/edit/rxjs-update-global-state-ejdkie?file=app/app.component.ts
Be aware that of(1) will emit complete notification anyway which means that the subject inside publish() will complete as well. This might (or might not) be a problem for you but it really depends on your use-case.
Edit: This is updated demo where source is Subject: https://stackblitz.com/edit/rxjs-update-global-state-bws36m?file=app/app.component.ts
Try something like this please once. I cannot run it anywhere. If it gives an error let me know
public counter: number = 0;
public items: any = [];
public ngOnInit(): void {
const obs: Observable<number> = of(1).pipe(
tap((value): void => {
if(! Array.prototype.includes(value)) {
this.items.push(value);
++this.counter;
}
}));
// this.counter increments for each subscription.
// How to make it increment once per obs new value emission.
obs.subscribe(1);
obs.subscribe(2);
obs.subscribe(1);
// Now this.counter === 3
}

Categories