AngularJSTesting (Jasmine & Karma)

Testing Angular Apps with Jasmine and Karma

Testing is a first-class citizen in Angular. The framework ships with Jasmine (the test framework), Karma (the test runner), and TestBed (Angular's testing utility) configured out of the box.

This guide covers unit testing services, pipes, and other non-visual logic. For component-specific testing see the Component Testing page.

Testing Tools Overview

Tool

Role

Jasmine

Test framework — describe, it, expect, spyOn

Karma

Test runner — runs tests in a real browser

TestBed

Angular testing module — configures DI, components, etc.

Angular CDK testing

Component harnesses for stable testing

Jest

Popular alternative to Karma (faster, no browser)

Running Tests

Bash
# Run all tests
ng test

# Run with code coverage
ng test --code-coverage

# Run specific file
ng test --include='**/my-service.spec.ts'

# Run in headless mode (CI)
ng test --watch=false --browsers=ChromeHeadless
Chrome Headless 120.0.0 (Linux): Executed 42 of 42 SUCCESS (1.234 secs / 1.001 secs)
TOTAL: 42 SUCCESS

Coverage summary
Statements   : 94.2% ( 162/172 )
Branches     : 88.9% ( 48/54 )
Functions    : 96.7% ( 29/30 )
Lines        : 94.1% ( 160/170 )
Jasmine Fundamentals

TS
// Basic Jasmine structure
describe('Calculator', () => {
  let calc: Calculator;

  beforeEach(() => {
    calc = new Calculator();
  });

  afterEach(() => {
    // cleanup after each test
  });

  it('should add two numbers', () => {
    expect(calc.add(2, 3)).toBe(5);
  });

  it('should subtract numbers', () => {
    expect(calc.subtract(10, 4)).toBe(6);
  });

  it('should throw on division by zero', () => {
    expect(() => calc.divide(10, 0)).toThrowError('Cannot divide by zero');
  });
});
Common Jasmine Matchers

TS
// Equality
expect(value).toBe(5);              // Strict equality (===)
expect(value).toEqual({ a: 1 });    // Deep equality

// Truthiness
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
expect(value).toBeDefined();

// Numbers
expect(value).toBeGreaterThan(3);
expect(value).toBeLessThanOrEqual(10);
expect(value).toBeCloseTo(3.14, 2); // 2 decimal places

// Strings / Arrays
expect('hello world').toContain('world');
expect([1, 2, 3]).toContain(2);
expect(arr).toHaveSize(3);

// Errors
expect(() => fn()).toThrow();
expect(() => fn()).toThrowError('message');

// Negation
expect(value).not.toBe(null);
Testing Services

TS
// calculator.service.ts
import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class CalculatorService {
  add(a: number, b: number): number {
    return a + b;
  }

  multiply(a: number, b: number): number {
    return a * b;
  }
}

TS
// 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 positive numbers', () => {
    expect(service.add(3, 4)).toBe(7);
  });

  it('should handle negative numbers', () => {
    expect(service.add(-1, -2)).toBe(-3);
  });

  it('should multiply numbers', () => {
    expect(service.multiply(4, 5)).toBe(20);
  });
});
Testing Services with Dependencies

TS
// 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;
  email: string;
}

@Injectable({ providedIn: 'root' })
export class UserService {
  private http = inject(HttpClient);

  getUsers(): Observable<User[]> {
    return this.http.get<User[]>('/api/users');
  }

  getUserById(id: number): Observable<User> {
    return this.http.get<User>(`/api/users/${id}`);
  }

  createUser(user: Partial<User>): Observable<User> {
    return this.http.post<User>('/api/users', user);
  }
}

TS
// user.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { UserService, User } from './user.service';

describe('UserService', () => {
  let service: UserService;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
    });
    service = TestBed.inject(UserService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    httpMock.verify(); // Ensures no unexpected requests were made
  });

  it('should fetch all users', () => {
    const mockUsers: User[] = [
      { id: 1, name: 'Alice', email: 'alice@example.com' },
      { id: 2, name: 'Bob', email: 'bob@example.com' },
    ];

    service.getUsers().subscribe((users) => {
      expect(users).toHaveSize(2);
      expect(users[0].name).toBe('Alice');
    });

    const req = httpMock.expectOne('/api/users');
    expect(req.request.method).toBe('GET');
    req.flush(mockUsers);
  });

  it('should fetch user by id', () => {
    const mockUser: User = { id: 1, name: 'Alice', email: 'alice@example.com' };

    service.getUserById(1).subscribe((user) => {
      expect(user.name).toBe('Alice');
    });

    const req = httpMock.expectOne('/api/users/1');
    req.flush(mockUser);
  });

  it('should handle HTTP errors', () => {
    service.getUsers().subscribe({
      error: (err) => {
        expect(err.status).toBe(404);
      },
    });

    const req = httpMock.expectOne('/api/users');
    req.flush('Not found', { status: 404, statusText: 'Not Found' });
  });
});
Spies and Mocks

