Angular

Testing

15 Questions

TestBed is Angular's testing utility that configures a testing module for component tests. Use configureTestingModule to declare the component and provide mock services. Call compileComponents() to compile template and styles. CreateComponent returns a fixture for interacting with the component.
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.

Test @Input by setting the property directly on the component instance and calling detectChanges(). Test @Output by spying on the EventEmitter's emit method and triggering the action that causes the emission. Verify the DOM updates for inputs and verify emit was called with the correct value for outputs.
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.

Use jasmine.createSpyObj to create a mock service with spy methods. Configure the spy to return test data using and.returnValue(). Provide the mock in TestBed using the provide/useValue pattern. This isolates the component from its real dependencies for true unit testing.
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.

fakeAsync wraps a test to control async operations synchronously. tick() simulates the passage of time in milliseconds. This lets you test debounced inputs, setTimeout, setInterval, and Observable delays without waiting. All async operations in the fakeAsync zone are executed synchronously.
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.

Use HttpClientTestingModule and HttpTestingController to mock HTTP requests. The controller intercepts real HTTP calls and lets you verify the request and flush mock responses. Use expectOne() to assert a single request was made to a URL. Call flush() with mock data to simulate the server response.
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.

Query DOM elements using nativeElement.querySelector for CSS selectors or debugElement.query with By.css or By.directive predicates. Use queryAll for multiple matches. The debugElement provides Angular-specific features like triggerEventHandler and injector access.
// 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.

fixture.detectChanges() triggers change detection for the test component. In tests, Angular does not run change detection automatically, so you must call it after modifying component properties to update the DOM. Without calling it, the template will not reflect your changes and assertions will fail.
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.

Pipes are the simplest Angular constructs to test. Create a new instance directly without TestBed and call the transform method with test inputs. Since pipes are pure functions, they do not need dependency injection. Test both normal cases and edge cases like empty strings, null values, and boundary conditions.
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.

Test guards by using TestBed.runInInjectionContext to execute the guard function with mock route and state. Mock the auth service to control the guard's behavior. Verify that the guard returns true for authorized users and UrlTree for unauthorized redirects.
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 tests test a single component or service in isolation with all dependencies mocked. Integration tests test how multiple components work together, often rendering child components and using real services. Unit tests are fast and focused while integration tests catch interaction bugs.
// 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.

Services are tested by creating them with TestBed and calling their methods. For services with dependencies like HttpClient, mock the dependencies using jasmine.createSpyObj or provide HttpClientTestingModule. Services without dependencies can be tested without TestBed by creating a new instance directly.
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.

Directives are tested by creating a test host component that uses the directive in its template. Use TestBed to compile both the host component and the directive. Interact with the host element and verify the directive applied the expected behavior like changing styles or adding classes. This tests the directive in a realistic context.
@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.

fixture.detectChanges() manually triggers change detection for the test component. In tests, Angular does not run change detection automatically like it does in a running application. You must call it after setting properties, triggering events, or when async operations complete. Without calling it, the template will not reflect your changes.
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.

For async operations, use fakeAsync with tick(), waitForAsync, or async/await. fakeAsync lets you control time by simulating the passage of milliseconds. waitForAsync waits for all async operations to complete inside the zone. Use fixture.whenStable() to wait for pending async operations.
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.

Use RouterTestingModule to set up a mock router environment. Spy on Router.navigate to verify that components trigger navigation correctly. Use the Location service to verify the current URL after navigation. This lets you test navigation without actually loading route components.
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.