- Published on
Testing RxJS Application
- Authors
- Name
- Amit Barletz
- @BarletzA52
How to Test Asynchronous Code
It has been a while since my last blog post. Setting aside all excuses, the main reason I didn't post anything is that for a long time I was thinking what to write about. Finally, I decided to take my two weaknesses when it comes to code and merged them together:
RxJS and Unit Testing 😵💫
First, let's discuss what RxJs and unit testing are.
RxJS - a library for reactive programming. Reactive programming is an asynchronous programming paradigm concerned with data streams and the propagation of change. RxJS uses observables that makes it easier to compose asynchronous or callback-based code.
Unit Testing - also known as component testing, is a level of software testing where individual units/components of a software are tested. The purpose is to validate that each unit of the software performs as designed.
There is also integration testing and end to end (e2e) testing. Integration testing is a level of software testing where individual units / components are combined and tested as a group. The purpose of this level of testing is to expose faults in the interaction between integrated units. End-to-end testing is a technique that tests the entire software product from beginning to end to ensure the application flow behaves as expected
There are some challenges when it comes to testing RxJS code:
- Values are asynchronously emitted
- More than one value can be emitted
- Order and timing can matter
- Values can be emitted in current macrotask, subsequent microtask or emission can be scheduled in a future
These challenges are related to the browser event loop (to understand it more, you can read one of my previous articles about asynchronous JS in the browser Big Words - Part 2).
When I googled 'How to test RxJS code' I've encountered the term 'Scheduler'.
According to Wikipedia, schedulers are data structures that control when emissions are delivered. In other words, it is a piece of logic that helps you parametrize when, where and how code is executed.
There are 2 types of schedulers:
1. VirtualTimeScheduler
A scheduler that executes tasks according to the execution context:
- queueScheduler - synchronous tasks (of and range operators). Schedules value emission in a queue and then performs emission (same tick).
- asyncScheduler - macro tasks (setTimeout). Delays value emission in another macrotask
- asapScheduler - schedules tasks on the microtask queue (promises). Emission is performed just after the current macrotask (as soon as possible)
- animationFrameScheduler - paint events. Emission is aligned with browser re-draw event to create smooth animation (right before the next paint event of the browser)
In addition to executing tasks, a scheduler also has to:
- control the order of work - execution policy - a scheduler holds a variable called actions, which is an array of actions, and a
flush()
method that runs all of these actions in the right order. - schedule work on time - a scheduler holds
now()
function. That function returns a number usuallyDate.now()
2. TestScheduler
Scheduler for unit testing. Inherits from VirtualTimeScheduler and has additional methods for convenient testing. Works on the current macro task, like asyncScheduler.
❗️ NOTES:
- For this blog post I have built a repository on github: angular-RxJS-testing, so that you can run the tests on your machine, play with them or even add some of your own tests.
- You should have a prior knowledge of RxJS operators and functions, or at least go to the resource I will provide.
- You should to be familiar with the syntax of tests.
- Some of my example are "angular oriented" (actually, the repo is an angular project), but you don't have to know angular to understand them.
The operators and functions you should know for this blog post
distinctUntilChanged()
operator:
Enough talking, let's dive into the code and start with an easy, warm up example of SYNCHRONOUS code:
import { map } from 'rxjs/operators'
import { of } from 'rxjs'
export class SchedulersTestingComponent {
addHi() {
return of(1, 2, 3).pipe(map((item) => item + 'Hi👋'))
} // '1Hi👋', '2Hi👋', '3Hi👋'
}
To test our methods, we should make an instance of our tested component (class), declare an expected result variable, and a result variable to work on. Then we have to check if the expected result is equal to the actual result.
This is the big picture of how I have tested my component, but as you will see in the following examples, as the code becomes more complex, there will be some other variables to help us in the test.
And now the test of our synchronous example:
it('should emit 3 values', () => {
const mockedComp = new SchedulersTestingComponent()
const result: string[] = []
const expectedResult = ['1Hi👋', '2Hi👋', '3Hi👋']
mockedComp.addHi().subscribe((val: string) => result.push(val))
expect(result).toEqual(expectedResult)
})
Testing synchronous code is pretty straightforward, to make it more complicated, we can add to addHi()
async scheduler. In this case, the test will fail because at the time we call expect()
not all the values are emitted.
addHiAsap() {
return scheduled(['1', '2', '3'], asapScheduler)
.pipe(
map(item => item+'Hi👋')
);
}
In this blog post I will discuss on 3 approaches of testing RxJS code:
- Fake time with jasmine
done()
callback - VirtualTimeScheduler
- Angular fakeAsync function (mock setInterval)
done()
callback:
1. Fake time with jasmine When a specific test is executed, jasmine holds and waits until the done()
callback is called, therefore, if data is provided asynchronously, we subscribe to it, make an assertion after we got all the data and then run the done()
callback to tell jasmine that we can continue.
it('should emit 3 values', (done) => {
const mockedComp = new SchedulersTestingComponent(mockHttp)
const values$ = mockedComp.addHiAsap()
const result: string[] = []
values$.subscribe({
next: (value) => {
result.push(value)
},
complete: () => {
expect(result).toEqual(['1Hi👋', '2Hi👋', '3Hi👋'])
done()
},
})
})
The done()
callback is called with no params to indicate that the code execution is done. It can be called with an Error object parameter to indicate that something has failed. The message of the Error object should indicate what failed. The expect()
function will be invoked only when the data is returned.
The following example uses Angular's httpClient to make a http get request to some API. It receives a specific user (by ID) with delay and if the request succeeds, it will repeat 2 more times.
(yes, I know, it doesn't really make sense, but it's just for the example 😬).
getUserData(userId: number, timeInSec: number) {
return this.httpClient.get(`api/users/${userId}`)
.pipe(repeatWhen(val => val.pipe(
delay(timeInSec * 1000),
take(2),
)),
);
}
There are two challenges in the above example:
- Testing code with delay time can take too long. To prevent it we can provide in the test a smaller delay time.
- Testing http requests adds another layer of complexity, but, luckily for us - we can mock the http request. Actually mocking is one of the essence of testing.
When we test a component, we create a mock of this component, therefore, if the component makes http calls with a help of some service/module/third party library, we must mock it. We provide our mocked module in a providers array of the mocked component.
describe('SchedulersTestingComponent', () => {
let component: SchedulersTestingComponent;
let fixture: ComponentFixture<SchedulersTestingComponent>;
let mockHttp: any;
beforeEach(async () => {
mockHttp = {get: () => scheduled([{userId: 12, firstName: 'Yosemite', lastName: 'Sam'}], asyncScheduler)}
await TestBed.configureTestingModule({
declarations: [ SchedulersTestingComponent ],
providers: [
{provide: HttpClient, useValue: mockHttp}
]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(SchedulersTestingComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
}
Now that we have a mock of http request, we add it as an argument when we create the new instance of the tested component.
interface User {
userId: number
firstName: string
lastName: string
}
it('should get user data 3 times (with jasmine done() callback)', (done) => {
const mockedComp = new SchedulersTestingComponent(mockHttp)
const userData$ = mockedComp.getUserData(12, 0.01)
const result: User[] = []
userData$.subscribe({
next: (value: any) => {
result.push(value)
},
complete: () => {
expect(result).toEqual([
{
userId: 12,
firstName: 'Yosemite',
lastName: 'Sam',
},
{
userId: 12,
firstName: 'Yosemite',
lastName: 'Sam',
},
{
userId: 12,
firstName: 'Yosemite',
lastName: 'Sam',
},
])
done()
},
})
})
To conclude: jasmine's done()
callback checks only the final result, is simple, good for asapScheduler (microtasks) and for single value with very small delay, or no delay at all. It is bad for long timed, hard coded timers
2. VirtualTimeScheduler:
Used when we want to test RxJS code with big emission delays.
Let's take the the getUserData()
example again and add another argument: scheduler
getUserDataAsyncScheduler(userId: number, timeInSec: number, scheduler = asyncScheduler) {
return this.httpClient.get(`api/users/${userId}`)
.pipe(repeatWhen(val => val.pipe(
delay(timeInSec * 1000, scheduler),
take(2),
)),
);
}
asyncScheduler uses setInterval internally to schedule value emission. VirtualTimeScheduler inherits from asyncScheduler, therefore, we can replace the asyncScheduler instance with a VirtualTimeScheduler instance in our test code.
In that case, VirtualTimeScheduler prevent asyncScheduler to call setInterval and instead, puts the tasks in an internal queue sorted by delay.
Then we use the flush()
method of VirtualTimeScheduler and the tasks are executed.
import { asyncScheduler, scheduled, VirtualTimeScheduler } from 'rxjs'
it('should get user data 3 times (with VirtualTimeScheduler)', () => {
const scheduler = new VirtualTimeScheduler()
mockHttp = {
get: () => scheduled([{ userId: 12, firstName: 'Yosemite', lastName: 'Sam' }], scheduler),
}
const mockedComp = new SchedulersTestingComponent(mockHttp)
const userData$ = mockedComp.getUserDataAsyncScheduler(12, 0.01, scheduler)
const result: User[] = []
userData$.subscribe({
next: (value: any) => {
result.push(value)
},
})
scheduler.flush()
expect(result).toEqual([
{
userId: 12,
firstName: 'Yosemite',
lastName: 'Sam',
},
{
userId: 12,
firstName: 'Yosemite',
lastName: 'Sam',
},
{
userId: 12,
firstName: 'Yosemite',
lastName: 'Sam',
},
])
})
good to know:
🔘️️ The debounceTime()
operator uses asyncScheduler ander the hood, therefore, next time you use debounceTime in your RxJS code, you can test it with VirtualTimeScheduler and its flush method.
🔘 Since asapScheduler inherits from asyncScheduler, we can replace the asapScheduler with VirtualTimeScheduler as well.
VirtualTimeScheduler also checks only the final result, but as apposed to jasmine's done()
callback, we work with predicted delay value, and we have the ability to test hardcoded values.
3. Angular fakeAsync function:
Here is a more concrete example: search input. The user starts typing and as long as their typed value doesn't reach a declared length, and their typed value hasn't changed, the http call will not be executed.
getSearchResult(input$: Observable<any>, timeout = 500, scheduler = asyncScheduler) {
return input$.pipe(
map((event: Event) => (event.target as HTMLInputElement).value),
filter((text: string) => text.length > 3),
debounceTime(timeout, scheduler),
distinctUntilChanged(),
switchMap((text) => this.httpClient.get(`someUrl?searchParam=${text}`)),
);
}
Angular fakeAsync mock setInterval in the browser. It uses FakeAsyncTestZoneSpec (instead of ngZone).
The standard browser APIs such as setInterval, setTimeout and promises are patched in that test zone to a special internal schedulerQueue. Once we call in the test a special method called tick(timeInMs)
, the mocked setInterval executes (flushes) the scheduled tasks from the internal queue (in ms).
There is also flushMicroTask()
method which flushes only micro tasks queue (promises), therefore, if we have asapScheduler in our code, we would have to use that method.
import { timeRange } from 'rxjs-toolbox'
it('should call http get and get request result', fakeAsync(() => {
const input$ = timeRange(
[
{ value: { target: { value: 'aaa' } }, delay: 100 },
{ value: { target: { value: 'aaab' } }, delay: 500 },
{ value: { target: { value: 'aaabc' } }, delay: 2500 },
],
true
)
const result: {}[] = []
mockHttp = { get: () => scheduled(['someValue'], asyncScheduler) }
const mockedComp = new SchedulersTestingComponent(mockHttp)
const searchResults$ = mockedComp.getSearchResult(input$)
searchResults$.subscribe({
next: (value) => {
result.push(value)
},
})
tick(3500)
expect(result).toEqual(['someValue', 'someValue'])
}))
Angular fakeAsync function, like the other testing methods, checks only the final result. It produces delayed values and can be tested with hardcoded values, so we control the time.
TestScheduler
Inherits from VirtualTimeScheduler, therefore we can use TestScheduler in the same way we use VirtualTimeScheduler. So why are they called differently? There must be at least one difference between them.
The differences between VirtualTimeScheduler and TestScheduler are:
- The maxFrame of VirtualTimeScheduler is infinity, while the maxFrame of TestScheduler is 750ms.
- TestScheduler's constructor requires an assertion equality function, while VirtualTimeScheduler does not.
- TestScheduler can perform marble testing.
it('should getUserData 3 times (use TestScheduler as VirtualTimeScheduler)', () => {
const assertion = (actual: object, expected: object) => {
expect(actual).toEqual(expected)
}
const scheduler = new TestScheduler(assertion)
mockHttp = {
get: () => scheduled([{ userId: 12, firstName: 'Yosemite', lastName: 'Sam' }], scheduler),
}
const mockedComp = new SchedulersTestingComponent(mockHttp)
const userData$ = mockedComp.getUserDataAsyncScheduler(12, 0.01, scheduler)
// as the maxFrame of TestScheduler is 750ms we must add the following line
scheduler.maxFrames = Number.POSITIVE_INFINITY
const result: User[] = []
userData$.subscribe({
next: (value: any) => {
result.push(value)
},
})
scheduler.flush()
expect(result).toEqual([
{
userId: 12,
firstName: 'Yosemite',
lastName: 'Sam',
},
{
userId: 12,
firstName: 'Yosemite',
lastName: 'Sam',
},
{
userId: 12,
firstName: 'Yosemite',
lastName: 'Sam',
},
])
})
All the testing approaches I have explained in this article don't give us a visual representation of the test. TestScheduler has a special method for testing with Marble Diagram which helps us to visualize the tests.
Marble Diagram is a visual representation for events emitted over time. You can read more about testing with marble diagram in rxjs github Repo, but as I realized that nowadays we have simpler ways to test rxjs code, I decided to not cover it. For example, besides all the methods I have covered, you can use this library, which was written by Shai Reznik: observer-spy
Thanks for reading, I hope you have learned something new, or at least gained a better understanding of a topic you already know.