Tech Blog

Getting Started Testing Angular Apps

A recent version of the Cloud App CLI added support for the test command, which wraps ng test from the Angular framework. I used the opportunity to learn about testing Angular apps and to add some tests to an existing app. In this post, I’ll share my experiences getting started with testing using the Jasmine test syntax.

The first thing I did was to update to the latest version of the Cloud App CLI by running

$ npm install -g @exlibris/exl-cloudapp-cli

Then I created a new app with eca init as described in the Getting Started Guide.

Starting with a new app

The base Cloud App which is created by the init command includes all of the set up and configuration required to run the test suite. After creating the app, I started it with eca start (which also runs npm install to install all of the dependencies). To add a new component, I can use eca generate component testing-component. The command adds the relevant files including a test specification file (component.spec.ts) with the test setup and a single test which validates that the component was created successfully.

/* tslint:disable:no-unused-variable */
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';

import { TestingComponent } from './testing.component';

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

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

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

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

Then I ran eca test from the command line to start up the Karma test server. A small Chrome window opened and displayed my passing test! Instant, small success.

Since that was so easy, I decided to add a few more tests. I added a property to the component class called “text” and added a test to validate the property:

it('should say hello', () => {
  expect(component.text).toContain('hello');
});

Then I changed the component HTML to render the text property and added a test to validate that:

it('should render title in a p tag', () => {
  expect(
    fixture.debugElement
     .nativeElement.querySelector('p')
     .textContent
  ).toContain('hello');
});

Now I have 3 passing tests, although I admit they’re pretty basic. To get a bit more complex, I’d like to add a test which validates a function is called when the corresponding button is clicked. So I add a button to the component HTML with a new function on the component class as a click handler. Then in my test, I use a technique called “spying” to keep track of the function being called. I get a handle for the button, “click” it, and then validate that the function was called and that the property was updated.

it('should call doIt', fakeAsync(() => {
  spyOn(component, 'doIt').and.callThrough(); //method attached to the click.
  let btn = fixture.debugElement.query(By.css('button'));
  btn.triggerEventHandler('click', null);
  fixture.detectChanges();
  expect(component.doIt).toHaveBeenCalled();
  expect(component.text).toBe('clicked');
}));

Now that I’ve played around a bit, I think I’m ready to add tests to my existing app.

Adding tests to an existing app

We recently added functionality to be able to import and export profiles from the CSV User Loader app. After manually testing loading new and existing profiles from text files, I wanted to add automatic tests to ensure the functionality is not broken in the future.

Since this is an existing app, I need to add some development dependencies in the package.json file. (My new app already had those dependencies from the new Cloud App base project.)

"devDependencies": {
  "jasmine-core": "^3.7.1",
  "jasmine-spec-reporter": "^7.0.0",
  "karma": "^6.3.2",
  "karma-chrome-launcher": "^3.1.0",
  "karma-coverage-istanbul-reporter": "^3.0.3",
  "karma-jasmine": "^4.0.1",
  "karma-jasmine-html-reporter": "^1.5.4",
  "@types/jasmine": "^3.6.9",
  "@types/jasminewd2": "^2.0.8"
}

After running npm install, I added a settings.component.spec.ts file next to my settings component with the basic “should create” test. I tried running eca test, but I get errors about non-existent attributes. I recognize this as the error which happens when I’m missing dependencies in the module. Tests have their own module which is loaded by the Karma test runner, so I find the configureTestingModule call in the beforeEach block. I add a bunch of dependencies until there are no more errors and my test passes:

import { RouterTestingModule } from '@angular/router/testing';
import { MaterialModule, getTranslateModule } from '@exlibris/exl-cloudapp-angular-lib';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ReactiveFormsModule } from '@angular/forms';
import { DialogModule } from 'eca-components';

...
beforeEach(async(() => {
  TestBed.configureTestingModule({
    imports: [ 
      BrowserAnimationsModule,
      RouterTestingModule,
      MaterialModule,
      getTranslateModule(),
      ReactiveFormsModule,
      DialogModule,
    ],
    declarations: [ 
      SettingsComponent,
      ProfileComponent,
    ],
  })
  .compileComponents();
}));

Now I’m ready to start adding functional tests. I want to run the import profile function and confirm that my profiles now include the added/updated profiles. To that end, I refactor my component class a bit to separate out the file load from the processing of the contents. That way I can call the processing function from my test. I add the profile JSON as a constant, and try to run the test.

it('should add new profile on import', () => {
  component.importProfiles(NEW_PROFILE);
  expect(component.profiles.value.length).toBe(3);
  expect(component.profiles.value).toContainObject({ name: 'Default New One' })
});

But my test fails. It seems like the profile is not added. I see that the function actually pops up a confirmation dialog before it runs the import code. So I need to intercept that dialog in my test and return true. I previously learned about spying, and now I can use it to intercept method calls to the service. To spy on my dialog service, I create a class property called dialogService and add DialogService to my providers in the module. I also use the injector to gain access to the DialogService and set it to my local property:

import { DialogService } from 'eca-components';

...
describe('SettingsComponent', () => {
  ...
  let dialogService: DialogService;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
    ...
      providers: [ 
        DialogService,
      ],
    }
    dialogService = fixture.debugElement.injector.get(DialogService);
  }
}

Now I can update my test slightly to spyOn the confirm method of the dialogService and return a value of true:

it('should add new profile on import', () => {
  spy = spyOn(dialogService, 'confirm').and.returnValue(of(true));
  component.importProfiles(NEW_PROFILE);
  expect(component.profiles.value.length).toBe(3);
  expect(component.profiles.value).toContainObject({ name: 'Default New One' })
});

Bingo! Now I add several other tests, including testing replacing profiles, validating confirmation messages and testing form validation. There’s nothing like seeing a bunch of green tests in the test runner:

You can check out all of the tests in the Github repo. I hope this post helps you get started with testing Angular apps in general and Cloud Apps in particular.

Leave a Reply