November 2018

Volume 33 Number 11

The Working Programmer - How to be MEAN: Testing Angularly

By Ted Neward | November 2018

Ted NewardWelcome back again, MEANers.

With any luck, the “debate” around unit testing of code no longer requires discussion—as a developer, you should be looking for ways to automate the testing code you write, without question. Arguments may continue as to whether those tests should come before or after the code in question, perhaps, but it’s pretty clear that tests are not an optional part of a modern software project anymore. Which then, of course, raises the question: How do you test an Angular application? I touched briefly on the test files, back in the May 2017 issue when I first started building out some Angular components, but it was hardly expansive (msdn.com/magazine/mt808505). Now it’s time to take a deeper look.

Back to Beginnings

Let’s take a step back to the beginning of the application. When “ng new” is run to scaffold out the initial stages of the application, it creates all the necessary tools and hooks and files in place to ensure that the application can be tested. In fact, immediately after “ng new,” without making even a single change to any file, you can run “ng test” to run the test runner, which in turn executes the code written against the test framework code scaffolded out.

When run, “npm test” fires up Karma, the test runner, which then fires up a browser instance and runs the tests inside that instance. Karma continues to run, keeping an open WebSocket to the browser so that any changes to the source files can be immediately tested, removing some of the overhead of testing and bringing us closer to a no-wait code-and-test cycle. The project scaffolding provides three tests, encoded in the “app.component.spec.ts” file, which tests the corresponding “app.component.ts” code. As a general rule, each component in the Angular application should have a corresponding “.spec.ts” file to hold all the tests. If each component is created by the Angular CLI, it will usually generate a corresponding test file, with a few exceptions (such as “class”) that will require a “--spec" argument to the “generate” command to create the spec (short for “specification”) file.

As a matter of fact, let’s generate a quick class, Speaker (of course), and then write some tests for it. As usual, create the component with the Angular CLI (“ng generate class Speaker --spec”), and edit the “speaker.ts” file to contain a simple class with five constructor public properties:

export class Speaker {
  constructor(public id: number,
    public firstName: string,
    public lastName: string,
    public age: number,
    public bio?: string,
    )
  { }
}

The corresponding tests should exercise the various properties to ensure they work as expected:

import { Speaker } from './speaker';
describe('Speaker', () => {
  it('should create an instance', () => {
    expect(new Speaker(1, "Ted", "Neward", 47, "Ted is a big geek")).toBeTruthy();
  });
  it('should keep data handy', () => {
    var s = new Speaker(1, "Ted", "Neward", 47, "Ted is a big geek");
    expect(s.firstName).toBe("Ted");
    expect(s.firstName).toEqual("Neward");
    expect(s.age).toBeGreaterThan(40);   
  })
});

As the class gets more complicated, more complex tests should be added, as well. Much of the power of the testing lies in the “expect” framework, which provides a large number of “toBe” methods to test various scenarios. Here you see several flavors of this, including “toEqual,” which does an equality test, and “toBeGreaterThan,” which does exactly as its name implies.

But those of you who are following along at home, however, will see something is wrong—after the spec file is saved, the open browser instance turns an ugly shade of red, pointing out that “Ted” is not the same as “Neward”! Sure enough, there’s a bug in the second “expect” statement, looking to compare “firstName” when it should be “lastName.” This is good, because while test code is testing for bugs in code, it’s also the case that sometimes the bug is in the test, and getting that feedback as soon as you write the test helps avoid test bugs.

More Tests

Of course, an Angular app is made up of more than simple classes; it also may include services, which are generally pretty simple to test, because they tend to provide behavior and very little state. In the case of services that provide some behavior on their own, such as formatting or some simple data transformation, testing is easy and reminiscent of testing a class like Speaker. But services also frequently have to interact with the world around them in some fashion (such as making HTTP requests, as the SpeakerService did a few columns back), which means testing them becomes trickier if you don’t want to have to include the dependencies. Actually sending requests out over HTTP, for example, would make the tests subject to the vagaries of network communication or server outages, which could yield some false-negative failures and make the tests less deterministic. That would be bad.

It’s for these situations that Angular makes such heavy use of dependency injection.

For example, let’s start with the version of SpeakerService that didn’t make any HTTP requests, as shown in Figure 1.

Figure 1 The SpeakerService Class That Didn’t Make Any HTTP Requests

@Injectable()
export class SpeakerService {
  private static speakersList : Speaker[] = [
    new Speaker(1, "Ted", "Neward", 47,
      "Ted is a big geek living in Redmond, WA"),
    new Speaker(2, "Brian", "Randell", 47,
      "Brian is a high-profile speaker and developer of 20-plus years.
      He lives in Southern California."),
    new Speaker(3, "Rachel", "Appel", 39,
      "Rachel is known for shenanigans the world over. She works for Microsoft."),
    new Speaker(4, "Deborah", "Kurata", 39,
      "Deborah is a Microsoft MVP and Google Developer Expert in Angular,
      and works for the Google Angular team."),
    new Speaker(5, "Beth", "Massi", 39,
      "Beth single-handedly rescued FoxPro from utter obscurity
      and currently works for Microsoft on the .NET Foundation.")
  ]
  public getSpeakers() : Speaker[] { return SpeakerService.speakersList; }
  public getSpeakerById(id : number) : Speaker {
    return SpeakerService.speakersList.find( (s) => s.id == id);
  }
}

This version is trivial to test, because it’s synchronous and requires no outside dependencies, as shown in Figure 2.

Figure 2 Testing the SpeakerService Class

describe('SpeakerService', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [SpeakerService]
    });
  });
  it('should be able to inject the SpeakerService', inject([SpeakerService],
    (service: SpeakerService) => {
    expect(service).toBeTruthy();
  }));
  it('should be able to get a list of Speakers', inject([SpeakerService],
    (service: SpeakerService) => {
    expect(service.getSpeakers()).toBeDefined();
    expect(service.getSpeakers().length).toBeGreaterThan(0);
  }));
});

