Angular 100 🅰️ Testing — Unit Tests with Jasmine and Karma
Every Angular application that ships without tests is an application that ships with unknown defects. The tests are the specification. They describe what the code is supposed to do. When the code changes, the tests tell you whether the change broke something. When a new developer joins the team, the tests are the documentation of the intended behavior. Angular has a built-in testing infrastructure: Jasmine for the test syntax and assertions, and Karma for running the tests in a real browser.
The Angular CLI configures both by default. When you create a new project with ng new, the testing setup is already in place. The ng test command compiles the application, launches a browser, runs the tests, and reports the results. The tests are written in .spec.ts files next to the code they test. The naming convention is standard: counter.component.ts has a corresponding counter.component.spec.ts .
Key point: Angular’s unit tests run in a real browser through Karma. This is different from Node-based test runners like Jest or Vitest. The browser environment means the tests exercise the actual DOM, the actual rendering, and the actual change detection. The trade-off is speed: Karma tests are slower to start than Node tests because they launch a browser. The benefit is fidelity: the tests run in the environment the application runs in.
Why unit tests matter
A unit test verifies one piece of the application in isolation. It calls a method and checks the result. It renders a component and checks the DOM. It injects a service and checks its state. The test is fast, focused, and repeatable.
The regression problem. A developer changes a function that other code depends on. The change seems correct. The application is deployed. A feature that worked yesterday is broken today. Without tests, the break is discovered by a user. With tests, the break is discovered before the commit.
The refactoring problem. A developer needs to restructure a component. The behavior must remain the same. Without tests, the only way to verify the behavior is to test it manually. With tests, the behavior is verified automatically. The tests are the safety net that makes refactoring safe.
The specification problem. A requirement says “the counter increments when the button is clicked.” The test says the same thing in code. The test is the executable specification. When the requirement changes, the test changes. When the code changes, the test verifies that the requirement is still met.
The documentation problem. A new developer reads the test file and understands what the component does, what its inputs are, what its outputs are, and what the edge cases are. The test file is often more up-to-date than the documentation because the tests fail when the code changes.
The trade-off. Tests take time to write and maintain. A test suite that is slow or flaky becomes a burden. The team must invest in the tests and keep them reliable. The investment pays back in the confidence to change the code, the speed of the feedback loop, and the reduction in production defects.
a. The Testing Setup
The Angular CLI configures Jasmine and Karma when the project is created. The package.json includes jasmine-core, karma, karma-jasmine, karma-chrome-launcher, and karma-coverage. The angular.json file includes the test target with the @angular-devkit/build-angular:karma builder. The tsconfig.spec.json file includes the test files and the Jasmine types .
The test files are named with the .spec.ts extension and placed next to the code they test. The ng test command compiles the application, launches Chrome, runs the tests, and watches for changes. The --watch flag keeps the browser open and re-runs the tests on every change. The --code-coverage flag produces a coverage report .
A minimal test file has three parts: the describe block, the beforeEach block, and the it blocks.
describe('Calculator', () => {
let calculator: Calculator;
beforeEach(() => {
calculator = new Calculator();
});
it('should add two numbers', () => {
expect(calculator.add(2, 3)).toBe(5);
});
it('should subtract two numbers', () => {
expect(calculator.subtract(5, 3)).toBe(2);
});
});
The describe block groups related tests. The beforeEach block runs before every it block and sets up the test state. The it blocks are the individual tests. The expect function is the assertion .
b. Testing Components with TestBed
A component test uses the TestBed to configure a testing module. The TestBed is the Angular testing utility that provides the component’s dependencies and creates the component fixture .
The TestBed.configureTestingModule method takes the same configuration as an @NgModule. The declarations array lists the components and directives used in the test. The providers array lists the services. The imports array lists the modules .
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({
imports: [CounterComponent],
}).compileComponents();
fixture = TestBed.createComponent(CounterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should increment the count when the button is clicked', () => {
const button = fixture.nativeElement.querySelector('button');
button.click();
fixture.detectChanges();
expect(component.count()).toBe(1);
});
});
The compileComponents method compiles the component’s template and styles. The createComponent method creates the component and its fixture. The fixture.componentInstance is the component instance. The fixture.nativeElement is the component’s DOM element. The fixture.detectChanges method triggers change detection .
The fixture.nativeElement.querySelector method finds an element in the rendered DOM. The button.click() method simulates a click. The fixture.detectChanges() method updates the DOM after the click. The assertion checks the component’s state .
c. Testing Services and HTTP
A service test does not need the TestBed unless the service has dependencies. A service with no dependencies can be instantiated directly with new. A service with dependencies uses the TestBed to provide the dependencies.
import { TestBed } from '@angular/core/testing';
import { CalculatorService } from './calculator.service';
describe('CalculatorService', () => {
let service: CalculatorService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(CalculatorService);
});
it('should add two numbers', () => {
expect(service.add(2, 3)).toBe(5);
});
});
The TestBed.inject method retrieves the service instance from the injector. The service is instantiated with its dependencies .
A service that makes HTTP requests uses the HttpClientTestingModule and the HttpTestingController. The HttpTestingController intercepts the HTTP requests and allows the test to provide mock responses .
import { TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting, HttpTestingController } from '@angular/common/http/testing';
import { UserService } from './user.service';
describe('UserService', () => {
let service: UserService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
provideHttpClient(),
provideHttpClientTesting(),
],
});
service = TestBed.inject(UserService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});
it('should fetch users', () => {
const mockUsers = [{ id: 1, name: 'Alice' }];
service.getUsers().subscribe((users) => {
expect(users).toEqual(mockUsers);
});
const req = httpMock.expectOne('/api/users');
expect(req.request.method).toBe('GET');
req.flush(mockUsers);
});
});
The provideHttpClient and provideHttpClientTesting providers set up the HTTP client with the testing backend. The httpMock.expectOne method asserts that a request was made to the specified URL. The req.flush method provides the mock response. The httpMock.verify method asserts that no unexpected requests were made .
d. Spies and Mocks
Jasmine provides the spyOn function to replace a method with a spy. The spy records the calls and allows the test to provide a return value.
describe('UserService', () => {
let service: UserService;
let httpSpy: jasmine.Spy;
beforeEach(() => {
httpSpy = jasmine.createSpy('get').and.returnValue(of([]));
service = new UserService({ get: httpSpy } as any);
});
it('should call the http client', () => {
service.getUsers();
expect(httpSpy).toHaveBeenCalledWith('/api/users');
});
});
The jasmine.createSpy method creates a spy function. The and.returnValue method sets the return value. The toHaveBeenCalledWith matcher asserts that the spy was called with the specified arguments .
A spy can also be attached to an existing object with spyOn:
const userService = TestBed.inject(UserService);
spyOn(userService, 'getUsers').and.returnValue(of([]));
The spyOn method replaces the method with a spy. The original method is not called unless and.callThrough() is used .
Complete Example Session
This session tests a counter component, a calculator service, and an HTTP service.
// ============================================
// PART 1: THE COUNTER COMPONENT
// ============================================
// counter.component.ts
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<p>Count: {{ count() }}</p>
<button (click)="increment()">+</button>
<button (click)="decrement()">−</button>
<button (click)="reset()">Reset</button>
`,
})
export class CounterComponent {
count = signal(0);
increment(): void {
this.count.update((v) => v + 1);
}
decrement(): void {
this.count.update((v) => v - 1);
}
reset(): void {
this.count.set(0);
}
}
// ============================================
// PART 2: THE COUNTER COMPONENT TEST
// ============================================
// counter.component.spec.ts
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({
imports: [CounterComponent],
}).compileComponents();
fixture = TestBed.createComponent(CounterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create the component', () => {
expect(component).toBeTruthy();
});
it('should start with count 0', () => {
expect(component.count()).toBe(0);
});
it('should increment the count', () => {
component.increment();
expect(component.count()).toBe(1);
});
it('should decrement the count', () => {
component.decrement();
expect(component.count()).toBe(-1);
});
it('should reset the count', () => {
component.increment();
component.increment();
component.reset();
expect(component.count()).toBe(0);
});
it('should increment when the + button is clicked', () => {
const buttons = fixture.nativeElement.querySelectorAll('button');
buttons[0].click();
fixture.detectChanges();
expect(component.count()).toBe(1);
});
it('should display the count in the template', () => {
component.increment();
fixture.detectChanges();
const p = fixture.nativeElement.querySelector('p');
expect(p.textContent).toContain('Count: 1');
});
});
// ============================================
// PART 3: THE CALCULATOR SERVICE
// ============================================
// calculator.service.ts
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class CalculatorService {
add(a: number, b: number): number {
return a + b;
}
subtract(a: number, b: number): number {
return a - b;
}
multiply(a: number, b: number): number {
return a * b;
}
divide(a: number, b: number): number {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
}
// ============================================
// PART 4: THE CALCULATOR SERVICE TEST
// ============================================
// calculator.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { CalculatorService } from './calculator.service';
describe('CalculatorService', () => {
let service: CalculatorService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(CalculatorService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should add two numbers', () => {
expect(service.add(2, 3)).toBe(5);
});
it('should subtract two numbers', () => {
expect(service.subtract(5, 3)).toBe(2);
});
it('should multiply two numbers', () => {
expect(service.multiply(4, 5)).toBe(20);
});
it('should divide two numbers', () => {
expect(service.divide(10, 2)).toBe(5);
});
it('should throw when dividing by zero', () => {
expect(() => service.divide(10, 0)).toThrowError('Division by zero');
});
});
// ============================================
// PART 5: THE USER SERVICE WITH HTTP
// ============================================
// user.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface User {
id: number;
name: string;
}
@Injectable({ providedIn: 'root' })
export class UserService {
private http = inject(HttpClient);
getUsers(): Observable<User[]> {
return this.http.get<User[]>('/api/users');
}
getUser(id: number): Observable<User> {
return this.http.get<User>(`/api/users/${id}`);
}
createUser(user: Omit<User, 'id'>): Observable<User> {
return this.http.post<User>('/api/users', user);
}
}
// ============================================
// PART 6: THE USER SERVICE TEST
// ============================================
// user.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting, HttpTestingController } from '@angular/common/http/testing';
import { UserService, User } from './user.service';
describe('UserService', () => {
let service: UserService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
provideHttpClient(),
provideHttpClientTesting(),
],
});
service = TestBed.inject(UserService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});
it('should fetch users', () => {
const mockUsers: User[] = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
];
service.getUsers().subscribe((users) => {
expect(users.length).toBe(2);
expect(users).toEqual(mockUsers);
});
const req = httpMock.expectOne('/api/users');
expect(req.request.method).toBe('GET');
req.flush(mockUsers);
});
it('should fetch a single user', () => {
const mockUser: User = { id: 1, name: 'Alice' };
service.getUser(1).subscribe((user) => {
expect(user).toEqual(mockUser);
});
const req = httpMock.expectOne('/api/users/1');
expect(req.request.method).toBe('GET');
req.flush(mockUser);
});
it('should create a user', () => {
const newUser = { name: 'Carol' };
const createdUser: User = { id: 3, name: 'Carol' };
service.createUser(newUser).subscribe((user) => {
expect(user).toEqual(createdUser);
});
const req = httpMock.expectOne('/api/users');
expect(req.request.method).toBe('POST');
expect(req.request.body).toEqual(newUser);
req.flush(createdUser);
});
it('should handle a 404 error', () => {
service.getUser(999).subscribe({
next: () => fail('Expected an error'),
error: (error) => {
expect(error.status).toBe(404);
},
});
const req = httpMock.expectOne('/api/users/999');
req.flush('Not found', { status: 404, statusText: 'Not Found' });
});
});
// ============================================
// PART 7: THE SPY EXAMPLE
// ============================================
// notification.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { NotificationService } from './notification.service';
import { UserService } from './user.service';
import { of } from 'rxjs';
describe('NotificationService', () => {
let service: NotificationService;
let userService: jasmine.SpyObj<UserService>;
beforeEach(() => {
const spy = jasmine.createSpyObj('UserService', ['getUsers']);
TestBed.configureTestingModule({
providers: [
NotificationService,
{ provide: UserService, useValue: spy },
],
});
service = TestBed.inject(NotificationService);
userService = TestBed.inject(UserService) as jasmine.SpyObj<UserService>;
});
it('should notify active users', () => {
userService.getUsers.and.returnValue(of([{ id: 1, name: 'Alice' }]));
service.notifyAll();
expect(userService.getUsers).toHaveBeenCalled();
});
});
// ============================================
// PART 8: THE ASYNC TEST
// ============================================
// async.service.spec.ts
import { TestBed, fakeAsync, tick } from '@angular/core/testing';
import { AsyncService } from './async.service';
describe('AsyncService', () => {
let service: AsyncService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(AsyncService);
});
it('should resolve after a delay', fakeAsync(() => {
let result: string | undefined;
service.getDelayedValue().then((value) => (result = value));
tick(1000);
expect(result).toBe('done');
}));
});
// ============================================
// PART 9: THE TEST COMMANDS
// ============================================
// Run all tests
// ng test
// Run once and exit
// ng test --watch=false
// Run with code coverage
// ng test --code-coverage
// Run a specific test file
// ng test --include='**/counter.component.spec.ts'
// ============================================
// PART 10: THE TESTING CHECKLIST
// ============================================
// 1. Write tests for every component
// 2. Write tests for every service
// 3. Test the happy path
// 4. Test the edge cases
// 5. Test the error cases
// 6. Use TestBed for components with dependencies
// 7. Use HttpTestingController for HTTP
// 8. Use spies for external dependencies
// 9. Use fakeAsync for timers
// 10. Run the tests in CI
The ten parts cover the counter component, the counter component test, the calculator service, the calculator service test, the user service with HTTP, the user service test, the spy example, the async test, the test commands, and the testing checklist.
Quick Reference
The Jasmine Functions
| Function | Purpose |
|---|---|
describe | Group related tests |
it | Define a single test |
beforeEach | Run before each test |
afterEach | Run after each test |
beforeAll | Run once before all tests |
afterAll | Run once after all tests |
expect | Assert a value |
fail | Force a test failure |
spyOn | Replace a method with a spy |
jasmine.createSpy | Create a standalone spy |
jasmine.createSpyObj | Create an object with spy methods |
The Jasmine Matchers
| Matcher | Purpose |
|---|---|
toBe | Strict equality |
toEqual | Deep equality |
toBeTruthy | Truthy value |
toBeFalsy | Falsy value |
toContain | Contains a value |
toThrowError | Throws an error |
toHaveBeenCalled | Spy was called |
toHaveBeenCalledWith | Spy was called with args |
The TestBed Methods
| Method | Purpose |
|---|---|
configureTestingModule | Configure the test module |
compileComponents | Compile the template |
createComponent | Create the component |
inject | Retrieve a service |
overrideProvider | Override a provider |
The ComponentFixture Properties
| Property | Purpose |
|---|---|
componentInstance | The component instance |
nativeElement | The DOM element |
debugElement | The debug element |
detectChanges | Trigger change detection |
whenStable | Wait for async stability |
The HTTP Testing
| Method | Purpose |
|---|---|
provideHttpClient() | Provide the HTTP client |
provideHttpClientTesting() | Provide the testing backend |
httpMock.expectOne(url) | Assert one request |
httpMock.expectNone(url) | Assert no request |
req.flush(data) | Provide a response |
req.error(error) | Provide an error |
httpMock.verify() | Assert no pending requests |
Best Practices
✅ Do This:
// Use beforeEach to set up the state
beforeEach(async () => {
await TestBed.configureTestingModule({ imports: [CounterComponent] }).compileComponents();
}); // ✅
// Call detectChanges after state changes
component.increment();
fixture.detectChanges(); // ✅
// Use HttpTestingController for HTTP
const req = httpMock.expectOne('/api/users');
req.flush(mockUsers); // ✅
// Verify no pending HTTP requests
afterEach(() => httpMock.verify()); // ✅
// Use spies for external dependencies
spyOn(userService, 'getUsers').and.returnValue(of([])); // ✅
❌ Don’t Do This:
// Don't test implementation details
expect(component['privateMethod']()).toBe(true); // ❌
// Don't forget to flush HTTP requests
httpMock.expectOne('/api/users'); // no flush → test hangs // ❌
// Don't use real HTTP in unit tests
http.get('/api/users'); // use HttpTestingController // ❌
// Don't rely on test order
// Each test should be independent. // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Test hangs | HTTP request not flushed | Use req.flush() |
| Change detection not running | detectChanges not called | Call fixture.detectChanges() |
| Service not found | Provider not configured | Add to providers |
| Async test fails | Promise not awaited | Use fakeAsync and tick |
| Test depends on order | Shared state | Reset in beforeEach |
| Coverage low | Tests missing | Add tests for edge cases |
Real-World Examples
1. Component Test
fixture = TestBed.createComponent(CounterComponent);
component = fixture.componentInstance;
2. DOM Query
const button = fixture.nativeElement.querySelector('button');
3. Click Simulation
button.click();
fixture.detectChanges();
4. Service Test
service = TestBed.inject(CalculatorService);
5. HTTP Test
const req = httpMock.expectOne('/api/users');
req.flush(mockUsers);
6. Spy
spyOn(userService, 'getUsers').and.returnValue(of([]));
7. Spy Object
const spy = jasmine.createSpyObj('UserService', ['getUsers']);
8. Async Test
it('should resolve', fakeAsync(() => {
let result;
service.getValue().then(v => result = v);
tick(1000);
expect(result).toBe('done');
}));
9. Error Test
expect(() => service.divide(10, 0)).toThrowError('Division by zero');
10. Run Tests
ng test --watch=false --code-coverage
Visual
The Testing Pyramid
┌──────────────────────────────────────────────┐
│ TESTING PYRAMID │
│ │
│ E2E │
│ (few) │
│ ─────── │
│ Integration │
│ (some) │
│ ─────────── │
│ Unit Tests │
│ (many) │
│ │
│ Unit tests are fast and focused. │
│ Integration tests verify the wiring. │
│ E2E tests verify the whole application. │
│ │
└──────────────────────────────────────────────┘
The Test Lifecycle
┌──────────────────────────────────────────────┐
│ TEST LIFECYCLE │
│ │
│ describe('Component') │
│ │ │
│ ├─ beforeAll() │
│ │ │
│ ├─ beforeEach() │
│ │ └─ Set up the state │
│ │ │
│ ├─ it('should do something') │
│ │ └─ Test body │
│ │ │
│ ├─ afterEach() │
│ │ └─ Clean up │
│ │ │
│ ├─ beforeEach() │
│ ├─ it('should do another thing') │
│ │ │
│ └─ afterAll() │
│ │
└──────────────────────────────────────────────┘
The HTTP Testing Flow
┌──────────────────────────────────────────────┐
│ HTTP TESTING FLOW │
│ │
│ service.getUsers().subscribe(...) │
│ │ │
│ ▼ │
│ httpMock.expectOne('/api/users') │
│ │ │
│ ▼ │
│ req.flush(mockUsers) │
│ │ │
│ ▼ │
│ The subscriber receives the data │
│ │ │
│ ▼ │
│ httpMock.verify() │
│ │
│ The HttpTestingController intercepts the │
│ request and provides the mock response. │
│ │
└──────────────────────────────────────────────┘
The TestBed Configuration
┌──────────────────────────────────────────────┐
│ TESTBED CONFIGURATION │
│ │
│ TestBed.configureTestingModule({ │
│ imports: [CounterComponent], │
│ providers: [UserService], │
│ }).compileComponents(); │
│ │
│ fixture = TestBed.createComponent(CounterComponent)│
│ component = fixture.componentInstance │
│ fixture.detectChanges() │
│ │
│ The TestBed provides the dependencies. │
│ The fixture wraps the component. │
│ The detectChanges triggers rendering. │
│ │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Test framework | Jasmine |
| Test runner | Karma |
| Test files | *.spec.ts |
| Test command | ng test |
| Component testing | TestBed, ComponentFixture |
| DOM query | fixture.nativeElement.querySelector |
| Change detection | fixture.detectChanges() |
| Service testing | TestBed.inject |
| HTTP testing | HttpTestingController |
| Spies | spyOn, jasmine.createSpy, jasmine.createSpyObj |
| Async testing | fakeAsync, tick, whenStable |
Key takeaways:
- Angular’s testing infrastructure is Jasmine and Karma. Jasmine provides the test syntax and assertions. Karma runs the tests in a real browser. The CLI configures both by default. The
ng testcommand runs the tests . - The
TestBedconfigures the testing module. TheconfigureTestingModulemethod takes the same configuration as an@NgModule. ThecompileComponentsmethod compiles the template. ThecreateComponentmethod creates the component and its fixture . - The
ComponentFixtureprovides access to the component and its DOM. ThecomponentInstanceis the component. ThenativeElementis the DOM element. ThedetectChangesmethod triggers change detection. ThewhenStablemethod waits for async stability . - The
HttpTestingControllerintercepts HTTP requests. TheexpectOnemethod asserts that a request was made. Theflushmethod provides the response. Theverifymethod asserts that no unexpected requests were made . - Jasmine spies replace methods with controllable implementations. The
spyOnmethod replaces a method on an existing object. Thejasmine.createSpymethod creates a standalone spy. Thejasmine.createSpyObjmethod creates an object with spy methods . - Async tests use
fakeAsyncandtick. ThefakeAsyncfunction wraps the test in a fake timer context. Thetickfunction advances the virtual clock. The promise callbacks run synchronously within the fake context . - The tests are the specification. They describe the intended behavior. They catch regressions. They document the component’s API. They make refactoring safe. A codebase without tests is a codebase with unknown defects.
Remember: Testing is not optional. Every component, every service, every HTTP call, every edge case should have a test. The TestBed provides the infrastructure. The ComponentFixture provides the component. The HttpTestingController provides the HTTP. The spies provide the mocks. The fakeAsync provides the time control. The tests run in a real browser through Karma. The ng test command runs them. Write the tests. Run them. Keep them green.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!