[RxJS] Introduction to RxJS Marble Testing

Marble testing is an expressive way to test observables by utilizing marble diagrams. This lesson will walk you through the syntax and features, preparing you to start writing marble tests today!

 

Grep two files from the rxjs

  • https://github.com/ReactiveX/rxjs/blob/master/spec/helpers/marble-testing.ts
  • https://github.com/ReactiveX/rxjs/blob/master/spec/helpers/test-helper.ts

 

复制代码
/*
    RxJS marble testing allows for a more natural style of testing observables.
    To get started, you need to include a few helpers libraries, marble-testing.ts and test-helper.ts,
    in your karma.conf or wallaby.js configuration file. 
    These files provide helpers for parsing marble diagrams and asserting against the subscription points and result
    of your observables under test. For these examples I will be using Jasmine, but Mocha and Chai works just as well.

    Let's get started with the basics of marble testing!

    First, let's understand the pieces that make up a valid marble diagram.

    Dash: Indicates a passing of time, you can think of each dash as 10ms when it comes to your tests.
    -----                 <----- 50ms
    Characters: Each character inside the dash indicates an emission.
    -----a-----b-----c    <----- Emit 'a' at 60ms, 'b' at 120ms, 'c' at 180ms
    Pipes |: Pipes indicate the completion point of an observable.
    -----a|               <----- Emit 'a' at 60ms then complete (70ms)
    Parenthesis (): Parenthesis indicate multiple emissions in same time frame, think Observable.of(1,2,3)
    -----(abc|)           <----- Emit 'a''b''c' at 60ms then complete (60ms)
    Caret ^: Indicates the starting point of a subscription, used with expectSubscription assertion.
    ^-------              <----- Subscription point at caret.
    Exclamation Point - !: Indicates the end point of a subscription, also used with expectSubscription assertion.
    ^------!              <----- Subscription starts at caret, ends at exclamation point.
    Pound Sign - #: Indicates an error
    ---a---#              <----- Emit 'a' at 40ms, error at 80ms
    There are also a few methods included to parse marble sequences and transpose values.

    cold(marbles: string, values?: object, error?: any) : Subscription starts when test begins
    cold(--a--b--|, {a: 'Hello', b: 'World'})           <----- Emit 'Hello' at 30ms and 'World' at 60ms, complete at 90ms
    hot(marbles: string, values?: object, error?: any) : Behaves like subscription starts at point of caret
    hot(--^--a---b--|, {a: 'Goodbye', b: 'World'})      <----- Subscription begins at point of caret
*/
复制代码

 

 

For example we want to test:

const source =       "---a---b---c--|";
const expected =   "---a---b---c--|";

they should be equal. 

 

Here each '-' means 1. frames.

'|' means completed.

 

The method we need to use is 'expectObservable' & 'cold':

    it('should parse marble diagrams', () => {
        const source = cold('---a---b---c---|');
        const expected =    '---a---b---c---|';

        expectObservable(source).toBe(expected)
    });

Cold will treat the beginning of the diagram as a subscription point. Now the test passing.

 

But if we change a little bit:

    it('should parse marble diagrams', () => {
        const source = cold('---a---b---c---|');
        const expected =    '---a--b---c---|';

        expectObservable(source).toBe(expected)
    });

It reports error:

复制代码
    Expected 
    {"frame":30,"notification":{"kind":"N","value":"a","hasValue":true}}
    {"frame":70,"notification":{"kind":"N","value":"b","hasValue":true}}
    {"frame":110,"notification":{"kind":"N","value":"c","hasValue":true}}
    {"frame":150,"notification":{"kind":"C","hasValue":false}}
    
    to deep equal 
    {"frame":30,"notification":{"kind":"N","value":"a","hasValue":true}}
    {"frame":60,"notification":{"kind":"N","value":"b","hasValue":true}}
    {"frame":100,"notification":{"kind":"N","value":"c","hasValue":true}}
    {"frame":140,"notification":{"kind":"C","hasValue":false}}
复制代码

 

Test 'concat' opreator:

    it('should work with cold observables', () => {
        const obs1 = cold('-a---b-|');
        const obs2 = cold('-c---d-|');
        const expectedConcatRes = '-a---b--c---d-|';

        expectObservable(obs1.concat(obs2)).toBe(expectedConcatRes)
    });

 

