describe('HeroComponent', () => {
let component: HeroComponent;
let fixture: ComponentFixture<HeroComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [HeroComponent],
providers: [{ provide: HeroService, useValue: mockHeroService }]
}).compileComponents();
fixture = TestBed.createComponent(HeroComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
});
Call fixture.detectChanges() after creating the component to trigger initial change detection. Use fixture.componentInstance to access the component.
Why it matters: TestBed is fundamental to Angular testing. Shows you can isolate and test components effectively.
Real applications: Every Angular unit test uses TestBed. Setting up proper test configuration prevents flaky tests and false passes.
Common mistakes: Developers forget compileComponents() on lazy-compiled components. They don't call detectChanges() causing initialization issues. They don't properly mock dependencies.
it('should display name', () => {
component.name = 'Batman';
fixture.detectChanges();
const el = fixture.nativeElement.querySelector('h1');
expect(el.textContent).toContain('Batman');
});
it('should emit on click', () => {
spyOn(component.selected, 'emit');
fixture.nativeElement.querySelector('button').click();
expect(component.selected.emit).toHaveBeenCalledWith(component.hero);
});
Always call fixture.detectChanges() after setting input values. Use spyOn to verify output emissions without subscribing.
Why it matters: Tests understand component communication mechanisms. Shows you can verify data flow in both directions.
Real applications: All component interaction tests check inputs and outputs. This is core to Angular testing.
Common mistakes: Developers forget detectChanges() after setting inputs. They use subscribe instead of spyOn for outputs. They test implementation details instead of public API.
const mockService = jasmine.createSpyObj('HeroService', ['getHeroes']);
mockService.getHeroes.and.returnValue(of([{ id: 1, name: 'Hero' }]));
TestBed.configureTestingModule({
providers: [{ provide: HeroService, useValue: mockService }]
});
Always mock external dependencies like HTTP services and routers. Use of() from RxJS to return Observable mock data synchronously.
Why it matters: Mocking enables true unit testing with isolated components. Shows you can control dependencies for reproducible tests.
Real applications: Every test that uses services must mock them to avoid flaky network-dependent tests.
Common mistakes: Developers test external services instead of mocking. They hardcode test data instead of using createSpyObj. They don't configure spy return values properly.
it('should update after debounce', fakeAsync(() => {
component.search('hello');
tick(300); // Simulate 300ms debounce
fixture.detectChanges();
expect(component.results.length).toBe(3);
}));
Use flush() to drain all pending async operations instead of specifying exact timing. fakeAsync also supports flushMicrotasks() for Promise-based code.
Why it matters: Testing async code without waiting. Shows you can control time and test observable delays.
Real applications: Testing debounced search, animated transitions, setTimeout logic, Observable timers.
Common mistakes: Developers use real timeouts instead of fakeAsync. They forget to tick() exact milliseconds. They mix async/await with fakeAsync causing confusion.
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule]
});
httpMock = TestBed.inject(HttpTestingController);
});
it('should fetch users', () => {
service.getUsers().subscribe(users => expect(users.length).toBe(2));
const req = httpMock.expectOne('/api/users');
expect(req.request.method).toBe('GET');
req.flush([{ name: 'A' }, { name: 'B' }]);
});
Call httpMock.verify() in afterEach to ensure no unexpected requests were made. Use expectNone() to verify no request was sent.
Why it matters: HTTP testing verifies data layer communication. Shows you can mock and verify API interactions.
Real applications: All HTTP tests use HttpTestingController to avoid real network calls.
Common mistakes: Developers forget httpMock.verify() allowing stray requests. They hardcode URLs instead of using matching logic. They don't verify request method and headers.
// By CSS selector
const el = fixture.nativeElement.querySelector('.title');
// By directive
const debugEl = fixture.debugElement.query(By.css('app-child'));
// All matching
const items = fixture.debugElement.queryAll(By.css('li'));
expect(items.length).toBe(3);
Use By.directive() to find elements with a specific directive. The nativeElement gives you direct DOM access for simpler queries.
Why it matters: DOM querying in tests mimics user interaction. Shows you can verify rendered output.
Real applications: Every test verifies that components render the correct DOM based on state.
Common mistakes: Developers use wrong CSS selectors. They don't use By.directive() for component queries. They assume DOM elements exist without tests verifying them.
it('should show updated title', () => {
component.title = 'Updated Title';
// DOM still shows old value here
fixture.detectChanges(); // NOW the DOM updates
const el = fixture.nativeElement.querySelector('h1');
expect(el.textContent).toContain('Updated Title');
});
Call detectChanges() after setting inputs, triggering events, or completing async operations. The first call also triggers ngOnInit.Why it matters: Change detection control is essential to testing. Shows you understand Angular's rendering pipeline.
Real applications: Every test calls detectChanges() to synchronize component state with DOM.
Common mistakes: Developers forget to call detectChanges() after state changes. They call it unnecessarily on every line. They don't understand it triggers lifecycle hooks.
describe('TruncatePipe', () => {
const pipe = new TruncatePipe();
it('should truncate long text', () => {
expect(pipe.transform('Hello World', 5)).toBe('Hello...');
});
it('should not truncate short text', () => {
expect(pipe.transform('Hi', 5)).toBe('Hi');
});
});
Pipes with dependencies can be tested with TestBed using inject(). Pure pipes are especially easy to test since they have no side effects.
Why it matters: Pipe testing is straightforward and high-value. Shows you can test data transformation logic.
Real applications: Custom date, currency, and formatting pipes need tests to ensure correctness.
Common mistakes: Developers over-complicate pipe tests with TestBed when direct instantiation works. They don't test edge cases like null or undefined.
it('should allow access for logged-in user', () => {
authService.isLoggedIn.and.returnValue(true);
const result = TestBed.runInInjectionContext(() => authGuard(mockRoute, mockState));
expect(result).toBeTrue();
});
it('should redirect to login', () => {
authService.isLoggedIn.and.returnValue(false);
const result = TestBed.runInInjectionContext(() => authGuard(mockRoute, mockState));
expect(result).toEqual(router.parseUrl('/login'));
});
For async guards, use fakeAsync and subscribe to the returned Observable. Test both success and failure paths for complete coverage.
Why it matters: Guard testing ensures route security. Shows you can test authentication and authorization logic.
Real applications: Auth guards, permission checks, lazy loading guards all need tests.
Common mistakes: Developers don't test redirect scenarios. They forget to mock the router. They don't test async guards properly.
// Unit test: mock everything
TestBed.configureTestingModule({
declarations: [ParentComponent],
schemas: [NO_ERRORS_SCHEMA] // ignore child components
});
// Integration test: include real child components
TestBed.configureTestingModule({
declarations: [ParentComponent, ChildComponent],
providers: [RealService]
});
Use NO_ERRORS_SCHEMA in unit tests to ignore unknown child components. Integration tests provide more confidence but are slower to run.
Why it matters: Understanding test scopes improves efficiency and coverage. Shows you know when each approach is appropriate.
Real applications: Unit tests run first for speed, integration tests catch interaction bugs.
Common mistakes: Developers mix unit and integration tests. They write all integration tests and miss edge cases. They write isolated unit tests that don't reflect real usage.
describe('CalculatorService', () => {
let service: CalculatorService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(CalculatorService);
});
it('should add two numbers', () => {
expect(service.add(2, 3)).toBe(5);
});
});
Test the public API of the service including methods, return values, and side effects. Use TestBed.inject() to get the service instance.
Why it matters: Service testing verifies business logic. Shows you can test data and logic layers independently.
Real applications: Services handle data fetching, calculations, state management. All need thorough testing.
Common mistakes: Developers test implementation instead of public interface. They don't mock dependencies properly. They test multiple services in one test.
@Component({ template: '<p appHighlight>Test</p>' })
class TestHostComponent {}
describe('HighlightDirective', () => {
it('should highlight on hover', () => {
TestBed.configureTestingModule({
declarations: [HighlightDirective, TestHostComponent]
});
const fixture = TestBed.createComponent(TestHostComponent);
const el = fixture.debugElement.query(By.directive(HighlightDirective));
el.triggerEventHandler('mouseenter', null);
expect(el.nativeElement.style.backgroundColor).toBe('yellow');
});
});
Use By.directive() to find the element with the directive. Use triggerEventHandler to simulate DOM events in tests.
Why it matters: Directive testing verifies behavior wrappers. Shows you can test reusable behavior directives.
Real applications: Custom directives for validation, tracking, styling all need tests.
Common mistakes: Developers test directives in isolation without templates. They don't use test host components. They forget to trigger event handlers.
it('should display the title', () => {
component.title = 'Hello';
// DOM still shows old value
fixture.detectChanges(); // NOW the DOM updates
const el = fixture.nativeElement.querySelector('h1');
expect(el.textContent).toContain('Hello');
});
The first call to detectChanges() triggers ngOnInit. Subsequent calls update the DOM with the latest property values.it('should load data', fakeAsync(() => {
component.loadData();
tick(1000); // simulate 1 second passing
fixture.detectChanges();
expect(component.data.length).toBeGreaterThan(0);
}));
Use flush() to drain all pending async operations instead of specifying exact timing. For HTTP calls, use HttpClientTestingModule to mock and flush requests synchronously.
Why it matters: Async testing controls time and futures. Shows you can test Observable and Promise-based code.
Real applications: HTTP requests, timers, animations all need async testing strategies.
Common mistakes: Developers use real setTimeout instead of fakeAsync. They forget to mock HTTP calls. They mix async strategies confusingly.
beforeEach(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule],
declarations: [MyComponent]
});
router = TestBed.inject(Router);
location = TestBed.inject(Location);
});
it('should navigate to login', () => {
const navigateSpy = spyOn(router, 'navigate');
component.logout();
expect(navigateSpy).toHaveBeenCalledWith(['/login']);
});
For testing routed components, provide routes with RouterTestingModule.withRoutes(). Use spyOn to verify navigation calls.
Why it matters: Router testing confirms navigation logic. Shows you can test client-side routing effectively.
Real applications: Testing logout redirects to login, permission checks preventing access, successful form submissions redirecting to success page.
Common mistakes: Developers don't use RouterTestingModule causing real navigation. They don't spy on navigate expecting it to work. They forget Location service for URL verification.