Angular Testing

From bibbleWiki
Jump to navigation Jump to search

Jasmine

Simple Example

Angular by default using Jasmine for testing. Test follow the same approach as other frameworks. You can use pending and fail to simulate pending and guess what, failing.

describe('CalculatorService', ()=> {

    it('Should add two numbers', () => {
        const logger = new LoggerService()
        spyOn(logger, 'log')

        const calculator  = new CalculatorService(logger)
        const result = calculator.add(2,2)
        expect(result).toBe(4)
        expect(logger.log).toHaveBeenCalledTimes(1)
    });

})

Spies

These allow you to track the things about the objects. In this simple example we are wanting to track how many time the function log was called on the LoggerService.

describe('CalculatorService', ()=> {

    it('Should add two numbers', () => {
        const logger = new LoggerService()
        spyOn(logger, 'log')

        const calculator  = new CalculatorService(new LoggerService())
        const result = calculator.add(2,2)
        expect(result).toBe(4)

        expect(logger.log).toHaveBeenCalledTimes(1)
    });

})

In reality we should not use dependant services because the error could be in the logger service and not the one being tested. So we need to create a mock of the logger service using a SpyObject. Spy Objects are automatically spied on. We also use the beforeEach to simplify the test

    let calculator: CalculatorService
    let loggerSpy: any

    beforeEach(()=> {
        calculator  = new CalculatorService(new LoggerService())
        loggerSpy = jasmine.createSpyObj('LoggerService', ["log"])
    })

    it('Should add two numbers', () => {
        const result = calculator.add(2,2)
        expect(result).toBe(4)
        expect(loggerSpy.log).toHaveBeenCalledTimes(1)
    });
...
    it('Should add two numbers', () => {
        const logger = jasmine.createSpyObj('LoggerService', ["log"])
        const calculator  = new CalculatorService(new LoggerService())
        const result = calculator.add(2,2)
        expect(result).toBe(4)

        expect(logger.log).toHaveBeenCalledTimes(1)
    });
})

Dependency Injection

In the above example we call new on the CalculatorService. The better way to do this is to use the TestBed.configureTestingModule where we use a similar approach to the running code. We specify the class being tested as a provider from TestBed and we use our mocked versions of the logger to ensure we are ONLY testing the service.

    beforeEach(()=> {
        loggerSpy = jasmine.createSpyObj('LoggerService', ["log"])

        TestBed.configureTestingModule({
            providers: [
                CalculatorService,
                {provide: LoggerService, useValue: loggerSpy}
            ]
        })

        calculator = TestBed.inject(CalculatorService)
    })

Example Testing HTTP Requests

Request to be Tested

Here is an example of testing the HTTP module from angular. Angular comes with its own testing client so this is fairly simple. The example shows a typical use of subscribe. Below is the method to be tested.

    findAllCourses(): Observable<Course[]> {
        return this.http.get('/api/courses')
            .pipe(
                map(res => res['payload'])
            );
    }

Setting up the Test

We setup the http testing client

    beforeEach(()=> {

        TestBed.configureTestingModule({
            imports: [HttpClientTestingModule],
            providers: [
                CoursesService,
            ]
        })

        coursesService = TestBed.inject(CoursesService)
        httpTestingController = TestBed.inject(HttpTestingController)
    })

Test case for GET Request

Now we can write our tests. It is only when flush is called that the subscribe part of the test is run.

    it('should retrieve all course',()=> {

        coursesService.findAllCourses().subscribe(
            courses => {
                // Not undefined, NULL
                expect(courses).toBeTruthy()

                // length of array returned
                expect(courses.length).toBe(12)

                // Get a course to examine
                const course = courses.find(course => course.id == 12)

                // Test a property
                expect(course.titles.description).toBe("Angular Testing Course")
            })

        const req = httpTestingController.expectOne('/api/courses')
        expect(req.request.method).toEqual("GET")
        req.flush({payload: Object.values(COURSES)})
    })

Verifying no Other Requests

At the end of each request we call verify on the controller. This ensures no unmatched requests are outstanding.

    afterEach(()=>{
        httpTestingController.verify()
    })

