Angular 6, Rxjs, Jest, Jasmine-marbles.
very common scenario: a component that searches for server-side items. In the component, there are some controls that can change search critera, and I'd like to code in "reactive-style". So in the component code I have something like this:
class SearchComponent implements OnInit {
public searchData: SearchData = {};
public searchConditions$ = new Subject<SearchData>();
constructor(private searchService: SearchService) { }
public results$: Observable<ResultData> = this.searchConditions$.pipe(
distinctUntilChanged(this.compareProperties), // omissis but it works
flatMap(searchData => this.searchService.search(searchData)),
shareReplay(1)
);
// search actions
ngOnInit() {
this.searchConditions$.next(this.searchData);
}
public onChangeProp1(prop1: string) {
this.searchData = { ...this.searchData, prop1 };
this.searchConditions$.next(this.searchData);
}
public onChangeProp2(prop2: string) {
this.searchData = { ...this.searchData, prop2 };
this.searchConditions$.next(this.searchData);
}
}
That's, a Subject
that fires search conditions each time something in the UI has changed.
Now I'd like to test that search service will be called only for distinct input. I can do it "without marbles" in this way:
test('when searchConditions$ come with equal events search service will not be called more than once', (done: any) => {
service.search = jest.fn(() => of(TestData.results));
component.results$.subscribe({
complete: () => {
expect(service.Search).toHaveBeenCalledTimes(1);
done();
}
});
component.searchConditions$.next(TestData.searchCriteria);
component.searchConditions$.next(TestData.searchCriteria);
component.searchConditions$.next(TestData.searchCriteria);
component.searchConditions$.complete();
});
Now I'd like to convert this test using jasmine marbles, but I don't know how...
I'd like something like this:
test('when searchConditions$ come with equal events search service will not be called more than once', (done: any) => {
service.search = jest.fn(() => of(TestData.results));
component.searchConditions$ = cold('--a--a|', { a : TestData.searchCriteria});
const expected = cold('--b---|', { b : TestData.results});
expect(component.results$).toBeObservable(expected);
});
Obviously, it doesn't work...
somehow close...using a "test helper"
test('when searchConditions$ comes with equal events search service will not be called more than once - marble version', () => {
service.search = jest.fn(() => of(TestData.results));
const stream = cold('--a--a|', { a : TestData.searchCriteria});
const expected = cold('--b---|', { b : TestData.results});
stubSubject(component.searchConditions$, stream);
expect(component.results$).toBeObservable(expected);
});
// test helpers
const stubSubject = (subject: Subject<any> , marbles: TestObservable) => {
marbles.subscribe({
next: (value: any) => subject.next(value),
complete: () => subject.complete(),
error: (e) => subject.error(e)
});
};
What is marble testing? Marble testing allows you to test asynchronous RxJS code synchronously and step-by-step with the help of RxJS TestScheduler test utility and using virtual time steps. There are also marble diagrams which demonstrate what is happening with a particular operation in the observable stream.
I have a question in jasmine-marbles — cold, hot function. The hot and cold helpers create hot and cold observables respectively. With a cold test observable, each subscriber receives the notifications represented in the marble diagram, played from the diagram's beginning. hot test observables are different.
The main goal in tests to mock dependencies and don't change anything inside of testing unit SearchComponent.
Therefore stubSubject(component.searchConditions$, stream)
or component.searchConditions$ = cold
is a bad practice.
Because we want to schedule emits in searchConditions$
we need to have an internal scheduled stream and we can use cold
or hot
here too.
source data (sorry, I guessed some types)
type SearchData = {
prop?: string;
};
type ResultData = Array<string>;
@Injectable()
class SearchService {
public search(term: SearchData): Observable<any> {
return of();
}
}
@Component({
selector: 'search',
template: '',
})
class SearchComponent implements OnInit {
public searchData: SearchData = {};
public searchConditions$ = new Subject<SearchData>();
constructor(private searchService: SearchService) { }
public results$: Observable<ResultData> = this.searchConditions$.pipe(
distinctUntilKeyChanged('prop'),
tap(console.log),
flatMap(searchData => this.searchService.search(searchData)),
shareReplay(1),
);
// search actions
ngOnInit() {
this.searchConditions$.next(this.searchData);
}
public onChangeProp1(prop1: string) {
this.searchData = { ...this.searchData, prop: prop1 }; // I've changed it to prop
this.searchConditions$.next(this.searchData);
}
public onChangeProp2(prop2: string) {
this.searchData = { ...this.searchData, prop: prop2 }; // I've changed it to prop
this.searchConditions$.next(this.searchData);
}
}
and its test
test('when searchConditions$ come with equal events search service will not be called more than once', () => {
service.search = jest.fn(() => of(TestData.results));
// our internal scheduler how we click our component.
// the first delay `-` is important to allow `expect` to subscribe.
cold('-a--a--b--b--a-|', {
a: 'a',
b: 'b',
}).pipe(
tap(v => component.searchConditions$.next({prop: v})),
// or like user does it
// tap(v => component.onChangeProp1(v)),
finalize(() => component.searchConditions$.complete()),
).subscribe();
// adding our expectations.
expect(component.results$).toBeObservable(cold('-r-----r-----r-|', {
r: TestData.results,
}));
});
Now we don't change our testing unit, only its dependencies (service.search
).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With