Testing Cloud Apps

Cloud Apps are written in Angular, which means they can leverage the Angular testing framework. Angular testing uses the Jasmine framework to write tests and the Karma runner to execute them. Cloud App base projects come with all of the configuration necessary to run tests, and the test command in the Cloud Apps CLI is used to execute the test suite.

$ eca test

There are many tutorials available online to learn about writing tests in Angular, including the official Angular testing guide.

The test command is available from version 1.0.0 of the Cloud App CLI. Existing apps will need to add dependencies to their package.json file. The devDependencies list can be found here.

Click below to view a video which walks through the process of setting up a test suite for your Cloud App:
32:51

Setup

When creating a test spec file, you need to configure the testing module. The basic configuration looks as follows:

import { waitForAsync, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { TestingComponent } from './testing.component';

describe('TestingComponent', () => {
  let component: TestingComponent;
  let fixture: ComponentFixture<TestingComponent>;

  beforeEach(waitForAsync(() => {
    TestBed.configureTestingModule({
      declarations: [ TestingComponent ],
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(TestingComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
})

Many Cloud App components use Material components, Reactive Forms, and routing. For those tests to compile, you need to include additional modules in your import statement:

import { waitForAsync, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { TestingComponent } from './testing.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
import { MaterialModule } from '@exlibris/exl-cloudapp-angular-lib';

describe('TestingComponent', () => {
  let component: TestingComponent;
  let fixture: ComponentFixture<TestingComponent>;

  beforeEach(waitForAsync(() => {
    TestBed.configureTestingModule({
      imports: [
        BrowserAnimationsModule,
        FormsModule,
        ReactiveFormsModule,
        MaterialModule,
        RouterTestingModule,
      ],
      declarations: [ TestingComponent ]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(TestingComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});

Cookbook

In this section, we’ll provide “recipes” for how to test some common Cloud App scenarios.

REST APIs

A common scenario is to call a REST API and display the results. Since we don’t want any external dependencies in our tests, we will need to provide a mock service and hard code the response. This example uses the Jasmine spy to intercept any requests to the CloudAppRestService and return a response.

import { Component, OnInit } from '@angular/core';
import { CloudAppRestService } from '@exlibris/exl-cloudapp-angular-lib';

@Component({
  selector: 'app-testing',
  templateUrl: './testing.component.html',
  styleUrls: ['./testing.component.css']
})
export class TestingComponent implements OnInit {

  constructor(
    private restService: CloudAppRestService,
  ) { }

  ngOnInit() {
    this.restService.call('/users')
    .subscribe(users => this.users = users.user);
  }
}
<ul id='users'>
  <li *ngFor="let user of users">{{user.first_name}} {{user.last_name}}</li>
</ul>
/* tslint:disable:no-unused-variable */
import { waitForAsync, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';

import { TestingComponent } from './testing.component';
import { CloudAppRestService } from '@exlibris/exl-cloudapp-angular-lib';

describe('TestingComponent', () => {
  let component: TestingComponent;
  let fixture: ComponentFixture<TestingComponent>;
  let restService: CloudAppRestService;
  let spy: jasmine.Spy;

  beforeEach(waitForAsync(() => {
    TestBed.configureTestingModule({
      declarations: [ TestingComponent ],
      providers: [ 
        CloudAppRestService,
      ]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(TestingComponent);
    component = fixture.componentInstance;
    compiled = fixture.debugElement.nativeElement;

    restService = fixture.debugElement.injector.get(CloudAppRestService);
    spy = spyOn<any>(restService, 'call').and.callFake((request: any) => {
      switch (request) {
        case '/users': 
          return of(USERS);
      }
    });

    fixture.detectChanges();
  });
  
  it('should show a list of users', fakeAsync(() => {
    const ul = fixture.debugElement.query(By.css("#users"));
    expect(ul.nativeElement.innerText).toContain('Three');
  }));
});

const USERS = {
  "user": [{
    "primary_id": "00715924",
    "first_name": "User",
    "last_name": "One"
  }, {
    "primary_id": "00715925",
    "first_name": "User",
    "last_name": "Two"
  }, {
    "primary_id": "00715926",
    "first_name": "User",
    "last_name": "Three"
  }],
  "total_record_count": 3
}

Events Entity

In this example we have a component which uses the entities$ observable to work with the entities on the page. Since we don’t have a real page to use in our test, we’ll need to stub the CloudAppEventsService and override the response, sending a hardcoded list of entities.

import { Component, OnInit } from '@angular/core';
import { CloudAppEventsService } from '@exlibris/exl-cloudapp-angular-lib';

@Component({
  selector: 'app-testing',
  templateUrl: './testing.component.html',
  styleUrls: ['./testing.component.css']
})
export class TestingComponent implements OnInit {
  entities$ = this.eventsService.entities$;

  constructor(
    private eventsService: CloudAppEventsService,
  ) { }

  ngOnInit() { }
}
<ul id='entities'>
  <li *ngFor="let entity of entities$ | async">{{entity.description}}</li>
</ul>
/* tslint:disable:no-unused-variable */
import { waitForAsync, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';

import { TestingComponent } from './testing.component';
import { CloudAppEventsService } from '@exlibris/exl-cloudapp-angular-lib';
import { of, Subject } from 'rxjs';
import { map } from 'rxjs/operators';

describe('TestingComponent', () => {
  let component: TestingComponent;
  let fixture: ComponentFixture<TestingComponent>;
  let compiled: HTMLElement;

  const onPageLoadSubject$ = new Subject<any>();
  let eventsService = {
    onPageLoad: handler => onPageLoadSubject$.subscribe(data => handler(data)),
    entities$: onPageLoadSubject$.asObservable().pipe(map(info => info.entities)),
  }

  beforeEach(waitForAsync(() => {
    TestBed.configureTestingModule({
      declarations: [ TestingComponent ],
      providers: [ 
        { provide: CloudAppEventsService, useValue: eventsService },
      ]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(TestingComponent);
    component = fixture.componentInstance;
    compiled = fixture.debugElement.nativeElement;

    fixture.detectChanges();
  });

  it('should show a list of entities', fakeAsync(() => {
    onPageLoadSubject$.next(PAGE_INFO);
    fixture.detectChanges();
    const ul = fixture.debugElement.query(By.css("#entities"));
    expect(ul.nativeElement.innerText).toContain('AUT1469');
  }))
});

const PAGE_INFO = {
  entities: [{
    "id": "23140519980000561",
    "type": "ITEM",
    "description": "AUT1469",
    "link": "/bibs/99242439000561/holdings/22140539880000561/items/23140519980000561"
  }, {
    "id": "23139149860000561",
    "type": "ITEM",
    "description": "AUTO1365",
    "link": "/bibs/99242439000561/holdings/22138939940000561/items/23139149860000561"
  }, {
    "id": "23139149850000561",
    "type": "ITEM",
    "description": "4356",
    "link": "/bibs/99242439000561/holdings/22138939940000561/items/23139149850000561"
  }]
};

Page Load

In this example, our component subscribes to the onPageLoad event and provides a handler which does some action. We want to test the result, so we’ll override the subscription in our test.

import { Component, OnInit } from '@angular/core';
import { CloudAppEventsService } from '@exlibris/exl-cloudapp-angular-lib';
import { Subscription } from 'rxjs';
import { tap } from 'rxjs/operators';

@Component({
  selector: 'app-testing',
  templateUrl: './testing.component.html',
  styleUrls: ['./testing.component.css']
})
export class TestingComponent implements OnInit {
  subscription$: Subscription;
  entityType: string;

  constructor(
    private eventsService: CloudAppEventsService,
  ) { }

  ngOnInit() {
    this.subscription$ = this.eventsService.onPageLoad(pageInfo => {
      this.entityType = pageInfo.entities.length && pageInfo.entities[0] && pageInfo.entities[0].type;
    })
  }

  ngOnDestroy() {
    this.subscription$.unsubscribe();
  }
}
/* tslint:disable:no-unused-variable */
import { waitForAsync, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';

import { TestingComponent } from './testing.component';
import { CloudAppEventsService, Entity } from '@exlibris/exl-cloudapp-angular-lib';
import { of, Subject } from 'rxjs';
import { map } from 'rxjs/operators';

describe('TestingComponent', () => {
  let component: TestingComponent;
  let fixture: ComponentFixture<TestingComponent>;
  let compiled: HTMLElement;

  const onPageLoadSubject$ = new Subject<any>();
  let eventsService = {
    onPageLoad: handler => onPageLoadSubject$.subscribe(data => handler(data)),
    entities$: onPageLoadSubject$.asObservable().pipe(map(info => info.entities)),
  }

  beforeEach(waitForAsync(() => {
    TestBed.configureTestingModule({
      declarations: [ TestingComponent ],
      providers: [ 
        { provide: CloudAppEventsService, useValue: eventsService },
      ]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(TestingComponent);
    component = fixture.componentInstance;
    compiled = fixture.debugElement.nativeElement;

    fixture.detectChanges();
  });

  it('should update entity type on page log', fakeAsync(() => {
    onPageLoadSubject$.next(PAGE_INFO);
    expect(component.entityType).toBe('ITEM');
  }))
});

const PAGE_INFO = {
  entities: [{
    "id": "23140519980000561",
    "type": "ITEM",
    "description": "AUT1469",
    "link": "/bibs/99242439000561/holdings/22140539880000561/items/23140519980000561"
  }, {
    "id": "23139149860000561",
    "type": "ITEM",
    "description": "AUTO1365",
    "link": "/bibs/99242439000561/holdings/22138939940000561/items/23139149860000561"
  }, {
    "id": "23139149850000561",
    "type": "ITEM",
    "description": "4356",
    "link": "/bibs/99242439000561/holdings/22138939940000561/items/23139149850000561"
  }]
};