Back to Blog
Angular18 min read

Angular Testing with Jasmine and Karma: Complete Guide

Testing is essential for building reliable Angular applications. Learn how to write unit tests for components, services, pipes, directives, and implement E2E testing with Jasmine, Karma, and testing utilities in Angular.

I'll be honest—when I first started with Angular, I skipped testing. I thought it was too time-consuming, and my code worked fine without it. Then I had to refactor a large feature, and I broke three things I didn't even know existed. That's when I learned the value of good tests. They give you confidence to refactor, catch bugs before they reach production, and serve as documentation for how your code should work.

Angular provides excellent testing support out of the box with Jasmine (the testing framework) and Karma (the test runner). TestBed makes it easy to configure testing modules, mock dependencies, and test components in isolation. The testing utilities are powerful enough to test complex scenarios while being simple enough to write tests quickly.

In this guide, I'll walk you through testing Angular applications the way I do it in production. We'll cover component testing (testing component logic and templates), service testing (testing business logic and HTTP calls), pipe and directive testing, mocking dependencies with Jasmine spies, testing async operations, testing forms and user interactions, and testing best practices. I'll also share some patterns I've learned for writing maintainable tests and avoiding common testing pitfalls.

Component Testing

Test Angular components with TestBed:

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BusinessListComponent } from './business-list.component';
import { BusinessService } from 'src/services/business.service';
import { of } from 'rxjs';

describe('BusinessListComponent', () => {
  let component: BusinessListComponent;
  let fixture: ComponentFixture<BusinessListComponent>;
  let businessService: jasmine.SpyObj<BusinessService>;

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

    await TestBed.configureTestingModule({
      declarations: [BusinessListComponent],
      providers: [
        { provide: BusinessService, useValue: spy }
      ],
      imports: [ReactiveFormsModule, HttpClientTestingModule]
    }).compileComponents();

    fixture = TestBed.createComponent(BusinessListComponent);
    component = fixture.componentInstance;
    businessService = TestBed.inject(BusinessService) as jasmine.SpyObj<BusinessService>;
  });

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

  it('should load businesses on init', () => {
    const mockBusinesses = [{ id: 1, name: 'Business 1' }];
    businessService.GetBusinesses.and.returnValue(of({ data: mockBusinesses }));

    fixture.detectChanges();

    expect(businessService.GetBusinesses).toHaveBeenCalled();
    expect(component.businesses).toEqual(mockBusinesses);
  });

  it('should display businesses in template', () => {
    component.businesses = [{ id: 1, name: 'Business 1' }];
    fixture.detectChanges();

    const compiled = fixture.nativeElement;
    expect(compiled.querySelector('.business-name').textContent).toContain('Business 1');
  });
});

Service Testing

Test Angular services:

import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { BusinessService } from './business.service';
import { environment } from 'src/environments/environment';

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

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

  afterEach(() => {
    httpMock.verify();
  });

  it('should fetch businesses', () => {
    const mockBusinesses = [{ id: 1, name: 'Business 1' }];

    service.GetBusinesses({}).subscribe(businesses => {
      expect(businesses.data).toEqual(mockBusinesses);
    });

    const req = httpMock.expectOne(`${environment.ApiUrl}business/get`);
    expect(req.request.method).toBe('POST');
    req.flush({ data: mockBusinesses });
  });

  it('should handle errors', () => {
    service.GetBusinesses({}).subscribe(
      () => fail('should have failed'),
      error => expect(error.status).toBe(500)
    );

    const req = httpMock.expectOne(`${environment.ApiUrl}business/get`);
    req.flush('Error', { status: 500, statusText: 'Server Error' });
  });
});

Pipe Testing

Test custom pipes:

import { TruncatePipe } from './truncate.pipe';

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

  beforeEach(() => {
    pipe = new TruncatePipe();
  });

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

  it('should truncate long text', () => {
    const result = pipe.transform('This is a very long text', 10);
    expect(result).toBe('This is a ...');
  });

  it('should return original text if shorter than limit', () => {
    const result = pipe.transform('Short', 10);
    expect(result).toBe('Short');
  });
});

Directive Testing

Test custom directives:

import { Component, DebugElement } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { HighlightDirective } from './highlight.directive';

@Component({
  template: '<p appHighlight="yellow">Test</p>'
})
class TestComponent {}

describe('HighlightDirective', () => {
  let fixture: ComponentFixture<TestComponent>;
  let element: DebugElement;

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [TestComponent, HighlightDirective]
    });
    fixture = TestBed.createComponent(TestComponent);
    element = fixture.debugElement.query(By.directive(HighlightDirective));
  });

  it('should apply highlight on mouseenter', () => {
    element.triggerEventHandler('mouseenter', null);
    fixture.detectChanges();
    expect(element.nativeElement.style.backgroundColor).toBe('yellow');
  });
});

Async Testing

Test asynchronous operations:

import { fakeAsync, tick } from '@angular/core/testing';

it('should handle async operations', fakeAsync(() => {
  let value = false;
  
  setTimeout(() => {
    value = true;
  }, 1000);

  expect(value).toBe(false);
  tick(1000);
  expect(value).toBe(true);
}));

it('should test observables', (done) => {
  service.getData().subscribe(data => {
    expect(data).toBeDefined();
    done();
  });
});

Best Practices

  • Write tests for each component, service, pipe, and directive
  • Use TestBed for component and service testing
  • Mock dependencies with Jasmine spies
  • Use HttpClientTestingModule for HTTP testing
  • Test both success and error scenarios
  • Use fakeAsync and tick for async operations
  • Query DOM elements with debugElement
  • Test component inputs, outputs, and events
  • Maintain high test coverage (aim for 80%+)
  • Keep tests isolated and independent
  • Use descriptive test names
  • Clean up after each test

Conclusion

Testing is crucial for building reliable Angular applications. With Jasmine and Karma, you can write comprehensive tests for all parts of your application. Following these patterns ensures code quality and enables confident development.