How to test Errors in Requests

Here is an example of testing an error. We deliberately flush with a status:500. Note that we fail the test if the next() function is called.

    it('should give an error if save course fails', () => {

        const changes :Partial<Course> = {titles:{description: 'Testing Course'}};

        coursesService.saveCourse(12, changes)
            .subscribe({
                next:() => fail("the save course operation should have failed"),

                error:(error: HttpErrorResponse) => {
                    expect(error.status).toBe(500);
                }
            });

        const req = httpTestingController.expectOne('/api/courses/12');

        expect(req.request.method).toEqual("PUT");

        req.flush('Save course failed', {status:500,
            statusText:'Internal Server Error'});
    });

How to test a request with Parameters

This is very similar to the first GET request but maybe worth showing how to approach this. Here is the code, not the use of default arguments to make the test easier.

    findLessons(
        courseId:number, filter = '', sortOrder = 'asc',
        pageNumber = 0, pageSize = 3):  Observable<Lesson[]> {

        return this.http.get('/api/lessons', {
            params: new HttpParams()
                .set('courseId', courseId.toString())
                .set('filter', filter)
                .set('sortOrder', sortOrder)
                .set('pageNumber', pageNumber.toString())
                .set('pageSize', pageSize.toString())
        }).pipe(
            map(res =>  res["payload"])
        );
    }

And the test case. I think the point was not to have complex request urls and to separate the testing of parameters.

    it('should find a list of lessons', () => {

        coursesService.findLessons(12).subscribe(lessons => {
                expect(lessons).toBeTruthy();
                expect(lessons.length).toBe(3);
            });

        const req = httpTestingController.expectOne(
            req => req.url == '/api/lessons');

        expect(req.request.method).toEqual("GET");
        expect(req.request.params.get("courseId")).toEqual("12");
        expect(req.request.params.get("filter")).toEqual("");
        expect(req.request.params.get("sortOrder")).toEqual("asc");
        expect(req.request.params.get("pageNumber")).toEqual("0");
        expect(req.request.params.get("pageSize")).toEqual("3");

        req.flush({
            payload: findLessonsForCourse(12).slice(0,3)
        });
    });

Jasmine Tip

Putting "f" before the "it" or "describe" means on that test case/test suite will run, putting "x" e.g. xdescribe, xit will exclude this test case/test suite.

Testing Presentation components

Setup

When you want to test components you need to make sure the testbed has all of the necessary dependencies. Generally you can do this by importing the module with the component and the modules it depends on. When we import the module we need to compile the components. We do this with compileComponents which returns a promise as it is asynchronous there for we need a waitForAsync (used to be called async) to ensure before is executed BEFORE a test. This is not the await/async in the language but a utility provided by Angular. Lastly Angular provides ComponentFixture which provides an API for testing components. We have also created an debugElement which we can use to query the DOM in the test specifications.

