Unit Testing in Angular

A new programmer’s journey into a once foreign territory

Jordan Kittle
Pixelmatic Tech

--

Introduction

Automated tests were something I started noticing more and more on my programming journey. Undoubtedly you have seen them in many places. They exist in code-camp courses, code competition sites, and are generated by default every time you use Angular CLI to generate a new component. At first, they seemed like some foreign concept that I could ignore, because I could test my functions by myself. I was coding fun projects for personal use and I knew every line of code and how to test them. Testing only took a few clicks. However, now that I am working as part of a growing team on a large project, they have become a necessity. There is no way to efficiently test the entire application surface area by hand. So here I am, diving into automated testing with the goal to be able to write valuable tests for the new components I code, and the existing components that are not yet covered.

One of the first questions I had is, “What do I test?” Should we implement tests for every piece of production code in our project? The answer to this question will vary greatly depending on who you ask. Depending on available time and budget, automated testing might not make sense for your project at all. A startup company, with limited funding and narrow time to deliver functional software, may choose to write fewer or no tests. On the opposite end of this example, a large company with many employees may decide that a test for every possible logical pathway of their code is not only warranted but necessary.

To aid in our learning, I have published a repository of the tests I demo here:
https://github.com/jordankittle/unitTesting

Types of Automated Tests

  1. Unit tests are simple and involve only the typescript class. They do not test the HTML template or services and API’s. Any services or API’s consumed will be faked. Unit tests are easier to write but do not provide as much confidence as integration and end-to-end tests can.
  2. Integration tests are more involved in their setup; they test the typescript class in combination with its HTML template. They are slightly more difficult to code than unit tests but provide more confidence. When integration testing an angular component, the template will be used but API’s and services may still be faked.
  3. End-to-end tests test the application as a whole. They are more challenging to implement than integration and unit tests and provide the most confidence. However, they are slow and fragile. A small change in the HTML can break an end-to-end test.

Unit testing alone may not be enough, but testing all edge cases with end-to-end tests may take way too much time, and be prone to breaking as HTML templates are changed. A good strategy is to cover very important interactions with end-to-end tests, with edge cases covered by integration and unit tests. This article will focus mainly on unit tests, with integration and end-to-end tests covered in future articles.

Best Practices

  1. Unit tests should be coded with clean, readable, and maintainable code.
  2. Test functions should be kept small and to the point.
  3. Care should be taken when naming tests.
  4. Test functions should have a single responsibility.
  5. Tests should not be fragile.

It is important that tests take into consideration possible changes to HTML templates so they are not prone to breaking. As an example, suppose we have a component that greets the user. Instead of checking that the output is exactly “Hello, {username}”, we may instead test that the output only contains the username. This way, if we change the output to “Hey, {username}”, the test still confirms the output contains the desired welcome to the user but does not break. This test is specific, effective, and not prone to easily breaking.

Common Test Functions

describe(() => {})   // suite: the group of tests we will runit(() => {})         // spec: specific testsexpect()             // what we expect a result of a test to betoBe()               // expect the result to betoContain()          // expect the result to containbeforeEach(() => {}) // set up: do this before each testafterEach(() => {})  // tear down: do this after each testbeforeAll(() => {})  // run before all testsafterAll(() => {})   // run after all tests

Basic Example

import { functionName } from './functionName';describe('functionName', () => {
it('should return X if Y condition', () => {
const result = functionName(Y);
expect(result).toBe(X);
});
it('should return A if B condition', () => {
const result = functionName(B);
expect(result).toBe(A);
});
});

The tests should test all possible function paths. If functions contain if-else blocks, both the if, and the else pathways should be tested. This is known as code coverage. Testing all of the logical pathways your function can take will mitigate the chances of running into unexpected outcomes.

Angular Component Example

To demonstrate testing a very simple Angular component, we will make a basic test for a counter component, a simple component that keeps track of a count. This could be something commonly used in real-world applications like a favorite button or a voting mechanism. To test this component, we will arrange our tests according to a common testing structure: arrange, act, and assert. We will use the beforeEach() method to set up a new instance of CounterComponent so that it will be called before each test. This is the arrange portion of the structure. Using the it() method, we state what should happen when we perform a certain action. We then perform the action on the newly created instance of our component, this is the Act portion of the structure. After the action has been performed, we use the expect() method — the assert portion of the structure — to assert what we expect the outcome of the action to be.

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CounterComponent } from './counter.component';
describe('CounterComponent', () => {
let component: CounterComponent;
let fixture: ComponentFixture<CounterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ CounterComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(CounterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should increment counter when incremented', () => {
component.increment();
expect(component.count).toBe(1);
});
it('should decrement counter when decremented', () => {
component.decrement();
expect(component.count).toBe(-1);
});
});

