From ce20af9bfc0e3a47022394670ec23ec7a2d69a5f Mon Sep 17 00:00:00 2001 From: Jay Phelps Date: Thu, 3 May 2018 13:58:25 -0400 Subject: [PATCH] docs(marbles): docs for testScheduler.run() and marble diagrams (#3612) --- doc/internal-marble-tests.md | 188 +++++++++++++++++++++++++++++++++++ doc/marble-testing.md | 175 ++++++++++++++++++++++++++++++++ doc/writing-marble-tests.md | 188 +---------------------------------- 3 files changed, 365 insertions(+), 186 deletions(-) create mode 100644 doc/internal-marble-tests.md create mode 100644 doc/marble-testing.md diff --git a/doc/internal-marble-tests.md b/doc/internal-marble-tests.md new file mode 100644 index 0000000000..f55e4ac633 --- /dev/null +++ b/doc/internal-marble-tests.md @@ -0,0 +1,188 @@ +> NOTE: This document refers to the writing marble tests for the **RxJS repo internals** and is intended for anyone wishing to help maintain the RxJS repo. **Users of RxJS** should instead view the [guide for writing marbles tests for applications](./marble-testing.md). The major difference is that the behavior of the TestScheduler differs between manual usage and using the `testScheduler.run(callback)` helper. + +# Writing Marble Tests + +"Marble Tests" are tests that use a specialized VirtualScheduler called the `TestScheduler`. They enable us to test +asynchronous operations in a synchronous and dependable manner. The "marble notation" is something that's been adapted +from many teachings and documents by people such as @jhusain, @headinthebox, @mattpodwysocki and @staltz. In fact, +@staltz first recommended this as a DSL for creating unit tests, and it has since been altered and adopted. + +#### links + +- [Contribution](../CONTRIBUTING.md) +- [Code of Conduct](../CODE_OF_CONDUCT.md) + +## Basic methods + +The unit tests have helper methods that have been added to make creating tests easier. + +- `hot(marbles: string, values?: object, error?: any)` - creates a "hot" observable (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. +- `cold(marbles: string, values?: object, error?: any)` - creates a "cold" observable whose subscription starts when + the test begins. +- `expectObservable(actual: Observable).toBe(marbles: string, values?: object, error?: any)` - schedules an assertion + for when the TestScheduler flushes. The TestScheduler will automatically flush at the end of your jasmine `it` block. +- `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. + +### Ergonomic defaults for `hot` and `cold` + +In both `hot` and `cold` methods, value characters specified in marble diagrams are emitted as strings unless a `values` +argument is passed to the method. Therefor: + +`hot('--a--b')` will emit `"a"` and `"b"` whereas + +`hot('--a--b', { a: 1, b: 2 })` will emit `1` and `2`. + +Likewise, unspecified errors will just default to the string `"error"`, so: + +`hot('---#')` will emit error `"error"` whereas + +`hot('---#', null, new SpecialError('test'))` will emit `new SpecialError('test')` + + +## Marble Syntax + +Marble syntax is a string which represents events happening over "time". The first character of any marble string + +always represents the "zero frame". A "frame" is somewhat analogous to a virtual millisecond. + +- `"-"` time: 10 "frames" of time passage. +- `"|"` complete: The successful completion of an observable. This is the observable producer signaling `complete()` +- `"#"` error: An error terminating the observable. This is the observable producer signaling `error()` +- `"a"` any character: All other characters represent a value being emitted by the producer signaling `next()` +- `"()"` sync groupings: When multiple events need to be in the same frame synchronously, parentheses are used + to group those events. You can group nexted values, a completion or an error in this manner. The position of the + initial `(` determines the time at which its values are emitted. +- `"^"` 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. + +### Examples + +`'-'` or `'------'`: Equivalent to `Observable.never()`, or an observable that never emits or completes + +`|`: Equivalent to `Observable.empty()` + +`#`: Equivalent to `Observable.throw()` + +`'--a--'`: An observable that waits 20 "frames", emits value `a` and then never completes. + +`'--a--b--|'`: On frame 20 emit `a`, on frame 50 emit `b`, and on frame 80, `complete` + +`'--a--b--#'`: On frame 20 emit `a`, on frame 50 emit `b`, and on frame 80, `error` + +`'-a-^-b--|'`: In a hot observable, on frame -20 emit `a`, then on frame 20 emit `b`, and on frame 50, `complete`. + +`'--(abc)-|'`: on frame 20, emit `a`, `b`, and `c`, then on frame 80 `complete` + +`'-----(a|)'`: on frame 50, emit `a` and `complete`. + +## Subscription Marble Syntax + +The subscription marble syntax is slightly different to conventional marble syntax. It represents the **subscription** and an **unsubscription** points happening over time. There should be no other type of event represented in such diagram. + +- `"-"` time: 10 "frames" of the passage. +- `"^"` subscription point: shows the point in time at which a subscription happen. +- `"!"` unsubscription point: shows the point in time at which a subscription is unsubscribed. + +There should be **at most one** `^` point in a subscription marble diagram, and **at most one** `!` point. Other than that, the `-` character is the only one allowed in a subscription marble diagram. + +### Examples + +`'-'` or `'------'`: no subscription ever happened. + +`'--^--'`: a subscription happened after 20 "frames" of time passed, and the subscription was not unsubscribed. + +`'--^--!-'`: on frame 20 a subscription happened, and on frame 50 was unsubscribed. + +## Anatomy of a Test + +A basic test might look as follows: + +```js + +var e1 = hot('----a--^--b-------c--|'); +var e2 = hot( '---d-^--e---------f-----|'); +var expected = '---(be)----c-f-----|'; + +expectObservable(e1.merge(e2)).toBe(expected); +``` + +- The `^` characters of `hot` observables should **always** be aligned. +- The **first character** of `cold` observables or expected observables should **always** be aligned + with each other, and with the `^` of hot observables. +- Use default emission values when you can. Specify `values` when you have to. + +A test example with specified values: + +```js +var values = { + a: 1, + b: 2, + c: 3, + d: 4, + x: 1 + 3, // a + c + y: 2 + 4, // b + d +} +var e1 = hot('---a---b---|', values); +var e2 = hot('-----c---d---|', values); +var expected = '-----x---y---|'; + +expectObservable(e1.zip(e2, function(x, y) { return x + y; })) + .toBe(expected, values); +``` + +- Use the same hash to look up all values, this ensures that multiple uses of the same character have the + same value. +- Make the result values as obvious as possible as to what they represent, these are *tests* afterall, we want + clarity more than efficiency, so `x: 1 + 3, // a + c` is better than just `x: 4`. The former conveys *why* it's 4, + the latter does not. + +A test example with subscription assertions: + +```js +var x = cold( '--a---b---c--|'); +var xsubs = '------^-------!'; +var y = cold( '---d--e---f---|'); +var ysubs = '--------------^-------------!'; +var e1 = hot( '------x-------y------|', { x: x, y: y }); +var expected = '--------a---b----d--e---f---|'; + +expectObservable(e1.switch()).toBe(expected); +expectSubscriptions(x.subscriptions).toBe(xsubs); +expectSubscriptions(y.subscriptions).toBe(ysubs); +``` + +- Align the start of `xsubs` and `ysubs` diagrams with `expected` diagram. +- Notice how the `x` cold observable is unsubscribed at the same time `e1` emits `y`. + +In most tests it will be unnecessary to test subscription and unsubscription points, being either obvious or implied from the `expected` diagram. In those cases do not write subscription assertions. In test cases that have inner subscriptions or cold observables with multiple subscribers, these subscription assertions can be useful. + +## Generating PNG marble diagrams from tests + +Typically, each test case in Jasmine is written as `it('should do something', function () { /* ... */ })`. To mark a test case for PNG diagram generation, you must use the `asDiagram(label)` function, like this: + + +```js +it.asDiagram(operatorLabel)('should do something', function () { + // ... +}); +``` + +For instance, with `zip`, we would write + +```js +it.asDiagram('zip')('should zip by concatenating', function () { + var e1 = hot('---a---b---|'); + var e2 = hot('-----c---d---|'); + var expected = '-----x---y---|'; + var values = { x: 'ac', y: 'bd' }; + + var result = e1.zip(e2, function(x, y) { return String(x) + String(y); }); + + expectObservable(result).toBe(expected, values); +}); +``` + +Then, when running `npm run tests2png`, this test case will be parsed and a PNG file `zip.png` (filename determined by `${operatorLabel}.png`) will be created in the `img/` folder. diff --git a/doc/marble-testing.md b/doc/marble-testing.md new file mode 100644 index 0000000000..d2a52a5b15 --- /dev/null +++ b/doc/marble-testing.md @@ -0,0 +1,175 @@ +> IMPORTANT: This guide refers to usage of marble diagrams when using the new `testScheduler.run(callback)`. Some details here do not apply to using the TestScheduler manually, without using the `run()` helper. + +# Testing RxJS Code with Marble Diagrams + +We can test our _asynchronous_ RxJS code _synchronously_ and deterministically by virtualizing time using the TestScheduler. ASCII **marble diagrams** provide a visual way for us to represent the behavior of an Observable. We can use them to assert that a particular Observable behaves as expected, as well as to create [hot and cold Observables](https://medium.com/@benlesh/hot-vs-cold-observables-f8094ed53339) we can use as mocks. + +> At this time the TestScheduler can only be used to test code that uses timers, like delay/debounceTime/etc (i.e. it uses AsyncScheduler with delays > 1). If the code consumes a Promise or does scheduling with AsapScheduler/AnimationFrameScheduler/etc it cannot be reliably tested with TestScheduler, but instead should be tested more traditionally. See the [Known Issues](#Known-Issues) section for more details. + +```ts +const testScheduler = new TestScheduler((actual, expected) => { + // some how assert the two objects are equal + // e.g. with chai `expect(actual).deep.equal(expected)` +}); + +// This test will actually run *synchronously* +testScheduler.run(({ cold }) => { + const input = cold('-a-b-c--------|'); + const output = input.pipe( + debounceTime(5) + ); + const expected = ' ----------c---|'; + expectObservable(output).toBe(expected); +}); +``` + +## API + +The callback you provide to `testScheduler.run(callback)` is called with an object that contains the helper functions you'll use to write your tests. + +> When the code inside this callback is being executed, any operator that uses timers/AsyncScheduler (like delay, debounceTime, etc) will **automaticaly** use the TestScheduler instead, so that we have "virtual time". You do not need to pass the TestScheduler to them, like in the past. + +```ts +testScheduler.run(helpers => { + const { cold, hot, expectObservable, expectSubscriptions, flush } = helpers; + // use them +}); +``` + +- `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. +- `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. +- `expectObservable(actual: Observable).toBe(marbleDiagram: string, values?: object, error?: any)` - schedules an assertion for when the TestScheduler flushes. The TestScheduler will automatically flush at the end of your jasmine `it` block. +- `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. +- `flush()` - immediately starts virtual time. Not used often, since `run()` will automatically flush for you when your callback returns, but in some cases you may wish to flush more than once or otherwise have more control. + +## Marble Syntax + +Marble syntax is a string which represents events happening over virtual time. Time progresses by _frames_. The first character of any marble string always represents the _zero frame_, or the start of time. Inside of `testScheduler.run(callback)` the frameTimeFactor is set to 1, which means one frame is equal to one virtual millisecond. + +How many virtual milliseconds one frame represents depends on the value of `TestScheduler.frameTimeFactor`. For legacy reasons the value of `frameTimeFactor` is 1 _only_ when your code inside the `testScheduler.run(callback)` callback is running. Outside of it, it's set to 10. This will likely change in a future version of RxJS so that it is always 1. + +> IMPORTANT: This syntax guide refers to usage of marble diagrams when using the new `testScheduler.run(callback)`. The semantics of marble diagrams when using the TestScheduler manually are different, and some features like the new time progression syntax are not supported. + +- `' '` whitespace: horizontal whitespace is ignored, and can be used to help vertically align multiple marble diagrams. +- `'-'` frame: 1 "frame" of virtual time passing (see above description of frames). +- `[0-9]+[ms|s|m]` time progression: the time progression syntax lets you progress virtual time by a specific amount. It's a number, followed by a time unit of `ms` (milliseconds), `s` (seconds), or `m` (minutes) without any space between them, e.g. `a 10ms b`. See [Time progression syntax](#Time-progression-syntax) for more details. +- `'|'` complete: The successful completion of an observable. This is the observable producer signaling `complete()`. +- `'#'` error: An error terminating the observable. This is the observable producer signaling `error()`. +- `[a-z0-9]` e.g. `'a'` any alphanumeric character: Represents a value being emitted by the producer signaling `next()`. +- `'()'` 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). +- `'^'` 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 neccesary, usually involving ReplaySubjects. + +### Time progression syntax + +The new time progression syntax takes inspiration from the CSS duration syntax. It's a number (int or float) immediately followed by a unit; ms (milliseconds), s (seconds), m (minutes). e.g. `100ms`, `1.4s`, `5.25m`. + +When it's not the first character of the diagram it must be padded a space before/after to disambiguate it from a series of marbles. e.g. `a 1ms b` needs the spaces because `a1msb` will be interpreted as `['a', '1', 'm', 's', 'b']` where each of these characters is a value that will be next()'d as-is. + +**NOTE**: You may have to subtract 1 millisecond from the time you want to progress because the alphanumeric marbles (representing an actual emitted value) _advance time 1 virtual frame_ themselves already, after they emit. This can be very unintuitive and frustrating, but for now it is indeed correct. + +```ts +const input = ' -a-b-c|'; +const output = '-- 9ms a 9ms b 9ms (c|)'; +/* + +// Depending on your personal preferences you could also +// use frame dashes to keep vertical aligment with the input +const input = ' -a-b-c|'; +const output = '------- 4ms a 9ms b 9ms (c|)'; +// or +const output = '-----------a 9ms b 9ms (c|)'; + +*/ + +const result = cold(input).pipe( + concatMap(d => of(d).pipe( + delay(10) + )) +); + +expectObservable(result).toBe(expected); +``` + +### Examples + +`'-'` or `'------'`: Equivalent to `never()`, or an observable that never emits or completes + +`|`: Equivalent to `empty()` + +`#`: Equivalent to `throwError()` + +`'--a--'`: An observable that waits 2 "frames", emits value `a` and then never completes. + +`'--a--b--|'`: On frame 2 emit `a`, on frame 5 emit `b`, and on frame 8, `complete` + +`'--a--b--#'`: On frame 2 emit `a`, on frame 5 emit `b`, and on frame 8, `error` + +`'-a-^-b--|'`: In a hot observable, on frame -2 emit `a`, then on frame 2 emit `b`, and on frame 5, `complete`. + +`'--(abc)-|'`: on frame 2 emit `a`, `b`, and `c`, then on frame 8 `complete` + +`'-----(a|)'`: on frame 5 emit `a` and `complete`. + +`'a 9ms b 9s c'`: on frame 0 emit `a`, on frame 10 emit `b`, on frame 10,012 emit `c`, then on on frame 10,013 `complete`. + +`'--a 2.5m b'`: on frame 2 emit `a`, on frame 150,003 emit `b` and never complete. + +## Subscription Marbles + +The `expectSubscriptions` helper allows you to assert that a `cold()` or `hot()` Observable you created was subscribed/unsubscribed to at the correct point in time. The subscription marble syntax is slightly different to conventional marble syntax. + +- `'-'` time: 1 frame time passing. +- `[0-9]+[ms|s|m]` time progression: the time progression syntax lets you progress virtual time by a specific amount. It's a number, followed by a time unit of `ms` (milliseconds), `s` (seconds), or `m` (minutes) without any space between them, e.g. `a 10ms b`. See [Time progression syntax](#Time-progression-syntax) for more details. +- `'^'` subscription point: shows the point in time at which a subscription happen. +- `'!'` unsubscription point: shows the point in time at which a subscription is unsubscribed. + +There should be **at most one** `^` point in a subscription marble diagram, and **at most one** `!` point. Other than that, the `-` character is the only one allowed in a subscription marble diagram. + +### Examples + +`'-'` or `'------'`: no subscription ever happened. + +`'--^--'`: a subscription happened after 2 "frames" of time passed, and the subscription was not unsubscribed. + +`'--^--!-'`: on frame 2 a subscription happened, and on frame 5 was unsubscribed. + +`'500ms ^ 1s !'`: on frame 500 a subscription happened, and on frame 1,501 was unsubscribed. + +*** + +## Known Issues + +#### You can't directly test RxJS code that consumes Promises or uses any of the other schedulers (e.g. AsapScheduler) + +If you have RxJS code that uses any other form of async scheduling other than AsyncScheduler, e.g. Promises, AsapScheduler, etc. you can't reliably use marble diagrams _for that particular code_. This is because those other scheduling methods won't be virtualized or known to TestScheduler. + +The solution is to test that code in isolation, with the traditional async testing methods of your testing framework. The specifics depend on your testing framework of choice, but here's a pseudo-code example: + +```ts +// Some RxJS code that also consumes a Promise, so TestScheduler won't be able +// to correctly virtualize and the test will always be really async +const myAsyncCode = () => from(Promise.resolve('something')); + +it('has async code', done => { + myAsyncCode().subscribe(d => { + assertEqual(d, 'something'); + done(); + }); +}); +``` + +On a related note, you also can't currently assert delays of zero, even with AsyncScheduler, e.g. `delay(0)` is like saying `setTimeout(work, 0)`. This schedules a new ["task" aka "macrotask"](https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/), so it's async, but without an explicit passage of time. + +#### Behavior is different outside of `testScheduler.run(callback)` + +The TestScheduler has been around since v5, but was actually intended for testing RxJS itself by the maintainers, rather than for use in regular user apps. Because of this, some of the default behaviors and features of the TestScheduler didn't work well (or at all) for users. In v6 we introduced the `testScheduler.run(callback)` method which allowed us to provide new defaults and features in a non-breaking way, but it's still possible to [use the TestScheduler outside](./internal-marble-tests.md) of `testScheduler.run(callback)`. It's important to note that if you do so, there are some major differences in how it will behave. + +* TestScheduler helper methods have more verbose names, like `testScheduler.createColdObservable()` instead of `cold()` +* The testScheduler instance is NOT automatically be used by operators that uses AsyncScheduler, e.g. delay, debounceTime, etc so you have to explicitly pass it to them. +* There is NO support for time progression syntax e.g. `-a 100ms b-|` +* 1 frame is 10 virtual milliseconds by default. i.e. `TestScheduler.frameTimeFactor = 10` +* Each space ` ` equals 1 frame, same as a hyphen `-`. +* There is a hard maximum number of frames set at 750 i.e. `maxFrames = 750`. After 750 they are silently ignored. +* You must explicitly flush the scheduler + +While at this time usage of the TestScheduler outside of `testScheduler.run(callback)` has not been officially deprecated, it is discouraged because it is likely to cause confusion. diff --git a/doc/writing-marble-tests.md b/doc/writing-marble-tests.md index f3420bc581..3bf9b9a67b 100644 --- a/doc/writing-marble-tests.md +++ b/doc/writing-marble-tests.md @@ -1,186 +1,2 @@ -# Writing Marble Tests - -"Marble Tests" are tests that use a specialized VirtualScheduler called the `TestScheduler`. They enable us to test -asynchronous operations in a synchronous and dependable manner. The "marble notation" is something that's been adapted -from many teachings and documents by people such as @jhusain, @headinthebox, @mattpodwysocki and @staltz. In fact, -@staltz first recommended this as a DSL for creating unit tests, and it has since been altered and adopted. - -#### links - -- [Contribution](../CONTRIBUTING.md) -- [Code of Conduct](../CODE_OF_CONDUCT.md) - -## Basic methods - -The unit tests have helper methods that have been added to make creating tests easier. - -- `hot(marbles: string, values?: object, error?: any)` - creates a "hot" observable (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. -- `cold(marbles: string, values?: object, error?: any)` - creates a "cold" observable whose subscription starts when - the test begins. -- `expectObservable(actual: Observable).toBe(marbles: string, values?: object, error?: any)` - schedules an assertion - for when the TestScheduler flushes. The TestScheduler will automatically flush at the end of your jasmine `it` block. -- `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. - -### Ergonomic defaults for `hot` and `cold` - -In both `hot` and `cold` methods, value characters specified in marble diagrams are emitted as strings unless a `values` -argument is passed to the method. Therefor: - -`hot('--a--b')` will emit `"a"` and `"b"` whereas - -`hot('--a--b', { a: 1, b: 2 })` will emit `1` and `2`. - -Likewise, unspecified errors will just default to the string `"error"`, so: - -`hot('---#')` will emit error `"error"` whereas - -`hot('---#', null, new SpecialError('test'))` will emit `new SpecialError('test')` - - -## Marble Syntax - -Marble syntax is a string which represents events happening over "time". The first character of any marble string - -always represents the "zero frame". A "frame" is somewhat analogous to a virtual millisecond. - -- `"-"` time: 10 "frames" of time passage. -- `"|"` complete: The successful completion of an observable. This is the observable producer signaling `complete()` -- `"#"` error: An error terminating the observable. This is the observable producer signaling `error()` -- `"a"` any character: All other characters represent a value being emitted by the producer signaling `next()` -- `"()"` sync groupings: When multiple events need to be in the same frame synchronously, parentheses are used - to group those events. You can group nexted values, a completion or an error in this manner. The position of the - initial `(` determines the time at which its values are emitted. -- `"^"` 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. - -### Examples - -`'-'` or `'------'`: Equivalent to `Observable.never()`, or an observable that never emits or completes - -`|`: Equivalent to `Observable.empty()` - -`#`: Equivalent to `Observable.throw()` - -`'--a--'`: An observable that waits 20 "frames", emits value `a` and then never completes. - -`'--a--b--|'`: On frame 20 emit `a`, on frame 50 emit `b`, and on frame 80, `complete` - -`'--a--b--#'`: On frame 20 emit `a`, on frame 50 emit `b`, and on frame 80, `error` - -`'-a-^-b--|'`: In a hot observable, on frame -20 emit `a`, then on frame 20 emit `b`, and on frame 50, `complete`. - -`'--(abc)-|'`: on frame 20, emit `a`, `b`, and `c`, then on frame 80 `complete` - -`'-----(a|)'`: on frame 50, emit `a` and `complete`. - -## Subscription Marble Syntax - -The subscription marble syntax is slightly different to conventional marble syntax. It represents the **subscription** and an **unsubscription** points happening over time. There should be no other type of event represented in such diagram. - -- `"-"` time: 10 "frames" of the passage. -- `"^"` subscription point: shows the point in time at which a subscription happen. -- `"!"` unsubscription point: shows the point in time at which a subscription is unsubscribed. - -There should be **at most one** `^` point in a subscription marble diagram, and **at most one** `!` point. Other than that, the `-` character is the only one allowed in a subscription marble diagram. - -### Examples - -`'-'` or `'------'`: no subscription ever happened. - -`'--^--'`: a subscription happened after 20 "frames" of time passed, and the subscription was not unsubscribed. - -`'--^--!-'`: on frame 20 a subscription happened, and on frame 50 was unsubscribed. - -## Anatomy of a Test - -A basic test might look as follows: - -```js - -var e1 = hot('----a--^--b-------c--|'); -var e2 = hot( '---d-^--e---------f-----|'); -var expected = '---(be)----c-f-----|'; - -expectObservable(e1.merge(e2)).toBe(expected); -``` - -- The `^` characters of `hot` observables should **always** be aligned. -- The **first character** of `cold` observables or expected observables should **always** be aligned - with each other, and with the `^` of hot observables. -- Use default emission values when you can. Specify `values` when you have to. - -A test example with specified values: - -```js -var values = { - a: 1, - b: 2, - c: 3, - d: 4, - x: 1 + 3, // a + c - y: 2 + 4, // b + d -} -var e1 = hot('---a---b---|', values); -var e2 = hot('-----c---d---|', values); -var expected = '-----x---y---|'; - -expectObservable(e1.zip(e2, function(x, y) { return x + y; })) - .toBe(expected, values); -``` - -- Use the same hash to look up all values, this ensures that multiple uses of the same character have the - same value. -- Make the result values as obvious as possible as to what they represent, these are *tests* afterall, we want - clarity more than efficiency, so `x: 1 + 3, // a + c` is better than just `x: 4`. The former conveys *why* it's 4, - the latter does not. - -A test example with subscription assertions: - -```js -var x = cold( '--a---b---c--|'); -var xsubs = '------^-------!'; -var y = cold( '---d--e---f---|'); -var ysubs = '--------------^-------------!'; -var e1 = hot( '------x-------y------|', { x: x, y: y }); -var expected = '--------a---b----d--e---f---|'; - -expectObservable(e1.switch()).toBe(expected); -expectSubscriptions(x.subscriptions).toBe(xsubs); -expectSubscriptions(y.subscriptions).toBe(ysubs); -``` - -- Align the start of `xsubs` and `ysubs` diagrams with `expected` diagram. -- Notice how the `x` cold observable is unsubscribed at the same time `e1` emits `y`. - -In most tests it will be unnecessary to test subscription and unsubscription points, being either obvious or implied from the `expected` diagram. In those cases do not write subscription assertions. In test cases that have inner subscriptions or cold observables with multiple subscribers, these subscription assertions can be useful. - -## Generating PNG marble diagrams from tests - -Typically, each test case in Jasmine is written as `it('should do something', function () { /* ... */ })`. To mark a test case for PNG diagram generation, you must use the `asDiagram(label)` function, like this: - - -```js -it.asDiagram(operatorLabel)('should do something', function () { - // ... -}); -``` - -For instance, with `zip`, we would write - -```js -it.asDiagram('zip')('should zip by concatenating', function () { - var e1 = hot('---a---b---|'); - var e2 = hot('-----c---d---|'); - var expected = '-----x---y---|'; - var values = { x: 'ac', y: 'bd' }; - - var result = e1.zip(e2, function(x, y) { return String(x) + String(y); }); - - expectObservable(result).toBe(expected, values); -}); -``` - -Then, when running `npm run tests2png`, this test case will be parsed and a PNG file `zip.png` (filename determined by `${operatorLabel}.png`) will be created in the `img/` folder. +- [USERS: Writing marble diagram tests for your application](./marble-testing.md). +- [INTERNAL MAINTAINERS ONLY: Writing marble diagram tests for the RxJS internals](./internal-marble-tests.md).