describe("CoursesCardListComponent", () => {
  let component: CoursesCardListComponent;
  let fixture: ComponentFixture<CoursesCardListComponent>;
  let el: DebugElement;

  beforeEach(
    waitForAsync(() => {
      TestBed.configureTestingModule({
        imports: [CoursesModule],
      })
        .compileComponents()
        .then(() => {
          fixture = TestBed.createComponent(CoursesCardListComponent);
          component = fixture.componentInstance;
          el = fixture.debugElement;
        });
    })
  );
...

Test Presentation Template we are Testing

This is the example template we are testing in the tests below

<mat-card *ngFor="let course of courses" class="course-card mat-elevation-z10">

    <mat-card-header>

        <mat-card-title>{{course.titles.description}}</mat-card-title>

    </mat-card-header>

    <img mat-card-image [src]="course.iconUrl">

    <mat-card-content>
        <p>{{course.titles.longDescription}}</p>
    </mat-card-content>

    <mat-card-actions class="course-actions">

        <button mat-button class="mat-raised-button mat-primary" [routerLink]="['/courses', course.id]">
            VIEW COURSE
        </button>

        <button mat-button class="mat-raised-button mat-accent"
                (click)="editCourse(course)">
            EDIT
        </button>

    </mat-card-actions>

</mat-card>

Tests Cases

When testing component we need to ensure that the data has been applied to the component. We do this by calling detectChanges() on the fixture. Also shown is how to output the html which is useful for debugging

  it("should display the course list", () => {

    // Assign the data to the component
    component.courses = Object.values(COURSES).sort(sortCoursesBySeqNo) as Course[];
    
    // When we assign data we need to notify component
    fixture.detectChanges()

    // Debug an element with
    console.log(el.nativeElement.outerHTML)

    // lets get the cards and test expectations
    const cards = el.queryAll(By.css(".course-card"))
    expect(cards).toBeTruthy()
    expect(cards.length).toEqual(12)
  });

Let see how to test parts of the component using the query to get the first card. This is probably a good example of how to extract parts to the elements in a template.

  it("should display the first course", () => {

    component.courses = Object.values(COURSES).sort(sortCoursesBySeqNo) as Course[];
    fixture.detectChanges()

    const course = component.courses[0]
    const card = el.query(By.css(".course-card:first-child"))
    const title = card.query(By.css("mat-card-title"))
    const image = card.query(By.css("img"))
    expect(card).toBeTruthy()
    expect(title.nativeElement.textContent).toBe(course.titles.description)
    expect(image.nativeElement.src).toBe(course.iconUrl)
  });

Testing Container components

Setup

To setup is much like the presentation setup. This component uses a service so we must create an instance to the service, create a spy and specify the method we will be mocking and specify it in the providers object. We need to specify the provide statement for it. Also, in this case, there are angular animations so we need to also use the NoopAnimationsModule which stubs the animations.

describe('HomeComponent', () => {

  let fixture: ComponentFixture<HomeComponent>;
  let component:HomeComponent;
  let el: DebugElement;
  let coursesService: any

  beforeEach(waitForAsync(() => {
    let coursesServiceSpy = jasmine.createSpyObj('CoursesService', ['findAllCourses', 'anotherMethod'])

      TestBed.configureTestingModule({
        imports: [CoursesModule,NoopAnimationsModule],
        providers: [
          {provide: CoursesService, useValue: coursesServiceSpy}
        ]
      }).compileComponents().then(() =>{
        fixture = TestBed.createComponent(HomeComponent)
        component = fixture.componentInstance
        el = fixture.debugElement
        coursesService = TestBed.inject(CoursesService)
      })
  }));

Test Container Template we are Testing

Here is the template we will be testing

<div class="container">

    <h3>All Courses</h3>

    <mat-tab-group>


      <ng-container *ngIf="(beginnerCourses$ | async) as beginnerCourses">

        <mat-tab label="Beginners" *ngIf="beginnerCourses?.length > 0">

          <courses-card-list (courseEdited)="reloadCourses()"
                             [courses]="beginnerCourses">

          </courses-card-list>

        </mat-tab>

      </ng-container>


      <ng-container  *ngIf="(advancedCourses$ | async) as advancedCourses">

        <mat-tab label="Advanced" *ngIf="advancedCourses?.length > 0">

          <courses-card-list (courseEdited)="reloadCourses()"
                             [courses]="advancedCourses">


          </courses-card-list>

        </mat-tab>

      </ng-container>

    </mat-tab-group>

</div>

Test Cases

Testing the ngIf

In this test case we will be testing it only the beginner courses exist by creating an observable for just the begginers course and querying the template to ensure only one tab is present. Angular Component Testing 30.png

  it("should display only beginner courses", () => {
     
    const courses = Object.values(COURSES).sort(sortCoursesBySeqNo) as Course[];
    const onlyBeginnerCourses = courses.filter(course => course.category == 'BEGINNER')
    coursesService.findAllCourses.and.returnValue(of(onlyBeginnerCourses))
    fixture.detectChanges()

    const tabs = el.queryAll(By.css(".mat-tab-label"))
    expect(tabs.length).toBe(1)
    pending();

  });

Understanding Asynchronous Testing

In this test we are simulating an method on element. They provided two approaches, one via the debugElement or they provided a utility function

export function click(el: DebugElement | HTMLElement, eventObj: any = ButtonClickEvents.left): void {
  if (el instanceof HTMLElement) {
    el.click();
  } else {
    el.triggerEventHandler('click', eventObj);
  }  
}

The original test is written and it fails due to the asynchronous nature of a button click.

  it("should display advanced courses when tab clicked", () => {

    const courses = Object.values(COURSES).sort(sortCoursesBySeqNo) as Course[];
    const allCourses = courses
    coursesService.findAllCourses.and.returnValue(of(allCourses))
    fixture.detectChanges()

    const tabs = el.queryAll(By.css(".mat-tab-label"))
  
    click(tabs[1])
    fixture.detectChanges()

    const cardTitles = el.queryAll(By.css('mat-card-title'))
    expect(cardTitles.length).toBeGreaterThan(0)
    expect(cardTitles[0].nativeElement.textContent).toContain("Angular Security Course")
  });

Next they used the setTimeout which did not work and then jasmine done() utility.

  it("should display advanced courses when tab clicked", (done: DoneFn) => {

    const courses = Object.values(COURSES).sort(sortCoursesBySeqNo) as Course[];
    const allCourses = courses
    coursesService.findAllCourses.and.returnValue(of(allCourses))
    fixture.detectChanges()

    const tabs = el.queryAll(By.css(".mat-tab-label"))
  
    click(tabs[1])
    fixture.detectChanges()

    setTimeout(()=>{
    
      const cardTitles = el.queryAll(By.css('.mat-tab-body-active .mat-card-title'));
      console.log(cardTitles);
      expect(cardTitles.length).toBeGreaterThan(0,"Could not find card titles");
      expect(cardTitles[0].nativeElement.textContent).toContain("Angular Security Course");    
      done()
    }, 4000)

  });

This way of writing tests is not nice to angular provided fakeAsync which tracks the timers and allows you to flush them before executing the tests.

  it("should display advanced courses when tab clicked", fakeAsync((done: DoneFn) => {

    const courses = Object.values(COURSES).sort(sortCoursesBySeqNo) as Course[];
    const allCourses = courses
    coursesService.findAllCourses.and.returnValue(of(allCourses))
    fixture.detectChanges()

    const tabs = el.queryAll(By.css(".mat-tab-label"))
  
    click(tabs[1])
    fixture.detectChanges();
    
    flush()

    const cardTitles = el.queryAll(By.css('.mat-tab-body-active .mat-card-title'));
    console.log(cardTitles);
    expect(cardTitles.length).toBeGreaterThan(0,"Could not find card titles");
    expect(cardTitles[0].nativeElement.textContent).toContain("Angular Security Course");    

  }));

We can achieve the same with the waitForAsync and fixture.whenStable()

  it("should display advanced courses when tab clicked", waitForAsync((done: DoneFn) => {

    const courses = Object.values(COURSES).sort(sortCoursesBySeqNo) as Course[];
    const allCourses = courses
    coursesService.findAllCourses.and.returnValue(of(allCourses))
    fixture.detectChanges()

    const tabs = el.queryAll(By.css(".mat-tab-label"))
  
    click(tabs[1])
    fixture.detectChanges();
    
    fixture.whenStable().then(()=> {
      const cardTitles = el.queryAll(By.css('.mat-tab-body-active .mat-card-title'));
      console.log(cardTitles);
      expect(cardTitles.length).toBeGreaterThan(0,"Could not find card titles");
      expect(cardTitles[0].nativeElement.textContent).toContain("Angular Security Course");    
 
    })
  }));

Macro and Micro Tasks

In javascript there are two types of tasks, micro and macro tasks. This is important because they are queued differently.
All micro-tasks logged are processed in one fell swoop in a single macro-task execution cycle. In comparison, the macro-task queue has a lower priority. Macro-tasks include parsing HTML, generating DOM, executing main thread JavaScript code and other events such as page loading, input, network events, timer events, etc.

Example MacroTasks: setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI Rendering.
Examples MicroTasks process.nextTick, Promises, queueMicrotask, MutationObserver
Below are some examples of dealing with Asynchronous.

import {fakeAsync, flush, flushMicrotasks, tick} from '@angular/core/testing';
import {of} from 'rxjs';
import {delay} from 'rxjs/operators';


describe('Async Testing Examples', () => {

    it('Asynchronous test example with Jasmine done()', (done: DoneFn) => {

        let test = false;

        setTimeout(() => {

            console.log('running assertions');

            test = true;

            expect(test).toBeTruthy();

            done();

        }, 1000);

    });


    it('Asynchronous test example - setTimeout()', fakeAsync(() => {

        let test = false;

        setTimeout(() => {
        });

        setTimeout(() => {

            console.log('running assertions setTimeout()');

            test = true;

        }, 1000);

        flush();

        expect(test).toBeTruthy();

    }));


    it('Asynchronous test example - plain Promise', fakeAsync(() => {

        let test = false;

        console.log('Creating promise');

        Promise.resolve().then(() => {

            console.log('Promise first then() evaluated successfully');

            return Promise.resolve();
        })
        .then(() => {

            console.log('Promise second then() evaluated successfully');

            test = true;

        });

        flushMicrotasks();

        console.log('Running test assertions');

        expect(test).toBeTruthy();

    }));


    it('Asynchronous test example - Promises + setTimeout()', fakeAsync(() => {

        let counter = 0;

        Promise.resolve()
            .then(() => {

               counter+=10;

               setTimeout(() => {

                   counter += 1;

               }, 1000);

            });

        expect(counter).toBe(0);

        flushMicrotasks();

        expect(counter).toBe(10);

        tick(500);

        expect(counter).toBe(10);

        tick(500);

        expect(counter).toBe(11);

    }));

    it('Asynchronous test example - Observables', fakeAsync(() => {

        let test = false;

        console.log('Creating Observable');

        const test$ = of(test).pipe(delay(1000));

        test$.subscribe(() => {

            test = true;

        });

        tick(1000);

        console.log('Running test assertions');

        expect(test).toBe(true);


    }));


});

E2E Testing with Cypress

Cypress is tool independent of Angular for perform E2E testing. In the npm we can put in commands to build production, start production server and run e2e tests. May need to look at start-server-and-test.

{
  "name": "angular-testing-course",
  "version": "0.0.0",
  "license": "MIT",
  "scripts": {
    "ng": "ng",
    "start": "ng serve  --proxy-config ./proxy.json",
    "server": "ts-node -P ./server/server.tsconfig.json ./server/server.ts",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "cypress:open": "cypress open",
    "cypress:run": "cypress run",
    "build:prod": "ng build --prod",
    "start:prod": "http-server ./dist -a localhost -p 4200",
    "build-and-start:prod": "run-s build:prod start:prod",
    "e2e": "start-server-and-test build-and-start:prod http://localhost:4200 cypress:run"...

This example was very simplistic so may need to look at more detail around this and the alternatives to cypress. Here is the test case.

describe('Home Page', () => {

    beforeEach(() => {

        cy.fixture('courses.json').as("coursesJSON");
        cy.server();
        cy.route('/api/courses', "@coursesJSON").as("courses");
        cy.visit('/');

    });

    it('should display a list of courses', () => {

        cy.contains("All Courses");
        cy.wait('@courses');
        cy.get("mat-card").should("have.length", 9);

    });

    it('should display the advanced courses', () => {

        cy.get('.mat-tab-label').should("have.length", 2);
        cy.get('.mat-tab-label').last().click();
        cy.get('.mat-tab-body-active .mat-card-title').its('length').should('be.gt', 1);

        cy.get('.mat-tab-body-active .mat-card-title').first()
            .should('contain', "Angular Security Course");

    });

});

The cypress:open provides the user interface for running the tests. The cypress:run is for getting the results at the cli

Setting up Karma

This took a lifetime to set up and make me wonder if I am using the right tool. It comes with Angular to stayed with it. What took the time was setting up the console.log to work. Here is my karma.conf.js.

module.exports = function (config) {
  config.set({
    basePath: "",
    frameworks: ["jasmine", "@angular-devkit/build-angular"],
    plugins: [
      require("karma-jasmine"),
      require("karma-chrome-launcher"),
      require("karma-jasmine-html-reporter"),
      require("karma-coverage-istanbul-reporter"),
      require("karma-coverage"),
      require("@angular-devkit/build-angular/plugins/karma"),
    ],
    client: {
      clearContext: false, // leave Jasmine Spec Runner output visible in browser
    },
    browserConsoleLogOptions: {
      terminal: true,
      level: ""
    },    
    jasmineHtmlReporter: {
      suppressAll: true // removes the duplicated traces
    },
    coverageIstanbulReporter: {
      dir: require("path").join(__dirname, "./coverage/debug-tests"),
      reports: ["html", "lcovonly", "text-summary"],
      fixWebpackSourcePaths: true,
    },
    coverageReporter: {
      dir: require('path').join(__dirname, './coverage/bibble-park-app'),
      subdir: '.',
      reporters: [
        { type: 'html' },
        { type: 'text-summary' }
      ]
    },
    reporters: ["progress", "kjhtml"],
    port: 9876,
    colors: true,
    logLevel: config.LOG_DEBUG,
    autoWatch: true,
    browsers: ["Chrome"],
    singleRun: false,
    restartOnFileChange: true,
  });
};

We can run the code coverage and display results with the following

# Run the coverage
ng test --watch=false ---code-coverage

# Add a global http-server if required
sudo npm install -g http-server

cd coverage

# Server content of current folder on localhost:8080
http-server -c-1 .

RxJs Testing

Decided to look a bit more into how to test observables. My starting point is Kevin Kreuzer's Medium page here.
The RxJs team provides and approach where you define a scheduler and a RunHelper

interface RunHelpers {
  cold: typeof TestScheduler.prototype.createColdObservable
  hot: typeof TestScheduler.prototype.createHotObservable
  flush: typeof TestScheduler.prototype.flush
  time: typeof TestScheduler.prototype.createTime
  expectObservable: typeof TestScheduler.prototype.expectObservable
  expectSubscriptions: typeof TestScheduler.prototype.expectSubscriptions
  animate: (marbles: string) => void
}

Let's define the code we are testing which is function which filters out odd numbers and multiplies the even ones by 10.

const evenTimesTen = (source$: Observable<number>): Observable<number> => {
  return source$.pipe(
    filter((n) => n % 2 === 0),
    map((n) => n * 10),
  )
}

Simple Test

Then a define the test case like any Jasmine case.

describe('ExampleRxjsTest', () => {
  let scheduler: TestScheduler

  beforeEach(() => {
    scheduler = new TestScheduler((actual, expected) => {
      expect(actual).toEqual(expected)
    })
  })

  it('should filter out odd numbers', () => {
    scheduler.run((runhelpers: RunHelpers) => {...})
  })

})

The author suggested that destructuring the interface implementation makes it a log easier to read. In this example we will define code and expectObservable.

/*
  it('should filter out odd numbers', () => {
    scheduler.run((runhelpers: RunHelpers) => {...})
  })
*/

  it('should filter out odd numbers', () => {
    scheduler.run(({ cold, expectObservable }) => {
      const values = { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6 }
      const source$ = cold('a-b-c-d-e-f|', values)
      const expectedMarble = '--a---b---c|'
      const expectedValues = { a: 20, b: 40, c: 60 }
      const result$ = evenTimesTen(source$)
      expectObservable(result$).toBe(expectedMarble, expectedValues)
    })
  })

Testing with an Error

I like this because I found my trusted divide by zero does not work with TS/Javascript. Much the same as above but this time we are testing for an error. Here is the function

const tenDivideByValue = (
  source$: Observable<number>,
): Observable<number> | Observable<unknown> => {
  return source$.pipe(
    map((n) => {
      const result = 10 / n
      console.log(`Value we got was ${result}`)
      if (!isFinite(result)) {
        throw new Error('Divide by zero')
      } else {
        return result
      }
    }),
  )
}

And here is the test case. The tricky part was understanding errorvalue meant an instance of Error.

  it('should fail on divide by zero', () => {
    const errorValue = new Error('Divide by zero')

    scheduler.run(({ cold, animate, expectObservable }) => {
      const values = { a: 5, b: 2, c: 10, d: 0 }
      const source$ = cold('--a---b---c---d---|', values)
      const expectedMarble = '--a---b---c---#'
      animate('       ----r----r---r--')
      const expectedValues = { a: 2, b: 5, c: 1 }
      const result$ = tenDivideByValue(source$)
      expectObservable(result$).toBe(expectedMarble, expectedValues, errorValue)
    })
  })

Testing with an catchError

Again a long struggle to get this working but here we go.

const tenDivideByValueCatchError = (
  source$: Observable<number>,
): Observable<number> | Observable<unknown> => {
  return source$.pipe(
    map((n) => {
      const result = 10 / n
      console.log(`Value we got was ${result}`)
      if (!isFinite(result)) {
        throw new Error('Divide by zero')
      } else {
        return result
      }
    }),
    catchError((error) => {
      return of({})
    }),
  )
}

And here is the test case. The tricky part this time was understanding how to define on frame 4 emit a and complete.

  it('should fail on divide by zero with catch', () => {
    scheduler.run(({ cold, expectObservable }) => {
      const values = { a: 5, b: 2, c: 10, d: 0 }
      const source$ = cold('--a---b---c---d|', values)
      const expectedMarble = '--a---b---c---(d|)'
      const expectedValues = { a: 2, b: 5, c: 1, d: {} }
      const result$ = tenDivideByValueCatchError(source$)
      expectObservable(result$).toBe(expectedMarble, expectedValues)
    })
  })

Testing with an concat with cold

Struggled again to work off the examples on the net but getting through it has allowed me to understand the concepts.

const concatHot = (
  source1$: Observable<number>,
  source2$: Observable<number>,
): Observable<number> => {
  return source1$.pipe(concat(source2$))
}

And the test case

  it('should work with cold observables', () => {
    scheduler.run(({ cold, expectObservable }) => {
      const values1 = { a: 5, b: 2 }
      const source1$ = cold('-a---b-|', values1)

      const values2 = { c: 10, d: 0 }
      const source2$ = cold('-c---d-|', values2)

      const expectedMarbles = '-a---b--c---d-|'
      const expectedValues = { a: 5, b: 2, c: 10, d: 0 }
      const result$ = concatCold(source1$, source2$)
      expectObservable(result$).toBe(expectedMarbles, expectedValues)
    })
  })

Testing with an concat with hot

I don't quite understand how we set the test up in real life. My guess it that the hot observable is mocking the timings of the release of the results.

const concatHot = (
  source1$: Observable<number>,
  source2$: Observable<number>,
): Observable<number> => {
  return source1$.pipe(concat(source2$))
}

Here is the test case which does make sense.

/*
    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', () => {
    scheduler.run(({ hot, expectObservable }) => {
      const values1 = { a: 5, b: 2 }
      const source1$ = hot('---a--^-b-|', values1)

      const values2 = { c: 10, d: 0 }
      const source2$ = hot('-----c^----d-|', values2)

      const expectedMarbles = '--b--d-|'
      const expectedValues = { b: 2, d: 0 }
      const result$ = concatHot(source1$, source2$)
      expectObservable(result$).toBe(expectedMarbles, expectedValues)
    })
  })

Marble Testing Anotation

You will see the source$ and expectedMarble have characters in them. This is a language that the library supports. Below is a list of some of these which can be found in detail at [1]

  • '-' or '------': Equivalent to NEVER, or an observable that never emits or errors or completes.
  • |: Equivalent to EMPTY, or an observable that never emits and completes immediately.
  • #: Equivalent to throwError, or an observable that never emits and errors immediately.
  • '--a--': An observable that waits 2 "frames", emits value a on frame 2 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 9,011 emit c, then on frame 9,012 complete.
  • '--a 2.5m b': on frame 2 emit a, on frame 150,003 emit b and never complete.