Skip to content
Jasmine and Karma: Angular Testing with Spies and Test Runner

Jasmine and Karma: Angular Testing with Spies and Test Runner

DodaTech Updated Jun 20, 2026 7 min read

Jasmine is a behavior-driven development framework for testing JavaScript code with a clean, readable syntax, while Karma is a test runner that executes Jasmine tests in real browsers and reports results to the command line.

What You’ll Learn

  • Jasmine’s describe, it, expect syntax and built-in matchers
  • Creating spies to mock functions and track calls
  • Using Angular’s TestBed for component and service testing
  • Configuring Karma for headless browser testing in CI/CD
  • Common Jasmine and Karma testing patterns

Why Jasmine and Karma Matter

Jasmine is the default testing framework for Angular projects and is widely used in enterprise applications. Unlike Jest or Mocha, Jasmine requires no DOM — tests can run in Node.js or real browsers via Karma. Its integrated spies, clock mocking, and async support make it a complete solution without external dependencies. Understanding Jasmine is essential for any Angular developer.

Durga Antivirus Pro uses Jasmine for testing its Angular-based security dashboard, ensuring that threat visualization components render correctly and respond to user interactions.

Learning Path

    flowchart LR
  A[Testing Basics] --> B[Jasmine & Karma<br/>You are here]
  B --> C[Angular Testing]
  C --> D[Component Testing]
  D --> E[E2E Testing with Protractor]
  style B fill:#f90,color:#fff
  

Jasmine Basics

Jasmine tests are organized in a nested structure of describe blocks with it specifications:

// calculator.js
function add(a, b) { return a + b; }
function divide(a, b) {
  if (b === 0) throw new Error('Division by zero');
  return a / b;
}

