A test should test only one thing.
One given behavior is tested in one and only one test.
The tests should be independent from others, that means no chaining and no run in a specific order is necessary.
A test is implemented incorrectly or the test scenario is meaningless if changes in the HTML structure of the component destroy the test result.
Example: The test fails if an additional input field is added to the form.
it('should check if input fields are rendered on HTML', () => {
const inputFields = element.getElementsByClassName('form-control');
expect(inputFields.length).toBe(4);
expect(inputFields[0]).toBeDefined();
expect(inputFields[1]).toBeDefined();
expect(inputFields[2]).toBeDefined();
});
Instead use the xdescribe
or xit
feature (just add an 'x
' before the method declaration) to exclude tests.
This way excluded tests are still visible as skipped and can be repaired later on.
xdescribe("description", function() {
it("description", function() {
...
});
});
This way the test itself documents the initial behavior of the unit under test.
Especially if you test that your action triggers a change: Test for the previous state!
it('should call the cache when data is available', () => {
// precondition
service.getData();
expect(cacheService.getCachedData).not.toHaveBeenCalled();
<< change cacheService mock to data available >>
// test again
service.getData();
expect(cacheService.getCachedData).toHaveBeenCalled();
});
Testing should not be done for the sake of tests existing:
It is easy to always test with toBeTruthy
or toBeFalsy
when you expect something as a return value, but it is better to make stronger assertions like toBeTrue
, toBeNull
or toEqual(12)
.
it('should cache data with encryption', () => {
customCacheService.storeDataToCache('My task is testing', 'task', true);
expect(customCacheService.cacheKeyExists('task')).toBeTruthy();
});
it('should cache data with encryption', () => {
customCacheService.storeDataToCache('My task is testing', 'task', true);
expect(customCacheService.cacheKeyExists('task')).toBeTrue();
});
Again, do not rely too much on the implementation.
If user customizations can easily break the test code, your assertions are too strong.
it('should test if tags with their text are getting rendered on the HTML', () => {
expect(element.getElementsByTagName('h3')[0].textContent).toContain('We are sorry');
expect(element.getElementsByTagName('p')[0].textContent).toContain(
'The page you are looking for is currently not available'
);
expect(element.getElementsByTagName('h4')[0].textContent).toContain('Please try one of the following:');
expect(element.getElementsByClassName('btn-primary')[0].textContent).toContain('Search');
});
it('should test if tags with their text are getting rendered on the HTML', () => {
expect(element.getElementsByClassName('error-text')).toBeTruthy();
expect(element.getElementsByClassName('btn-primary')[0].textContent).toContain('Search');
});
Methods like ngOnInit()
are lifecycle-hook methods which are called by Angular – The test should not call it directly.
When doing component testing, you most likely use TestBed
anyway, so use the detectChanges()
method of your available ComponentFixture
.
it('should call ngOnInit method', () => {
component.ngOnInit();
expect(component.loginForm).toBeDefined();
});
it('should contain the login form', () => {
fixture.detectChanges();
expect(component.loginForm).not.toBeNull();
});
The test name describes perfectly what the test is doing.
it ('wishlist test', () => {...})
it ('should add a product to an existing wishlist when the button is clicked', () => {...})
Basically it should read like a documentation for the unit under test, not a documentation about what the test does. Jasmine has named the methods accordingly.
Read it like `I am describing , it should when/if/on <condition/trigger> (because/to )`.
This also applies to assertions.
They should be readable like meaningful sentences.
const result = accountService.isAuthorized();
expect(result).toBeTrue();
const authorized = accountService.isAuthorized();
expect(authorized).toBeTrue();
or directly
expect(accountService.isAuthorized()).toBeTrue();
Tests should define Variables only in the scope where they are needed.
Do not define Variables before describe
or respective it
methods.
This increases readability of test cases.
beforeEach
methods.TestBed
you can handle injection to variables in a separate beforeEach
method.it('should create the app', async(() => {
const fixture = TestBed.createComponent(AppComponent);
const component = fixture.componentInstance;
...
});
it('should have the title "app"', async(() => {
const fixture = TestBed.createComponent(AppComponent);
const component = fixture.componentInstance;
...
});
it('should match the text passed in Header Component', async(() => {
const fixture = TestBed.createComponent(AppComponent);
});
describe('AppComponent', () => {
let translate: TranslateService;
let fixture: ComponentFixture<AppComponent>;
let component: AppComponent;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ... ] });
fixture = TestBed.createComponent(AppComponent);
component = fixture.componentInstance;
})
it('should create the app', () => { ... });
it(\`should have as title 'app'\`, () => { ... });
it('should match the text passed in Header Component', () => { ... });
});
This increases readability of test cases.
If you do not need the functionality of :
ComponentFixture.debugElement
TestBed
async, fakeAsync
inject
... do not use it.
it('should create the app', async(() => {
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
}));
it('should be created', () => {
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
The describe
methods in Jasmine are nestable.
You can use this to group various it
methods into a nested describe
where you can also use an additional beforeEach
initialization method.
describe('AccountLogin Component', () => {
it('should be created', () => { ... });
it('should check if controls are rendered on Login page', () => { ... });
....
describe('Username Field', () => {
it('should be valid when a correct email is assigned', () => { ... });
....
});
});
Always only declare what you need.
Unused variables, classes and imports reduce the readability of unit tests.
This way less code needs to be implemented which again increases readability of unit tests.
Also mocks can be stubbed on time, depending on the current method.
We decided to use ts-mockito as the Test Mocking Framework.
Use only IDs or definite class names to select DOM elements in tests.
Try to avoid general class names.
const selectedLanguage = element.getElementsByClassName('d-none');
// by id
const selectedLanguage = element.querySelector('#language-switch');
// by class
const selectedLanguage = element.getElementsByClassName('language-switch');
Use data-testing-id
via attribute binding to implement an identifier used for testing purpose only.
*.component.html
<ol class="viewer-nav">
<li *ngFor="let section of sections" [attr.data-testing-id]="section.value">{{ section.text }}</li>
</ol>
*.spec.ts
element.querySelectorAll('[data-testing-id]')[0].innerHTML;
element.querySelectorAll("[data-testing-id='en']").length;
⚠️ Note
Do not overuse this feature!
Every component should have a 'should be created' test like the one Angular CLI auto-generates.
This test handles runtime initialization Errors.
it('should be created', () => {
expect(component).toBeTruthy();
expect(element).toBeTruthy();
expect(() => fixture.detectChanges()).not.toThrow();
});
See Three Ways to Test Angular Components for more information.
toBeDefined
Be careful when using toBeDefined
, because a dynamic language like JavaScript has another meaning of defined (see: Is It Defined? toBeDefined, toBeUndefined).
⚠️ Warning
Do not usetoBeDefined
if you really want to check for not null because technically 'null' is defined. UsetoBeTruthy
instead.
Jasmine does not automatically reset all your variables for each test like other test frameworks do.
If you initialize directly under describe
, the variable is initialized only once.
⚠️ Warning
Since tests should be independent of each other, do not do this.
describe('...', () => {
let a = true; // initialized just once
const b = true; // immutable value
let c; // re-initialized in beforeEach
beforeEach(() => {
c = true;
});
it('test1', () => {
a = false;
// b = false; not possible
c = false;
});
it('test2', () => {
// a is still false
// c is back to true
});
});
As shown in the above example, a
shows the wrong way of initializing variables in tests.
If you do not need to change the value, use a const
declaration for primitive variables like b
.
If you need to change the value in some tests, assure it is reinitialized each time in the beforeEach
method like c
.
A const
declaration like b
should not be used for complex values, as the object behind b
is still mutable and has to be re-initialized properly:
describe('...', () => {
let a: any;
const b = { value: true };
beforeEach(() => {
a = { value: true };
});
it('test1', () => {
a.value = false;
b.value = false;
});
it('test2', () => {
// a.value is back to true
// b.value is still false
});
});
Testing EventEmitter
firing can be done in multiple ways that have advantages and disadvantages.
Consider the following example:
import { EventEmitter } from '@angular/core';
import { anything, capture, deepEqual, spy, verify } from 'ts-mockito';
describe('Emitter', () => {
class Component {
valueChange = new EventEmitter<{ val: number }>();
do() {
this.valueChange.emit({ val: 0 });
}
}
let component: Component;
beforeEach(() => {
component = new Component();
});
it('should detect errors using spy with extract', () => {
// *1*
const emitter = spy(component.valueChange);
component.do();
verify(emitter.emit(anything())).once();
const [arg] = capture(emitter.emit).last();
expect(arg).toEqual({ val: 0 });
});
it('should detect errors using spy with deepEqual', () => {
// *2*
const emitter = spy(component.valueChange);
component.do();
verify(emitter.emit(deepEqual({ val: 0 }))).once();
});
it('should detect errors using subscribe', done => {
// *3*
component.valueChange.subscribe(data => {
expect(data).toEqual({ val: 0 });
done();
});
component.do();
});
});
As EventEmitter
is Observable
, subscribing to it might be the most logical way of testing it.
We, however, would recommend using ts-mockito to increase readability.
The ways 1 and 2 portrait two options, we would recommend using the first one.
1 (preferred) | 2 | 3 | |
---|---|---|---|
Description | - Using ts-mockito spy and then verify it has fired - Then check argument for expected value | Using ts-mockito spy and then verify it has fired with the expected value | - Using subscription and asynchronous method safeguard |
Readability | Capturing arguments with ts-mockito might seem tricky and therefore reduces readability, but the test is done in the right order. | ||
In case it does not emit | |||
In case it emits another value |
The information provided in the Knowledge Base may not be applicable to all systems and situations. Intershop Communications will not be liable to any party for any direct or indirect damages resulting from the use of the Customer Support section of the Intershop Corporate Web site, including, without limitation, any lost profits, business interruption, loss of programs or other data on your information handling system.