docs/en/framework/ui/angular/testing.md
//[doc-seo]
{
"Description": "Learn how to unit test your ABP Angular UI applications with preconfigured Vitest and TestBed, plus ABP-specific testing topics."
}
ABP Angular UI is tested like any other Angular application. So, the guide here applies to ABP too. That said, we would like to point out some unit testing topics specific to ABP Angular applications.
The application template you download is preconfigured for unit testing. You can add a *.spec.ts file and run yarn test without adding extra test infrastructure.
| Package / API | Purpose |
|---|---|
| Vitest | Test runner and assertion library. |
| jsdom | Browser-like DOM environment for component tests. |
@angular/core/testing (TestBed) | The standard testing utilities of Angular for components, services, and pipes. |
@abp/ng.core/testing | ABP testing module and helpers that replace real ABP services with mocks. |
@abp/ng.theme.shared/testing | Testing module for shared theme features such as validation. |
ABP Angular packages in the framework repository use the same Vitest setup. Library tests there also use @ngneat/spectator/vitest for HTTP and component tests, but the application template uses TestBed directly.
The test target in angular.json uses Angular's built-in Vitest builder:
// angular.json
"test": {
"builder": "@angular/build:unit-test"
}
Spec files are compiled with tsconfig.spec.json, which enables Vitest globals:
// tsconfig.spec.json
{
"compilerOptions": {
"types": ["vitest/globals"]
},
"include": ["src/**/*.spec.ts"]
}
You do not need a karma.conf.js file. Angular CLI wires Vitest and jsdom for you.
Run tests in watch mode:
yarn test
Run tests once, which is useful for CI:
ng test --watch=false
Vitest exits with a non-zero status code when a test fails, so the command above works in pipelines.
An over-simplified spec file looks like this:
import { CoreTestingModule } from "@abp/ng.core/testing";
import { ThemeSharedTestingModule } from "@abp/ng.theme.shared/testing";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { NgxValidateCoreModule } from "@ngx-validate/core";
import { AuthService } from "@abp/ng.core";
import { vi } from "vitest";
import { MyComponent } from "./my.component";
describe("MyComponent", () => {
let fixture: ComponentFixture<MyComponent>;
let mockAuthService: { isAuthenticated: boolean; navigateToLogin: ReturnType<typeof vi.fn> };
beforeEach(async () => {
mockAuthService = {
isAuthenticated: false,
navigateToLogin: vi.fn(),
};
await TestBed.configureTestingModule({
imports: [
CoreTestingModule.withConfig(),
ThemeSharedTestingModule.withConfig(),
NgxValidateCoreModule,
MyComponent,
],
providers: [
{
provide: AuthService,
useValue: mockAuthService,
},
],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(MyComponent);
fixture.detectChanges();
});
it("should be initiated", () => {
expect(fixture.componentInstance).toBeTruthy();
});
});
If you take a look at the imports, you will notice that we have prepared some testing modules to replace built-in ABP modules. This is necessary for providing mocks for some features which otherwise would break your tests. Please remember to use testing modules and call their withConfig static method.
Current templates use standalone components, so put the component under test in the imports array instead of declarations.
If your application uses @abp/ng.theme.basic, also import ThemeBasicTestingModule.withConfig() from @abp/ng.theme.basic/testing.
Use Vitest mocks instead of Jasmine spies:
import { vi } from "vitest";
const deleteSpy = vi.fn().mockReturnValue(of(null));
fixture.componentInstance.service.delete = deleteSpy;
expect(deleteSpy).toHaveBeenCalledWith("some-id");
The template's home.component.spec.ts is a good reference for mocking ABP services and asserting DOM behavior with TestBed.
Tests run in jsdom, not a real browser. Components attached to document.body — such as modals, confirmation dialogs, and toasts — may not be removed automatically between specs.
We have prepared a simple function with which you can clear leftover DOM elements after each test:
// other imports
import { clearPage } from "@abp/ng.core/testing";
describe("MyComponent", () => {
let fixture: ComponentFixture<MyComponent>;
afterEach(() => clearPage(fixture));
// specs here
});
Please use it when you test features that render into the document body. Otherwise you may end up with multiple copies of modals, confirmation boxes, and similar elements.
Some components, modals in particular, work off the change-detection cycle. In other words, you cannot reach DOM elements inserted by these components immediately after opening them. Similarly, inserted elements are not immediately destroyed upon closing them.
For this purpose, we have prepared a wait function:
// other imports
import { wait } from "@abp/ng.core/testing";
describe("MyComponent", () => {
let fixture: ComponentFixture<MyComponent>;
it("should open a modal", async () => {
const openModalBtn = fixture.nativeElement.querySelector('[role="button"]');
openModalBtn.click();
await wait(fixture);
const modal = fixture.nativeElement.ownerDocument.querySelector('[role="dialog"]');
expect(modal).toBeTruthy();
});
});
The wait function takes a second parameter, i.e. timeout (default: 0). Try not to use it though. Using a timeout bigger than 0 is usually a signal that something is not quite right.
Although you can test your code with Angular TestBed, you may find Angular Testing Library a good alternative. It is not included in the application template by default, but you can add @testing-library/angular and @testing-library/user-event if you prefer that style.
The ABP testing modules work the same way with Testing Library:
import { CoreTestingModule } from "@abp/ng.core/testing";
import { ThemeSharedTestingModule } from "@abp/ng.theme.shared/testing";
import { ComponentFixture } from "@angular/core/testing";
import { NgxValidateCoreModule } from "@ngx-validate/core";
import { render, screen } from "@testing-library/angular";
import { MyComponent } from "./my.component";
describe("MyComponent", () => {
let fixture: ComponentFixture<MyComponent>;
beforeEach(async () => {
const result = await render(MyComponent, {
imports: [
CoreTestingModule.withConfig(),
ThemeSharedTestingModule.withConfig(),
NgxValidateCoreModule,
],
providers: [
/* mock providers here */
],
});
fixture = result.fixture;
});
it("should be initiated", () => {
expect(fixture.componentInstance).toBeTruthy();
});
});
The queries in Angular Testing Library follow practices for maintainable tests, the user event package provides a human-like interaction with the DOM, and the library in general has a clear API that simplifies component testing. Please find some useful links below:
When you use Testing Library with modals or confirmation dialogs, combine it with clearPage and wait from @abp/ng.core/testing as shown above.
Here is an example based on the application template's home.component.spec.ts. It shows how to mock an ABP service and assert component state and DOM output:
import { CoreTestingModule } from "@abp/ng.core/testing";
import { ThemeSharedTestingModule } from "@abp/ng.theme.shared/testing";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { NgxValidateCoreModule } from "@ngx-validate/core";
import { AuthService } from "@abp/ng.core";
import { vi } from "vitest";
import { HomeComponent } from "./home.component";
describe("HomeComponent", () => {
let fixture: ComponentFixture<HomeComponent>;
let mockAuthService: { isAuthenticated: boolean; navigateToLogin: ReturnType<typeof vi.fn> };
beforeEach(async () => {
mockAuthService = {
isAuthenticated: false,
navigateToLogin: vi.fn(),
};
await TestBed.configureTestingModule({
imports: [
CoreTestingModule.withConfig(),
ThemeSharedTestingModule.withConfig(),
NgxValidateCoreModule,
HomeComponent,
],
providers: [
{
provide: AuthService,
useValue: mockAuthService,
},
],
}).compileComponents();
});
it("should be initiated", () => {
fixture = TestBed.createComponent(HomeComponent);
fixture.detectChanges();
expect(fixture.componentInstance).toBeTruthy();
});
describe("when login state is false", () => {
beforeEach(() => {
mockAuthService.isAuthenticated = false;
fixture = TestBed.createComponent(HomeComponent);
fixture.detectChanges();
});
it("hasLoggedIn should be false", () => {
expect(fixture.componentInstance.hasLoggedIn).toBe(false);
});
it("button should exist", () => {
const button = fixture.nativeElement.querySelector('[role="button"]');
expect(button).toBeDefined();
});
describe("when button clicked", () => {
beforeEach(() => {
const button = fixture.nativeElement.querySelector('[role="button"]');
button.click();
});
it("navigateToLogin should have been called", () => {
expect(mockAuthService.navigateToLogin).toHaveBeenCalled();
});
});
});
});
For list pages with modals, confirmations, and service proxies, keep using the ABP testing modules, mock your generated proxy services with vi.fn(), and use clearPage / wait when body-level UI is involved.
Run unit tests once in CI with:
ng test --watch=false
If you need a dedicated CI configuration, add one under the test target in angular.json:
// angular.json
"test": {
"builder": "@angular/build:unit-test",
"configurations": {
"ci": {
"watch": false
}
}
}
Then run:
ng test --configuration=ci