Spies let you observe, stub, or replace function behavior without running real code:

TS
describe('spyOn examples', () => {
  it('should track calls with spyOn', () => {
    const service = new CalculatorService();
    const spy = spyOn(service, 'add').and.returnValue(99);

    const result = service.add(2, 3);

    expect(spy).toHaveBeenCalled();
    expect(spy).toHaveBeenCalledWith(2, 3);
    expect(result).toBe(99);
  });

  it('should use jasmine.createSpy for standalone spies', () => {
    const callbackSpy = jasmine.createSpy('callback');
    someFunction(callbackSpy);
    expect(callbackSpy).toHaveBeenCalledTimes(1);
  });

  it('should create a spy object', () => {
    const mockService = jasmine.createSpyObj('UserService', ['getUsers', 'createUser']);
    mockService.getUsers.and.returnValue(of([]));
    expect(mockService.getUsers).toBeDefined();
  });

  it('should spy on and call through', () => {
    const service = new LoggingService();
    spyOn(service, 'log').and.callThrough(); // calls original but also tracks
    service.log('hello');
    expect(service.log).toHaveBeenCalledWith('hello');
  });
});
Testing Pipes

TS
// truncate.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({ name: 'truncate', standalone: true })
export class TruncatePipe implements PipeTransform {
  transform(value: string, limit = 50, ellipsis = '...'): string {
    if (!value) return '';
    if (value.length <= limit) return value;
    return value.substring(0, limit) + ellipsis;
  }
}

TS
// truncate.pipe.spec.ts
import { TruncatePipe } from './truncate.pipe';

describe('TruncatePipe', () => {
  let pipe: TruncatePipe;

  beforeEach(() => {
    pipe = new TruncatePipe(); // No need for TestBed — it's a pure class
  });

  it('should create', () => {
    expect(pipe).toBeTruthy();
  });

  it('should not truncate short strings', () => {
    expect(pipe.transform('Hello')).toBe('Hello');
  });

  it('should truncate long strings with default ellipsis', () => {
    const input = 'A'.repeat(60);
    const result = pipe.transform(input);
    expect(result.length).toBe(53); // 50 chars + '...'
    expect(result.endsWith('...')).toBe(true);
  });

  it('should use custom limit', () => {
    expect(pipe.transform('Hello World', 5)).toBe('Hello...');
  });

  it('should use custom ellipsis', () => {
    expect(pipe.transform('Hello World', 5, ' →')).toBe('Hello →');
  });

  it('should handle empty string', () => {
    expect(pipe.transform('')).toBe('');
  });

  it('should handle null/undefined', () => {
    expect(pipe.transform(null as any)).toBe('');
  });
});
Testing Observables

TS
import { of, throwError } from 'rxjs';
import { fakeAsync, tick } from '@angular/core/testing';

describe('Observable testing', () => {
  it('should handle synchronous observable with done', (done) => {
    const obs$ = of(42);
    obs$.subscribe({
      next: (val) => {
        expect(val).toBe(42);
        done();
      },
    });
  });

  it('should test async observable with fakeAsync', fakeAsync(() => {
    let result: number | undefined;
    const obs$ = timer(1000).pipe(mapTo(99));

    obs$.subscribe((val) => (result = val));

    expect(result).toBeUndefined(); // Not resolved yet

    tick(1000); // Advance timer by 1 second

    expect(result).toBe(99);
  }));

  it('should test error handling', () => {
    const errorService = jasmine.createSpyObj('DataService', ['getData']);
    errorService.getData.and.returnValue(throwError(() => new Error('Network error')));

    let errorMessage = '';
    errorService.getData().subscribe({
      error: (err: Error) => (errorMessage = err.message),
    });

    expect(errorMessage).toBe('Network error');
  });
});
Code Coverage

Bash
ng test --code-coverage --watch=false

Coverage reports are generated in coverage/. Open coverage/my-app/index.html in a browser.

Configure coverage thresholds in karma.conf.js:

JS
// karma.conf.js
module.exports = function(config) {
  config.set({
    coverageReporter: {
      dir: require('path').join(__dirname, './coverage/my-app'),
      subdir: '.',
      reporters: [
        { type: 'html' },
        { type: 'text-summary' },
        { type: 'lcovonly' }
      ],
      check: {
        global: {
          statements: 80,
          branches: 80,
          functions: 80,
          lines: 80,
        },
      },
    },
  });
};
Migrating to Jest

Jest is faster than Karma because it doesn't require a browser. Angular 16+ supports Jest:

Bash
ng add @angular-builders/jest

JSON
// jest.config.ts
export default {
  preset: 'jest-preset-angular',
  setupFilesAfterFramework: ['<rootDir>/setup-jest.ts'],
  testEnvironment: 'jsdom',
  moduleNameMapper: {
    '^@app/(.*)$': '<rootDir>/src/app/$1',
  },
};
Tip
Jest runs in Node.js using jsdom to simulate the browser — no Karma, no Chrome. Tests run significantly faster especially in CI environments.