This test should be created in a special file called counter.component.spec.ts, next to its accompanying counter.component.ts file. This is the default naming convention that is used when components are generated using Angular CLI. Karma will load all the tests within these spec.ts files and run them.

Testing a Service

Suppose we have a component that calls a service in its ngOnInit() method to get a list of items. There is also an addToList() method that calls the service to save a new item to the list. This could be something like a shopping list, errands to run, or a list of languages. The component subscribes to this service and sets this.list to the data returned. In this situation, we will call ngOnInit() in our test, and then assert that this.list is initialized. However, we will not actually use the service, because are conducting a unit test. We are testing this component in isolation, so the service will be faked. We will assume that the response we get from the service is valid. We will also test the component’s addToList() function to make sure that when called, the service’s addToList() function is also called. Last, we will ensure that the list is updated in the component.

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { EMPTY, from } from 'rxjs';
import { ListService } from '../services/list.service';
import { ListComponent } from './list.component';
describe('ListComponent', () => {
let component: ListComponent;
let fixture: ComponentFixture<ListComponent>;
let service: ListService;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ListComponent],
providers: [ListService]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ListComponent);
component = fixture.debugElement.componentInstance;
service = fixture.debugElement.injector.get(ListService);
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should set list property returned from service', () =>
const testList = ['Item 1', 'Item 2'];
const spy = spyOn<any>(service, 'getList')
.and.callFake(() => {
return from([testList]);
});
component.ngOnInit();
expect(component.list).toEqual(testList);
});
it('should save new item to list', () => {
const spy = spyOn(service, 'addToList')
.and.callFake(() => {
return EMPTY;
});
component.addToList('New List Item');
expect(spy).toHaveBeenCalled();
});
it('should add the new list item to the component', () => {
const testList = ['Item 1', 'Item 2'];
const testListItem = 'New List Item';
const spy = spyOn<any>(service, 'getList')
.and.callFake(() => {
return from([testList]);
});
component.ngOnInit();
component.addToList(testListItem);
expect(component.list).toContain(testListItem);
});
});

Running the Tests

To run all tests, in the console, type ‘ng test’ (without quotation marks). Angular will run the tests and the console will report success or failure. There is also a window opened in the browser where you can see the results of the tests. We can test how much of our code is covered by our tests by adding a flag to the end of that command:

ng test --code-coverage

This will make a new folder in our project called coverage, with an index.html file inside of it. When we open this file in the browser, we can see how much of our project’s code is covered by tests.

Browser Output
coverage/index.html

Conclusion

The ability to develop automated tests is an extremely valuable skill-set to have as a programmer, and incorporating them can be an invaluable quality assurance process for a team. The extra time and money it takes to write code for tests can easily be outweighed by the resources it will take to continually test a project manually each time changes are made. Unit tests are a great tool for covering the many edge cases and logical pathways your code can take. Because unit tests only test a component in isolation, without utilizing outside factors such as data returned by file systems and API’s, they are only a part of a complete testing strategy. Integrated and end-to-end tests may need to be implemented to test more complex and critical scenarios. For me, a combination of automated and manual tests, with the most critical functions tested manually, will be my strategy. There is no situation in which I could feel completely confident delivering code to my employer without verifying certain key actions with my own eyes. But that confidence would not be complete without the extra assurance that automated tests can provide.

I hope you have found this article as educating as I have found writing it. I look forward to the day when all my components’ functions are covered by well-written and effective tests. I also look forward to continuing my automated testing journey with integrated and end-to-end tests and sharing that knowledge in future articles. If you have any questions, comments, or critiques, I would love to hear from you!

Resources and Acknowledgements

I would like to thank Mosh Hamedani for his excellent Angular course, Angular 4: Beginner to Pro. (https://codewithmosh.com). Though now slightly outdated, this article was heavily influenced by its contents. I would also like to thank everyone at Pixelmatic for helping me become the developer I am today.

--

--