Notice the “inject” calls in each test? This is basically how Angular manages the dependency injection in the testing environment; it’s what allows you to supply any sort of compatible service back end (real or mocked) to the environment.

Typically, the service does a little bit more than what’s in the simple SpeakerService, and it’s a pain to have to comment out and/or replace the “real” one with a fake one that does nothing, so this is where using a “mock” service works better. Angular has a useful construct called the “Spy” that can insert itself into a regular service and override certain methods to provide a mocked result:

it('should be able to get a list of Speakers',
    inject([SpeakerService], (service: SpeakerService) => {
  spy = spyOn(service, 'getSpeakers').and.returnValues([]);
  var speakers: Speaker[] = service.getSpeakers();
  expect(speakers).toBeDefined();
  expect(speakers.length).toBe(0);
}));

By using the Spy, you can “override” the method being invoked in the test in order to provide whatever values you want to get back.

Component Tests

A large part of building an Angular application, however, is building components that can appear on the page, and it’s important to be able to test those, too. To get an even better understanding of testing a visual component, let’s start with a simple on/off toggle-switch type component:

@Component({
  selector: 'app-toggle',
  template: `<button (click)="clicked()">
    I am {{isOn ? "on" : "off" }} -- Click me!
  </button>`,
  styleUrls: ['./toggle.component.css']
})
export class ToggleComponent {
  public isOn = false;
  public clicked() { this.isOn = !this.isOn; }
}

To test this, you can literally ignore the DOM entirely and just exam­ine the state of the component when various actions are invoked:

it('should toggle off to on and off again', () => {
  const comp = new ToggleComponent();
  expect(comp.isOn).toBeFalsy();
  comp.clicked();
  expect(comp.isOn).toBeTruthy();
  comp.clicked();
  expect(comp.isOn).toBeFalsy();
});

This doesn’t check the DOM at all, however, which could hide some critical bugs. Just because the “isOn” property has changed doesn’t mean the template has rendered the property correctly, for example. To check for this, you can get hold of the component instance created by the fixture, and examine the DOM rendered for it, like so:

it ('should render HTML correctly when clicked', () => {
  expect(fixture.componentInstance.isOn).toBeFalsy();
  const b = fixture.nativeElement.querySelector("button");
  expect(b.textContent.trim()).toEqual("Click me! I am OFF");
  fixture.componentInstance.clicked();
  fixture.detectChanges();
  expect(fixture.componentInstance.isOn).toBeTruthy();
  const b2 = fixture.nativeElement.querySelector("button");
  expect(b2.textContent.trim()).toEqual("Click me! I am ON");
});

The “nativeElement” here obtains the DOM node for the component, and I use “querySelector” to do a jQuery-style query to find the relevant DOM node inside—in this case, the button the Toggle creates. From there, I grab its text content (and trim it, because the preceding demo code line breaks in two places that would be awkward to replicate in the test) and compare it against the expected results. However, notice that after I “click” the component, there’s a call to “detectChanges”; this is because Angular needs to be told to go process the DOM-relative changes that the event handler might have caused, such as updating the interpolated strings in the template. Without this, the tests will fail despite the component working flawlessly in the browser. (I made this exact mistake while writing the article, in fact, so don’t feel bad if you forget to detect the changes.) Note that if the component does any significant initialization inside of its onInit method, the test will need to detectChanges before doing any meaningful work, for the same reason.

Wrapping Up

Keep in mind, by the way, that all of this test code is against the client-side of the application, not the server-side. Remember all the Express code I wrote to provide APIs to store data into the database and so on? All of that is basically “outside” this framework, and therefore needs to be maintained and run separately. You can use some of the “build” tools I’ve discussed to run both the server-side and client-side tests as part of a larger test cycle, and thus make sure they get triggered as part of any changes to either client or server. Angular also supports “E2E” (short for “end-to-end”) testing, which is outside the scope of what’s being talked about here, but is intended to support exactly this situation.

Happy coding!


Ted Neward is a Seattle-based polytechnology consultant, speaker and mentor, currently working as the director of Engineering and Developer Relations at Smartsheet.com. He has written a ton of articles, authored and co-authored a dozen books, and speaks all over the world. Reach him at ted@tedneward.com or read his blog at blogs.tedneward.com.

Thanks to the following technical expert for reviewing this article: Garvice Eakins (Smartsheet.com)


Discuss this article in the MSDN Magazine forum