In order to test Angular you can use Jasmine as a testing framework. Additionally, you can use Karma to manage tests in a browser.
Luckily, basic Angular projects are initialized with both Jasmine and Karma.
This is only a summary. Please read into e.g. the documentation for more information.
Use ng test
to run start a karma server. You can view it on the browser. There you can manage the execution of your tests.
There are two options on where to store the test files. Either you create a separate test folder or you put them into the component folder. In this project, we will store the test file within the component folder. This fits the behavior of the Angular CLI, as it already creates basic tests when creating a new module/component.
Test files are useally called <component-name>.spec.ts.
Tests constructed with Jasmine are behavior-driven tests. Therefore, you will first describe the environment in which you will run your tests. Then, you can add specifications on how the module/component should behave. To ensure proper behavior, you will use assertions.
First, you start with an suite group. This is a set of specs or test cases and be created by calling the function describe(String, function())
. The first argument is the title of the test suite and should describe it briefly. This will be displayed later on. The second argument is a callback function within which the behavior will go.
For example
describe('String Utils', () => {
/* here go your tests */
}
Note: You can nest test suits.
The specs and test cases are definded within the callback function. This is done by calling the function it(String, function)
. The first argument describes the title of the spec/test case and will be displayed later on. The second is a function which contains the acutal test:
describe('String Utils', () => {
it('should be able to look for a substring', () => {...});
it('should be able to upper case', () => {...});
it('should be able to lower case', () => {...});
});
Expectations are like assertions and created using the global expect()
function. To define an assertion, you have to combine it with matchers. There are some build in matchers:
- toBe() for testing for identity,
- toBeNull() for testing for null,
- toBeUndefined()/toBeDefined() for testing for undefined/not undefined,
- toBeNaN() for testing for NaN (Not A Number)
- toEqual() for testing for equality,
- toBeFalsy()/toBeTruthy() for testing for falseness/truthfulness etc.
describe('String Utils', () => {
it('should be able to check whether a string contains a char', () => {
expect(utils.contains).toBeDefined();
expect(utils.contains("Hello", 'e')).toBeTruthy();
expect(utils.contains("Hello", 'E')).toBeFalsy();
expect(utils.contains("Hello", 'x')).toBeFalsy();
});
it('should be able to upper case', () => {
expect(utils.toLowerCase).toBeDefined();
expect(utils.toLowerCase("HelLO WorLD")).toEqual("hello world");
});
it('should be able to lower case', () => {
expect(utils.toUpperCase).toBeDefined();
expect(utils.toUpperCase("HelLO WorLD")).toEqual("HELLO WORLD");
});
}
See the documentation for a detailed explanation/list of the matchers.
You can create additional custom matchers within the beforeEach()
function using jasmine.addMatchers()
function.
It is kind of common to need the same variables or initialization within each test. For this you can use:
- The
beforeEach()
function is called once before each spec in the suite where it is called. - The
afterEach()
function is called once after each spec in the suite where it's called.
Additionally, there are functions to execute code before and after all tests:
- The
beforeAll()
function is called once before all specs in the suite where it is called. - The
afterAll()
function is called once after all specs in the suite where it's called.
describe('String Utils', () => {
beforeAll(function(){
/* init something for all tests */
});
beforeEach(function(){
/* init something for each test */
});
afterEach(function(){
/* clean up after each test */
});
afterAll(function(){
/* clean up after all tests
might be usefull if global values have been touched */
});
/* ... */
}
If you want to fail a test due to reasons, you can use the global fail() function.
describe('String Utils', () => {
it("should explicitly fail", function () {
fail('Forced to fail');
});
}
If you want to check, whether certain exceptions are thrown, you can use the methods
toThrow()
to check whether an exception was thrown andtoThrowError(ErrorType)
to check whether an exception of a specific type was thrown.
E.g. you want to test the following function:
function throwsError() {
throw new TypeError("A type error");
}
Then you could test it like this:
it("it should throw an exception", function () {
expect(throwsError).toThrow();
expect(throwsError).toThrowError(TypeError);
});
Spys are a usefull tool to mock and oberserve calls of other methods. To create a spy you can use createSpy()
. To spy on a function you can use spyOn()
By default a spy will only report if a call was done without calling through the spied function. The spied function is therefore not called. You can change the default behavior using these methods:
and.callThrough()
: call through the original functionand.returnValue(value)
: return the specified valueand.callFake(fn)
: call the fake function instead of the original functionand.throwError(err)
: throw an errorand.stub()
: resets the default stubbing behavior
See the documentation for more information.
describe("String Utils", () => {
it("should be able to upper case", () => {
var spytoUpperCase = spyOn(String.prototype, "toUpperCase");
expect(utils.toUpperCase).toBeDefined(); // will succeed
expect(utils.toUpperCase("hello world")).toEqual("HELLO WORLD"); // will fail
});
it("should be able to upper case", () => {
var spytoUpperCase = spyOn(
String.prototype,
"toUpperCase"
).and.callThrough();
expect(utils.toUpperCase).toBeDefined(); // will succeed
expect(utils.toUpperCase("hello world")).toEqual("HELLO WORLD"); // will succeed
});
it("should be able to upper case", () => {
var faker = function () {
return "HELLO WORLD";
};
var spytoUpperCase = spyOn(String.prototype, "toUpperCase").and.callFake(
faker
);
expect(utils.toUpperCase).toBeDefined(); // will succeed
expect(utils.toUpperCase("hello world")).toEqual("HELLO WORLD"); // will succeed
expect(utils.toUpperCase("my World")).toEqual("MY WORLD"); // will fail
});
});
There is a proper testing environment for testing HTTP Requests. It creates a mocked module simulating a server, which can be easily setup for each individual test/spec. See the docu for more info:
- HTTPClient
- HTTPErrorResponse
- HTTPTestingController: this is the component used to mock/simulate the server.
By default, Jasmine waits for any asynchronous operation, defined by a callback, promise or the async keyword, to be finished. Aside from this you can ensure to wait for N internal ticks. See Jasmine Clock for this.
See the Jasmine Clock documentation for more information.
Angular provides some modules that should make testing easier. In addition, every time a component or module is created (ng g m
or ng g c
), Angular always sets a test file on it as well. When testing a component, most of the time you want to start like the following.
Please mind: At lot of the following code will come out of the box (ng g
). Nevertheless, there are some additions to the default template by ng g
:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LoginPageComponent } from './login-page.component';
import { DebugElement } from '@angular/core';
describe('LoginPageComponent', () => {
let component: LoginPageComponent;
let fixture: ComponentFixture<LoginPageComponent>;
let de: DebugElement
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ... ],
imports:[...]
})
.compileComponents(); // compiles the components html and css
});
beforeEach(() => {
fixture = TestBed.createComponent(LoginPageComponent); // fixture is the test environment for the component
component = fixture.componentInstance; // the component itself
de = fixture.debugElement; // the rendered component
fixture.detectChanges();
});
});
You can use debug Element de = fixture.debugElement
to access rendered HTML elements:
.query(By.all());
de.query(By.css('h1'));
de.query(By.css('.my-class'));
de.query(By.css('[attribute]'));
// not sure on how to use By.directive
// but it uses a directive to query all matches
de.query(By.directive(myDirective));
Alternativly you can use the fixture.nativeElement
to get the root Element:
fixture.nativeElement.querySelector("h1");
fixture.nativeElement.getElementById("myId");
See the documentation of the DebugElement for more information.
As Angular provides HTML which has to be compiled, changes, which affect ngIF, ngFor or ngInit, are not updated on itself. In order to fix it, you may call fixture.detectChanges()
. Example:
The login-component contains a test-card
, which is only displayed, if the inDevelopment() is true.
<mat-card class="test-card" *ngIf="inDevelopment()">
...
</mat-card>
In order to test this, you can use a spy to muck the behavior of the inDevelopment(). As the following test case does not use fixture.detectChanges()
, the build of the HTML (and the call of inDevelopment()
) are prior to the creation of the spy. Therefore, expect(spy).toHaveBeenCalled();
will fail the test:
it("should not show test div in production", () => {
expect(component.inDevelopment).toBeDefined();
let spy = spyOn(component, "inDevelopment").and.returnValue(false);
expect(spy).toHaveBeenCalled();
});
After adding fixture.detectChanges()
the test will not fail. To test whether the test-card
is displayed you can query the element as described above:
it("should not show test div in production", () => {
expect(component.inDevelopment).toBeDefined();
let spy = spyOn(component, "inDevelopment").and.returnValue(false);
expect(spy).toHaveBeenCalled();
expect(component.inDevelopment()).toBeFalse();
let test_div = de.query(By.css(".test-card"));
expect(test_div).toBeNull();
});
To test components using Angular Materials you can use the testclasses provided by Angular Materials: Read up on usage. Maybe this article will help: https://kevinkreuzer.medium.com/test-your-components-using-angular-materials-component-harnesses-f9c1deebdf5d
This guide was created using