'Hot' observable: Hot will actually let you identify the subscription point yourself:

When testing hot observables you can specify the subscription point using a caret '^', similar to how you specify subscriptions when utilizing the expectSubscriptions assertion.

    it('should work with hot observables', () => {
        const obs1 =     hot('---a--^--b---|');
        const obs2 =  hot('-----c---^-----------------d-|');
        const expected =           '---b--------------d-|';

        expectObservable(obs1.concat(obs2)).toBe(expected);
    });

Algin the ^, easy for read

 

Spread subscription and marble diagram:

复制代码
    /*
        For certain operators you may want to confirm the point at which
        an observable is subscribed or unsubscribed. Marble testing makes this 
        possible by using the expectSubscriptions helper method. The cold and hot
        methods return a subscriptions object, including the frame at which the observable 
        would be subscribed and unsubscribed. You can then assert against these
        subscription points by supplying a diagram which indicates the expected behavior.

        ^ - Indicated the subscription point.
        ! - Indicates the point at which the observable was unsubscribed.

        Example subscriptions object: {"subscribedFrame":70,"unsubscribedFrame":140}
    */
    it('should identify subscription points', () => {
        const obs1 = cold('-a---b-|');
        const obs2 = cold('-c---d-|')
        const expected =  '-a---b--c---d-|';
        const sub1 =      '^------!'
        const sub2 =      '-------^------!'

        expectObservable(obs1.concat(obs2)).toBe(expected);
        expectSubscriptions(obs1.subscriptions).toBe(sub1);
        expectSubscriptions(obs2.subscriptions).toBe(sub2);
    })
复制代码

 

Object to map the key and value:

复制代码
    /*
        Both the hot and cold methods, as well the the toBe method accept an object map as a
        second parameter, indicating the values to output for the appropriate placeholder.
        When the test is executed these values rather than the matching string in the marble diagram.
    */
    it('should correctly sub in values', () => {
        const values = {a: 3, b: 2};
        const source = cold(  '---a---b---|', values);
        const expected =      '---a---b---|';

        expectObservable(source).toBe(expected, values);
    });
复制代码

 

复制代码
    /*
        Multiple emissions occuring in same time frame can be represented by grouping in parenthesis.
        Complete and error symbols can also be included in the same grouping as simulated outputs.
    */
    it('should handle emissions in same time frame', () => {
        const obs1 = Observable.of(1,2,3,4);
        const expected = '(abcd|)';

        expectObservable(obs1).toBe(expected, {a: 1, b: 2, c: 3, d: 4});
    });
复制代码

 

复制代码
    /*
        For asynchronous tests RxJS supplies a TestScheduler.
        How it works...
    */
    it('should work with asynchronous operators', () => {
        const obs1 = Observable
            .interval(10, rxTestScheduler)
            .take(5)
            .filter(v => v % 2 === 0);
        const expected = '-a-b-(c|)';

        expectObservable(obs1).toBe(expected, {a: 0, b: 2, c: 4});
    });
复制代码

 

Error handling:

复制代码
    /*
        Observables that encounter errors are represented by the pound (#) sign.
        In this case, our observable is retried twice before ultimately emitting an error.
        A third value can be supplied to the toBe method specifying the error to be matched.
    */
    it('should handle errors', () => {
        const source = Observable.of(1,2,3,4)
            .map(val => {
                if(val > 3){
                    throw 'Number too high!';
                };
                return val;
            })
        .retry(2);

        const expected = '(abcabcabc#)';

        expectObservable(source).toBe(expected, {a: 1, b: 2, c: 3, d: 4}, 'Number too high!');
    }); 
复制代码

 

posted @   Zhentiw  阅读(808)  评论(0编辑  收藏  举报
编辑推荐:
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
历史上的今天:
2015-08-22 [Node.js] Web Scraping with Pagination and Advanced Selectors
2014-08-22 [jQuery] Custom event trigger
2014-08-22 [jQuery] $.map, $.each, detach() , $.getJSOIN()
点击右上角即可分享
微信分享提示