agent-rules-kit
Version:
Bootstrap of **Cursor** rules (`.mdc`) and mirror documentation (`.md`) for AI agent-guided projects.
258 lines (198 loc) • 9.12 kB
Markdown
---
description: Testing strategies, tools, and best practices for Angular applications.
globs: <root>/src/**/*.spec.ts
alwaysApply: true
---
Testing is a fundamental part of developing high-quality Angular applications in {projectPath}. Angular is designed with testability in mind and provides a robust testing framework and utilities.
A balanced testing strategy typically follows the testing pyramid:
1. **Unit Tests (Most Numerous)**:
- **Focus**: Test individual classes, methods, pipes, or functions in isolation.
- **Speed**: Very fast.
- **Tools**: Jasmine (framework), Karma (test runner for browser environment), Angular TestBed (for some isolated class tests without DOM).
2. **Component Tests (Integration/Shallow Integration Tests - Moderate Number)**:
- **Focus**: Test Angular components: their templates, class logic, interaction with dependencies (often mocked), and user interactions.
- **Speed**: Slower than unit tests, faster than E2E.
- **Tools**: Angular TestBed, Jasmine.
3. **End-to-End (E2E) Tests (Fewest)**:
- **Focus**: Test complete user flows through the application, simulating real user scenarios in a browser.
- **Speed**: Slowest.
- **Tools**: Traditionally Protractor (now deprecated by Angular team). Current recommendations include WebdriverIO, Cypress, or Playwright. Angular CLI can generate E2E setups for these.
## Key Testing Tools & Concepts
- **Jasmine**: The default testing framework for Angular projects. It provides functions like `describe()`, `it()`, `expect()`, `beforeEach()`, `afterEach()`, spies (`spyOn()`), etc.
- **Karma**: The default test runner. It executes tests in a browser environment, allowing you to test code that interacts with the DOM.
- **Angular TestBed**:
- Angular's primary utility for writing component tests. It creates an Angular module environment for the component under test.
- Allows you to configure a testing module, declare components, provide mock services, and interact with the component.
- Key methods: `TestBed.configureTestingModule()`, `TestBed.createComponent()`.
- **`ComponentFixture<T>`**:
- A handle on an instance of the tested component and its associated DOM element.
- Provides access to the component instance (`fixture.componentInstance`), its `DebugElement` (`fixture.debugElement`), and methods to trigger change detection (`fixture.detectChanges()`).
- **`DebugElement`**: An Angular abstraction that provides a way to inspect component and DOM element properties. `debugElement.nativeElement` gives access to the actual DOM element.
- **Spies, Stubs, and Mocks**:
- **Spies (`jasmine.createSpy()` or `spyOn(object, 'methodName')`)**: Track calls to functions, their arguments, and can optionally fake their implementation.
- **Stubs**: Replace functions with pre-programmed behavior, often returning specific values.
- **Mocks**: Objects that simulate the behavior of real dependencies. In Angular, you often provide mock implementations of services in `TestBed`.
## Writing Component Tests
Component tests are crucial for ensuring your UI behaves as expected.
```typescript
// my-greeter.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-my-greeter',
template: `
<h1>Hello, {{ name }}!</h1>
<button (click)="greet.emit('Hi from ' + name)">Greet Parent</button>
`,
})
export class MyGreeterComponent {
@Input() name: string = 'World';
@Output() greet = new EventEmitter<string>();
}
// my-greeter.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser'; // For querying DOM elements
import { MyGreeterComponent } from './my-greeter.component';
describe('MyGreeterComponent', () => {
let component: MyGreeterComponent;
let fixture: ComponentFixture<MyGreeterComponent>;
let h1: HTMLElement;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [MyGreeterComponent], // Declare the component being tested
}).compileComponents(); // Compile template and css
fixture = TestBed.createComponent(MyGreeterComponent);
component = fixture.componentInstance; // Get component instance
h1 = fixture.debugElement.query(By.css('h1')).nativeElement;
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should display default name if no input provided', () => {
fixture.detectChanges(); // Trigger change detection
expect(h1.textContent).toContain('Hello, World!');
});
it('should display provided name via @Input', () => {
component.name = 'Angular';
fixture.detectChanges();
expect(h1.textContent).toContain('Hello, Angular!');
});
it('should emit greet event when button is clicked', () => {
spyOn(component.greet, 'emit'); // Spy on the EventEmitter
component.name = 'Tester';
fixture.detectChanges();
const button = fixture.debugElement.query(
By.css('button')
).nativeElement;
button.click(); // Simulate button click
expect(component.greet.emit).toHaveBeenCalledWith('Hi from Tester');
});
});
```
Services often contain business logic or data access that needs to be tested.
```typescript
// data.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { of, throwError } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class DataService {
constructor(private http: HttpClient) {}
getData() {
return this.http.get('/api/data');
}
postData(item: any) {
if (!item) {
return throwError(() => new Error('Item cannot be null'));
}
return this.http.post('/api/data', item);
}
}
// data.service.spec.ts
import { TestBed } from '@angular/core/testing';
import {
HttpClientTestingModule,
HttpTestingController,
} from '@angular/common/http/testing';
import { DataService } from './data.service';
describe('DataService', () => {
let service: DataService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule], // Mock HttpClient
providers: [DataService],
});
service = TestBed.inject(DataService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify(); // Ensure no outstanding HTTP requests
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should retrieve data successfully', () => {
const mockData = [{ id: 1, name: 'Test' }];
service.getData().subscribe((data) => {
expect(data).toEqual(mockData);
});
const req = httpMock.expectOne('/api/data');
expect(req.request.method).toBe('GET');
req.flush(mockData); // Provide mock response
});
it('should return an error if item is null for postData', (done) => {
service.postData(null).subscribe({
error: (err) => {
expect(err.message).toBe('Item cannot be null');
done();
},
});
});
});
```
```typescript
// initial.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'initial' })
export class InitialPipe implements PipeTransform {
transform(value: string): string {
return value ? value.charAt(0).toUpperCase() + '.' : '';
}
}
// initial.pipe.spec.ts
import { InitialPipe } from './initial.pipe';
describe('InitialPipe', () => {
let pipe: InitialPipe;
beforeEach(() => {
pipe = new InitialPipe();
});
it('create an instance', () => {
expect(pipe).toBeTruthy();
});
it('should return the first letter capitalized with a dot', () => {
expect(pipe.transform('john')).toBe('J.');
});
it('should return empty string for empty input', () => {
expect(pipe.transform('')).toBe('');
});
});
```
Angular CLI integrates with Karma to produce code coverage reports (often using Istanbul).
- Run tests with coverage: `ng test --no-watch --code-coverage`
- Reports are typically generated in a `coverage/` directory.
- Aim for meaningful coverage, not just high percentages. Ensure critical logic paths are tested.
## Community Libraries for Testing
- **Spectator (by Netanel Basal)**: Provides a higher-level API for testing Angular components, directives, and services, reducing boilerplate.
- **ng-mocks (by Isaac Dat Dresner)**: A library for mocking dependencies in Angular tests with a focus on type safety and ease of use.
## End-to-End (E2E) Testing Considerations
- **Selector Strategy**: Use robust selectors (e.g., `data-testid` attributes) that are less prone to break with UI changes.
- **Page Object Model (POM)**: A design pattern to create an object repository for UI elements, making tests more readable and maintainable.
- **Wait Strategies**: Use explicit waits for elements to appear or become interactive to avoid flaky tests.
By implementing a comprehensive testing strategy using these tools and practices, you can ensure the reliability and quality of your Angular application in {projectPath}.
```
```