Skip to content

Commit 81f913f

Browse files
tomwhite007benlesh
authored andcommitted
Marble testing docs update (#4817)
* docs: added Synchronous Assertion section. Added Synchronous Assertion section to make the use of flush() clearer. Change scheduler variable name to match initialisation example. * docs: fixed link to Synchronous Assertion * docs: changed Synchronous Assertion example test PR review requested improved example * docs: simplified Synchronous Assertion example
1 parent b87ab8f commit 81f913f

File tree

1 file changed

+35
-10
lines changed

1 file changed

+35
-10
lines changed

docs_app/content/guide/testing/marble-testing.md

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
# Testing RxJS Code with Marble Diagrams
32

43
<div class="alert is-helpful">
@@ -12,21 +11,21 @@ We can test our _asynchronous_ RxJS code _synchronously_ and deterministically b
1211
```ts
1312
import { TestScheduler } from 'rxjs/testing';
1413

15-
const scheduler = new TestScheduler((actual, expected) => {
14+
const testScheduler = new TestScheduler((actual, expected) => {
1615
// asserting the two objects are equal
1716
// e.g. using chai.
1817
expect(actual).deep.equal(expected);
1918
});
2019

2120
// This test will actually run *synchronously*
2221
it('generate the stream correctly', () => {
23-
scheduler.run(helpers => {
22+
testScheduler.run(helpers => {
2423
const { cold, expectObservable, expectSubscriptions } = helpers;
2524
const e1 = cold('-a--b--c---|');
2625
const subs = '^----------!';
2726
const expected = '-a-----c---|';
2827

29-
expectObservable(e1.pipe(throttleTime(3, scheduler))).toBe(expected);
28+
expectObservable(e1.pipe(throttleTime(3, testScheduler))).toBe(expected);
3029
expectSubscriptions(e1.subscriptions).toBe(subs);
3130
});
3231
});
@@ -49,9 +48,9 @@ testScheduler.run(helpers => {
4948
});
5049
```
5150

52-
Although `run()` executes entirely synchronously, the helper functions inside your callback function do not! These functions **schedule assertions** that will execute either when your callback completes or when you explicitly call `flush()`. Be wary of calling synchronous assertions, for example `expect` from your testing library of choice, from within the callback.
51+
Although `run()` executes entirely synchronously, the helper functions inside your callback function do not! These functions **schedule assertions** that will execute either when your callback completes or when you explicitly call `flush()`. Be wary of calling synchronous assertions, for example `expect` from your testing library of choice, from within the callback. See [Synchronous Assertion](#synchronous-assertion) for more information on how to do this.
5352

54-
- `hot(marbleDiagram: string, values?: object, error?: any)` - creates a ["hot" observable](https://medium.com/@benlesh/hot-vs-cold-observables-f8094ed53339) (like a subject) that will behave as though it's already "running" when the test begins. An interesting difference is that `hot` marbles allow a `^` character to signal where the "zero frame" is. That is the point at which the subscription to observables being tested begins.
53+
- `hot(marbleDiagram: string, values?: object, error?: any)` - creates a ["hot" observable](https://medium.com/@benlesh/hot-vs-cold-observables-f8094ed53339) (like a subject) that will behave as though it's already "running" when the test begins. An interesting difference is that `hot` marbles allow a `^` character to signal where the "zero frame" is. This is the default point at which the subscription to observables being tested begins, (this can be configured - see `expectObservable` below).
5554
- `cold(marbleDiagram: string, values?: object, error?: any)` - creates a ["cold" observable](https://medium.com/@benlesh/hot-vs-cold-observables-f8094ed53339) whose subscription starts when the test begins.
5655
- `expectObservable(actual: Observable<T>, subscriptionMarbles?: string).toBe(marbleDiagram: string, values?: object, error?: any)` - schedules an assertion for when the TestScheduler flushes. Give `subscriptionMarbles` as parameter to change the schedule of subscription and unsubscription. If you don't provide the `subscriptionMarbles` parameter it will subscribe at the beginning and never unsubscribe. Read below about subscription marble diagram.
5756
- `expectSubscriptions(actualSubscriptionLogs: SubscriptionLog[]).toBe(subscriptionMarbles: string)` - like `expectObservable` schedules an assertion for when the testScheduler flushes. Both `cold()` and `hot()` return an observable with a property `subscriptions` of type `SubscriptionLog[]`. Give `subscriptions` as parameter to `expectSubscriptions` to assert whether it matches the `subscriptionsMarbles` marble diagram given in `toBe()`. Subscription marble diagrams are slightly different than Observable marble diagrams. Read more below.
@@ -71,6 +70,7 @@ How many virtual milliseconds one frame represents depends on the value of `Test
7170
- `'|'` complete: The successful completion of an observable. This is the observable producer signaling `complete()`.
7271
- `'#'` error: An error terminating the observable. This is the observable producer signaling `error()`.
7372
- `[a-z0-9]` e.g. `'a'` any alphanumeric character: Represents a value being emitted by the producer signaling `next()`. Also consider that you could map this into an object or an array like this:
73+
7474
```ts
7575
const expected = '400ms (a-b|)';
7676
const values = {
@@ -83,13 +83,14 @@ How many virtual milliseconds one frame represents depends on the value of `Test
8383
// This would work also
8484
const expected = '400ms (0-1|)';
8585
const values = [
86-
'value emitted',
86+
'value emitted',
8787
'another value emitted',
8888
];
8989

9090
expectObservable(someStreamForTesting)
9191
.toBe(expected, values);
9292
```
93+
9394
- `'()'` sync groupings: When multiple events need to be in the same frame synchronously, parentheses are used to group those events. You can group next'd values, a completion, or an error in this manner. The position of the initial `(` determines the time at which its values are emitted. While it can be unintuitive at first, after all the values have synchronously emitted time will progress a number of frames equal to the number of ASCII characters in the group, including the parentheses. e.g. `'(abc)'` will emit the values of a, b, and c synchronously in the same frame and then advance virtual time by 5 frames, `'(abc)'.length === 5`. This is done because it often helps you vertically align your marble diagrams, but it's a known pain point in real-world testing. [Learn more about known issues](#known-issues).
9495
- `'^'` subscription point: (hot observables only) shows the point at which the tested observables will be subscribed to the hot observable. This is the "zero frame" for that observable, every frame before the `^` will be negative. Negative time might seem pointless, but there are in fact advanced cases where this is necessary, usually involving ReplaySubjects.
9596

@@ -189,9 +190,9 @@ Manually unsubscribe from a source that will never complete:
189190

190191
```js
191192
it('should repeat forever', () => {
192-
const scheduler = createScheduler();
193+
const testScheduler = createScheduler();
193194

194-
scheduler.run(({ expectObservable }) => {
195+
testScheduler.run(({ expectObservable }) => {
195196
const foreverStream$ = interval(1).pipe(mapTo('a'));
196197

197198
// Omitting this arg may crash the test suite.
@@ -202,7 +203,31 @@ it('should repeat forever', () => {
202203
});
203204
```
204205

205-
***
206+
## Synchronous Assertion
207+
208+
Sometimes, we need to assert changes in state _after_ an observable stream has completed - such as when a side effect like `tap` updates a variable. Outside of Marbles testing with TestScheduler, we might think of this as creating a delay or waiting before making our assertion.
209+
210+
For example:
211+
212+
```ts
213+
let eventCount = 0;
214+
215+
const s1 = cold('--a--b|', { a: 'x', b: 'y' });
216+
217+
// side effect using 'tap' updates a variable
218+
const result = s1.pipe(tap(() => eventCount++));
219+
220+
expectObservable(result).toBe('--a--b|', ['x', 'y']);
221+
222+
// flush - run 'virtual time' to complete all outstanding hot or cold observables
223+
flush();
224+
225+
expect(eventCount).toBe(2);
226+
```
227+
228+
In the above situation we need the observable stream to complete so that we can test the variable was set to the correct value. The TestScheduler runs in 'virtual time' (synchronously), but doesn't normally run (and complete) until the testScheduler callback returns. The flush() method manually triggers the virtual time so that we can test the local variable after the observable completes.
229+
230+
---
206231

207232
## Known Issues
208233

0 commit comments

Comments
 (0)