// calculator.spec.js
describe('Calculator', () => {
  describe('add()', () => {
    it('should add two positive numbers', () => {
      expect(add(2, 3)).toBe(5);
    });

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

  describe('divide()', () => {
    it('should divide two numbers', () => {
      expect(divide(10, 2)).toBe(5);
    });

    it('should throw when dividing by zero', () => {
      expect(() => divide(5, 0)).toThrowError('Division by zero');
    });
  });
});

Expected output:

Calculator
  add()
    ✓ should add two positive numbers
    ✓ should handle negative numbers
  divide()
    ✓ should divide two numbers
    ✓ should throw when dividing by zero

Jasmine Matchers

Jasmine provides a rich set of matchers:

// Basic matchers
expect(true).toBe(true);
expect('hello').toEqual('hello');
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect([1, 2, 3]).toContain(2);

// String matchers
expect('Hello World').toMatch(/World/);
expect('Hello World').toContain('World');

// Number matchers
expect(10).toBeGreaterThan(5);
expect(10).toBeLessThanOrEqual(10);
expect(0.1 + 0.2).toBeCloseTo(0.3, 5);

// Object matchers
expect({ name: 'Alice' }).toEqual({ name: 'Alice' });
expect({ name: 'Alice', age: 30 }).toHaveSize(2);

// Custom matchers with asymmetric matching
expect({ name: 'Alice', age: 30 }).toEqual(
  jasmine.objectContaining({ name: 'Alice' })
);

Spies

Spies replace functions with test doubles that track calls, arguments, and return values:

describe('UserService', () => {
  let userService;
  let apiSpy;

  beforeEach(() => {
    // Create a spy on the API object
    apiSpy = jasmine.createSpyObj('API', ['get', 'post']);
    userService = new UserService(apiSpy);
  });

  it('should fetch user by ID', () => {
    apiSpy.get.and.returnValue(Promise.resolve({ id: 1, name: 'Alice' }));

    userService.getUser(1).then(user => {
      expect(user.name).toBe('Alice');
    });

    expect(apiSpy.get).toHaveBeenCalledWith('/users/1');
    expect(apiSpy.get).toHaveBeenCalledTimes(1);
  });

  it('should handle API errors', () => {
    apiSpy.get.and.returnValue(Promise.reject(new Error('Network error')));

    userService.getUser(1).catch(error => {
      expect(error.message).toContain('Network');
    });
  });

  it('should track call order', () => {
    apiSpy.get.and.returnValue(Promise.resolve([]));

    userService.getUser(1);
    userService.getUser(2);

    expect(apiSpy.get.calls.allArgs()).toEqual([
      ['/users/1'],
      ['/users/2'],
    ]);
  });

  it('should call through to the real function', () => {
    const realAdd = (a, b) => a + b;
    const spy = spyOn({ fn: realAdd }, 'fn').and.callThrough();

    expect(spy(2, 3)).toBe(5);
    expect(spy).toHaveBeenCalledWith(2, 3);
  });
});

Clock Mocking

Jasmine can mock time-based functions:

describe('Timer Service', () => {
  beforeEach(() => {
    jasmine.clock().install();
  });

  afterEach(() => {
    jasmine.clock().uninstall();
  });

  it('should fire callback after 5 seconds', () => {
    const callback = jasmine.createSpy('callback');
    setTimeout(callback, 5000);

    jasmine.clock().tick(5000);
    expect(callback).toHaveBeenCalled();
  });
});

Angular Testing with TestBed

Angular’s TestBed configures testing modules dynamically:

// user.component.ts
@Component({
  selector: 'app-user',
  template: `
    <div *ngIf="user$ | async as user; else loading">
      <h2>{{ user.name }}</h2>
      <p>{{ user.email }}</p>
    </div>
    <ng-template #loading>Loading...</ng-template>
  `
})
export class UserComponent implements OnInit {
  user$!: Observable<User>;
  @Input() userId!: number;

  constructor(private userService: UserService) {}

  ngOnInit() {
    this.user$ = this.userService.getUser(this.userId);
  }
}

// user.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { of } from 'rxjs';
import { UserComponent } from './user.component';
import { UserService } from './user.service';

describe('UserComponent', () => {
  let component: UserComponent;
  let fixture: ComponentFixture<UserComponent>;
  let userServiceSpy: jasmine.SpyObj<UserService>;

  beforeEach(async () => {
    const spy = jasmine.createSpyObj('UserService', ['getUser']);

    await TestBed.configureTestingModule({
      declarations: [UserComponent],
      providers: [
        { provide: UserService, useValue: spy }
      ]
    }).compileComponents();

    userServiceSpy = TestBed.inject(UserService) as jasmine.SpyObj<UserService>;
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(UserComponent);
    component = fixture.componentInstance;
  });

  it('should display user name', () => {
    const mockUser = { id: 1, name: 'Alice', email: 'alice@test.com' };
    userServiceSpy.getUser.and.returnValue(of(mockUser));

    component.userId = 1;
    fixture.detectChanges();

    const element = fixture.nativeElement;
    expect(element.querySelector('h2').textContent).toContain('Alice');
  });

  it('should show loading state initially', () => {
    component.userId = 1;
    fixture.detectChanges();

    const element = fixture.nativeElement;
    expect(element.textContent).toContain('Loading');
  });
});

Karma Configuration

Karma runs tests in real browsers and reports results:

// karma.conf.js
module.exports = function(config) {
  config.set({
    basePath: '',
    frameworks: ['jasmine', '@angular-devkit/build-angular'],
    plugins: [
      require('karma-jasmine'),
      require('karma-chrome-launcher'),
      require('karma-jasmine-html-reporter'),
      require('karma-coverage'),
    ],
    browsers: ['ChromeHeadless'],
    reporters: ['progress', 'kjhtml', 'coverage'],
    coverageReporter: {
      dir: 'coverage/',
      reporters: [
        { type: 'html', subdir: 'html' },
        { type: 'lcovonly', subdir: '.' },
      ],
    },
    singleRun: true,
    restartOnFileChange: true,
  });
};

CI Mode

# Run once in headless Chrome
ng test --watch=false --browsers=ChromeHeadless

# With coverage
ng test --watch=false --code-coverage --browsers=ChromeHeadless

Common Jasmine and Karma Mistakes

1. Not Cleaning Up Spies

Spies created with spyOn persist across tests and can cause false failures.

Fix: Use beforeEach to create spies. Call jasmine.createSpyObj or use inject for fresh instances.

2. Testing Asynchronous Code Without done or async

Jasmine tests complete when the function returns. Async operations that haven’t completed yet won’t be tested.

Fix: Use done callback, return promises, or use async/await.

3. Using toBe for Objects

toBe checks reference equality. Two objects with identical properties are not the same reference.

Fix: Use toEqual for object comparison.

4. Over-Mocking

Mocking every dependency creates tests that pass but don’t verify real behavior.

Fix: Mock only at component boundaries. Use real objects for services when possible.

5. Not Using fixture.detectChanges()

Angular doesn’t automatically detect changes in tests. Forgetting detectChanges() means the view doesn’t update.

Fix: Call fixture.detectChanges() after setting component properties that affect the template.

6. Hardcoding Ports and URLs

Tests that connect to real servers on hardcoded ports break in different environments.

Fix: Use environment configuration and provide test URLs via Angular providers.

7. Slow Karma Startup

Full Angular compilation before each test run makes Karma slow.

Fix: Use ng test --watch to keep the browser running between code changes. Use incremental builds.

Practice Questions

1. What are the three main Jasmine functions for organizing tests?

describe (test suite), it (individual test), and expect (assertion).

2. What is a Jasmine spy and how do you create one?

A spy replaces a function with a test double that tracks calls. Create spyOn(object, 'method') or jasmine.createSpyObj('name', ['method1', 'method2']).

3. How do you mock the clock in Jasmine for testing timeouts?

Call jasmine.clock().install() in beforeEach, then jasmine.clock().tick(ms) to advance time. Uninstall in afterEach.

4. What is the Angular TestBed and why is it used?

TestBed is Angular’s primary API for configuring and creating testing modules. It compiles components, provides dependency injection, and sets up the testing environment.

5. How do you run Karma tests in headless mode for CI?

Configure browsers: ['ChromeHeadless'] in karma.conf.js and run ng test --watch=false --browsers=ChromeHeadless.

Challenge: Create an Angular component that displays a list of items with filtering. Write Jasmine tests for: component creation, item display, filter behavior, empty state, and error handling.

FAQ

What is the difference between Jasmine and Jest?
Jasmine is a standalone testing framework often used with Karma for browser testing. Jest is an all-in-one framework with built-in assertions, mocking, and coverage. Jest is faster for Node.js projects; Jasmine is standard for Angular.
Can I use Jasmine without Karma?
Yes, Jasmine can run in Node.js directly. Karma provides browser-based testing with multiple browser support.
How do I test HTTP calls in Angular?
Use HttpClientTestingModule and HttpTestingController to mock HTTP requests and verify they were made with correct parameters.
How do I test Angular routing?
Use RouterTestingModule with RouterTestingModule.withRoutes() to set up routes, then navigate using location.path() to verify the URL.
What is the difference between spyOn and createSpyObj?
spyOn wraps an existing object’s method. createSpyObj creates a new object with spy methods — useful for dependency injection mocks.

What’s Next

TutorialWhat You’ll Learn
Angular Testing Best PracticesAdvanced Angular component and service testing
Mocha and Chai Testing GuideAlternative JavaScript testing frameworks
End-to-End Testing with ProtractorE2E testing for Angular applications

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. Updated 2026-06-20.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro