Angular unittest with Karma Jasime or Jest - NextensArelB/SwaggerGenerationTool GitHub Wiki
[[TOC]]
##Introduction
On this wiki page, you will find an overview of Nextens recommended practices when Unit Testing Angular UI (page) components.
The standard unit test tool for Angular is called Jasmine. Jasmine is a behavior-driven development (BDD) framework that provides a clean and readable syntax for writing tests in JavaScript. It integrates well with Angular and is widely used by the Angular community for testing Angular applications.
Jasmine provides a set of functions and matchers that allow you to define test suites, test cases, and assertions. Some key features of Jasmine include:
-
Descriptive Syntax: Jasmine uses a descriptive syntax that reads like natural language, making it easy to understand and write tests.
-
Test Suites: You can organize your tests into logical groups called test suites using the
describe
function provided by Jasmine. -
Test Cases: Within each test suite, you can define individual test cases using the
it
function. Each test case typically contains one or more assertions to verify specific behaviors or expectations. -
Matchers: Jasmine provides a wide range of built-in matchers such as
toEqual
,toBe
,toContain
, etc., which allow you to make assertions about values or conditions in your tests. -
Spies: Spies are a powerful feature of Jasmine that allows you to track function calls, return values, and other interactions during testing.
In addition to Jasmine, Angular also includes another testing tool called Karma as part of its default setup. Karma is a task runner that helps automate the execution of unit tests across different browsers or environments.
Currently we are making the transition from Jasmin to Jest.
##Recommentations when creating Unit test scripts using Jasmine or Jest
When writing unit test scripts , it's important to follow best practices to ensure effective and maintainable tests. Here are some guidelines for writing good unit test scripts:
-
Describe the behavior: Begin by using the
describe
function to define a suite of related tests. This helps organize your tests and provides a clear structure. -
Use
it
blocks: Within eachdescribe
block, use theit
function to define individual test cases. Eachit
block should focus on testing a specific behavior or functionality. - Arrange-Act-Assert (AAA) pattern: Follow the AAA pattern for structuring your tests:
- Arrange: Set up any necessary preconditions or initial state.
- Act: Perform the action or operation being tested.
- Assert: Verify that the expected outcome matches the actual result.
- Use matchers: Jasmine or Jest provides built-in matchers that allow you to make assertions about values and behaviors. Some commonly used matchers include:
-
expect(value).toBe(expected)
: Checks if two values are strictly equal. -
expect(value).toEqual(expected)
: Performs deep equality comparison between two objects. -
expect(value).toBeTruthy()
: Checks if a value is truthy (not null, undefined, false, 0, etc.).
-
Setup and teardown: Use the
beforeEach
,afterEach
,beforeAll
, and/orafterAll
functions provided by Jasmine or Jest to set up any necessary preconditions before each test case or perform cleanup after each test case. - Mock dependencies: When testing code with external dependencies (e.g., API calls), consider using mocks or stubs to isolate your code under test from those dependencies.
- Test edge cases and boundaries: Ensure that your unit tests cover different scenarios including edge cases, boundary conditions, error handling, etc., to provide comprehensive coverage of your codebase.
- Keep tests independent and isolated: Each unit test should be independent of others so that they can be run individually without relying on previous results.
- Provide descriptive failure messages: Make sure your failure messages clearly indicate what went wrong when a test fails so that it's easier to debug issues.
- Run tests frequently: Run your unit tests regularly during development as part of continuous integration processes to catch issues early on. These tests should be part of the Pipline.
##Nextens standards for Karma Jasmine or Jest Unit tests The following aspects of your Angular pages are candidate to be testsed:
- Input binding
- Services
- Pipe data transformations
- Directives
In the next sections we will how the tests could be contructed.
###Components - logic and child components input bindings Input binding is one of the aspects that needs to be tested to verify the correct behaviour. Here is a example:
// Assume we have a component called MyComponent with an input property called inputValue
describe('MyComponent', () => {
let component: MyComponent;
let fixture: ComponentFixture<MyComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [MyComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should bind the input value correctly', () => {
const inputValue = 'Test Input';
// Set the input value
component.inputValue = inputValue;
// Trigger change detection to update the bound value
fixture.detectChanges();
// Assert that the bound value is updated correctly
expect(component.inputValue).toEqual(inputValue);
});
});
###Example code for Components - logic and child components input bindings
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ComponentFixture, fakeAsync, inject, TestBed, tick, waitForAsync } from '@angular/core/testing';
import { By, DomSanitizer } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ActivatedRoute } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { Environment, getEnvironmentMock } from '@infrastructure/configuration';
import { BreadcrumbComponent, BreadcrumbModule } from '@nextens/stitch/breadcrumb';
import { InPageSearchAreaDirective, InPageSearchComponent, InPageSearchModule } from '@nextens/stitch/in-page-search';
import { OverviewPageHeaderModule } from '@nextens/stitch/overview-page-header';
import { StickyComponent, StickyModule } from '@nextens/stitch/sticky';
import { NaslagBreadcrumbConfiguration } from 'src/app/shared/breadcrumb';
import { PreProcessingExternalContentModule, ExternalHtmlContentComponent } from 'src/app/shared/components/external-content';
import { PipesModule } from '@shared/pipes';
import { ActivatedRouteMock, StickyMockProviders, TestUtilities } from 'src/app/shared/tests';
import { OnderwerpenBreadcrumbConfiguration } from '../../breadcrumb';
import { OnderwerpResult } from '../../models/onderwerp-result';
import { OnderwerpenSingleComponent } from '../single.component';
import { PrintButtonModule } from 'src/app/shared/components/print-button';
import { PrintButtonTestingModule } from 'src/app/shared/components/print-button/tests/print-button-testing.module';
import { HtmlContentPreprocessorService } from '@nextens/html-content-preprocessor';
import { ThreeColumnLayoutModule } from '@nextens/stitch/layout';
import { PageHeaderComponent, PageHeaderModule } from '@nextens/stitch/page-header';
describe('Onderwerpen single article component (functional)', () => {
let component: OnderwerpenSingleComponent;
let fixture: ComponentFixture<OnderwerpenSingleComponent>;
const naslagCrumb = new NaslagBreadcrumbConfiguration(getEnvironmentMock());
const onderwerpenCrumb = new OnderwerpenBreadcrumbConfiguration(naslagCrumb);
const singleArticleMock = new OnderwerpResult();
beforeEach(() => {
singleArticleMock.id = '1';
singleArticleMock.title = 'Lorem ipsumus neque.';
singleArticleMock.content = '<ul><li>Lorem ipsumus neque.</li></ul>';
});
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [OnderwerpenSingleComponent],
imports: [
RouterTestingModule,
HttpClientTestingModule,
BreadcrumbModule,
PreProcessingExternalContentModule,
PipesModule,
PageHeaderModule,
StickyModule,
InPageSearchModule,
NoopAnimationsModule,
PrintButtonModule,
PrintButtonTestingModule,
ThreeColumnLayoutModule
],
providers: [
{
provide: ActivatedRoute,
useFactory: () =>
ActivatedRouteMock.WithDataAndObservableParams(
{
single: singleArticleMock,
},
{}
),
},
{ provide: OnderwerpenBreadcrumbConfiguration, useValue: onderwerpenCrumb },
{ provide: Environment, useValue: getEnvironmentMock() },
StickyMockProviders,
{ provide: HtmlContentPreprocessorService, useValue: {process: (htmlContent: string) => htmlContent}}
],
}).compileComponents();
})
);
beforeEach(() => {
fixture = TestBed.createComponent(OnderwerpenSingleComponent);
component = fixture.componentInstance;
TestUtilities.addCommonParentSelectorsForStickyComponent(fixture);
fixture.detectChanges();
});
it('should have InPageSearch loaded',
fakeAsync(() => {
fixture.whenStable().then(() => {
const inPageSearchComponent = fixture.debugElement.query(By.directive(InPageSearchComponent)).componentInstance;
expect(inPageSearchComponent).toBeTruthy();
const inPageSearchAreaDirective = fixture.debugElement.query(By.directive(InPageSearchAreaDirective)).componentInstance;
expect(inPageSearchAreaDirective).toBeTruthy();
});
})
);
it(
'should have BreadcrumbComponent loaded',
waitForAsync(() => {
const breadcrumbComponent = fixture.debugElement.query(By.directive(BreadcrumbComponent)).componentInstance;
expect(component.breadcrumb).toEqual(breadcrumbComponent.breadcrumb);
})
);
it(
'should have PageHeaderComponent loaded',
waitForAsync(() => {
const pageHeaderComponent = fixture.debugElement.query(By.directive(PageHeaderComponent)).componentInstance;
expect(pageHeaderComponent).toBeTruthy();
})
);
it('should have ExternalHtmlContent loaded', inject([DomSanitizer], (domSanitizer: DomSanitizer) => {
const externalHtmlContentComponet = fixture.debugElement.query(By.directive(ExternalHtmlContentComponent)).componentInstance;
expect(component.singleArticle.content).toEqual(externalHtmlContentComponet.content);
}));
});
###Services Services that are invoked from JavaScript can be tested, in this example we are testing the authentication service:
import { OidcClientAuthService } from './oidc-client-auth.service';
import { UserContext } from '../user-context';
describe('OidcClientAuthService', () => {
let user: any;
let state: any;
let mapperMock: any;
let userManagerMock: any;
let authEventsMock: any;
let service: OidcClientAuthService;
beforeEach(() => {
state = { requestedUrl: 'test?x=y' };
user = { state, profile: { sub: '[email protected]', accessToken: 'test_token' } };
mapperMock = {
map: jest.fn()
};
userManagerMock = {
getUser: jest.fn(),
signinRedirect: jest.fn(),
signinRedirectCallback: jest.fn()
};
authEventsMock = {
raiseUserSignedOutEvent: jest.fn(),
raiseUserSignedInEvent: jest.fn(),
};
service = new OidcClientAuthService(userManagerMock, mapperMock, authEventsMock);
});
it('should return that user is authenticated given valid user stored in user manager', async () => {
userManagerMock.getUser.mockReturnValue(Promise.resolve(user));
const isAuthenticated = await service.isAuthenticated();
expect(userManagerMock.getUser).toHaveBeenCalled();
expect(isAuthenticated).toBeTruthy();
});
[...]
Please note this this sample only covers the happy flow.
###Pipe data transformations
The transformation of data using Pipes could be tested in a Unit test. In this example we are testing the highlight function:
import { HighlightPipe } from './highlight.pipe';
describe('Hightlight pipe', () => {
let hightlightPipe: HighlightPipe;
const text = 'Hello how are you doing? I am doing fine.';
beforeEach(() => {
hightlightPipe = new HighlightPipe();
});
it('No search text will return text', () => {
const actual = hightlightPipe.transform(text, '',3,0);
expect(actual).toEqual(text);
});
it('No match will return text', () => {
const actual = hightlightPipe.transform(text, 'xxxxx',3,0);
expect(actual).toEqual(text);
});
it('Matching text will be hightlighed', () => {
const actual = hightlightPipe.transform(text, 'doin',3,0);
const expected = `Hello how are you <span class="u-highlight">doin</span>g? I am <span class="u-highlight">doin</span>g fine.`;
expect(actual).toEqual(expected);
});
it('Matching text at the end will be hightlighed', () => {
const actual = hightlightPipe.transform(text, 'fine.', 2, 0);
const expected = `Hello how are you doing? I am doing <span class="u-highlight">fine.</span>`;
expect(actual).toEqual(expected);
});
###Directives Directives could be teste to make sure the behaviour of the component is correct when the Directive has been applied to a component. In this example we are testing the (extended) behaviour of the popup:
import { Component, DebugElement, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { NgxTippyService } from 'ngx-tippy-wrapper';
import { PopupOptions } from './models';
import { PopupModule } from './popup.module';
@Component({
template: `
<div id="commonContainer">
<div id="popupContainer" *nextensPopup="popupOptions">Has Popover</div>
<ng-template #template>
<div id="popupBody">Template</div>
</ng-template>
</div>
`,
})
class TestComponent implements OnInit {
@ViewChild('template', { static: true }) template: TemplateRef<any>;
popupOptions: PopupOptions;
ngOnInit() {
this.popupOptions = {
id: 'popup',
templateRef: this.template,
trigger: 'click',
onShow: (instance) => {},
} as PopupOptions;
}
}
describe('PopupDirective', () => {
let fixture: ComponentFixture<TestComponent>;
let component: TestComponent;
let commonContainerElement: DebugElement;
let triggerElement: DebugElement;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [TestComponent],
imports: [PopupModule],
});
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
fixture.detectChanges();
commonContainerElement = fixture.debugElement.query(By.css('#commonContainer'));
triggerElement = fixture.debugElement.query(By.css('#popupContainer'));
});
it('should open popover on click when trigger is set to "click"', () => {
const tippyService = commonContainerElement.injector.get(NgxTippyService);
const instance = tippyService.getInstance(component.popupOptions.id);
const spy = jest.spyOn(instance.props, 'onShow');
triggerElement.nativeElement.click();
expect(spy).toHaveBeenCalled();
});
it('should not open popover on focus when trigger is set to "click"', () => {
const tippyService = commonContainerElement.injector.get(NgxTippyService);
const instance = tippyService.getInstance(component.popupOptions.id);
const spy = jest.spyOn(instance.props, 'onShow');
triggerElement.nativeElement.focus();
expect(spy).not.toHaveBeenCalled();
});
});
[WIP]