DEV Community

Cover image for Angular Testing with Jasmine and Karma: Complete Guide | Unit Testing, Component Testing & E2E
Md. Maruf Rahman
Md. Maruf Rahman

Posted on • Originally published at marufrahman.live

Angular Testing with Jasmine and Karma: Complete Guide | Unit Testing, Component Testing & E2E

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.

📖 Want the complete guide with more examples and advanced patterns? Check out the full article on my blog for an in-depth tutorial with additional code examples, troubleshooting tips, and real-world use cases.

What is Angular Testing?

Angular Testing provides:

  • Jasmine - Behavior-driven testing framework
  • Karma - Test runner for running tests in browsers
  • TestBed - Testing utilities for configuring test modules
  • Component Testing - Test components in isolation
  • Service Testing - Test business logic and HTTP calls
  • Pipe & Directive Testing - Test custom transformations
  • Async Testing - Test asynchronous operations
  • Mocking - Mock dependencies with spies

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';
import { ReactiveFormsModule } from '@angular/forms';
import { HttpClientTestingModule } from '@angular/common/http/testing';

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');
  });
});
Enter fullscreen mode Exit fullscreen mode

Key Points:

  • Use TestBed.configureTestingModule() to configure test module
  • Mock dependencies with jasmine.createSpyObj()
  • Use fixture.detectChanges() to trigger change detection
  • Query DOM with fixture.nativeElement or fixture.debugElement

Testing Component Inputs and Outputs

describe('BusinessCardComponent', () => {
  let component: BusinessCardComponent;
  let fixture: ComponentFixture<BusinessCardComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [BusinessCardComponent]
    }).compileComponents();

    fixture = TestBed.createComponent(BusinessCardComponent);
    component = fixture.componentInstance;
  });

  it('should display business name', () => {
    component.business = { id: 1, name: 'Test Business' };
    fixture.detectChanges();

    const compiled = fixture.nativeElement;
    expect(compiled.textContent).toContain('Test Business');
  });

  it('should emit event on click', () => {
    spyOn(component.businessSelected, 'emit');
    component.business = { id: 1, name: 'Test Business' };
    fixture.detectChanges();

    const button = fixture.nativeElement.querySelector('button');
    button.click();

    expect(component.businessSelected.emit).toHaveBeenCalledWith(component.business);
  });
});
Enter fullscreen mode Exit fullscreen mode

Testing Component Forms

describe('BusinessFormComponent', () => {
  let component: BusinessFormComponent;
  let fixture: ComponentFixture<BusinessFormComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [BusinessFormComponent],
      imports: [ReactiveFormsModule]
    }).compileComponents();

    fixture = TestBed.createComponent(BusinessFormComponent);
    component = fixture.componentInstance;
  });

  it('should validate required fields', () => {
    const form = component.businessForm;
    expect(form.valid).toBeFalsy();

    form.patchValue({ name: 'Test Business' });
    expect(form.valid).toBeTruthy();
  });

  it('should submit form with valid data', () => {
    spyOn(component.formSubmit, 'emit');
    component.businessForm.patchValue({
      name: 'Test Business',
      email: 'test@example.com'
    });

    component.onSubmit();
    expect(component.formSubmit.emit).toHaveBeenCalled();
  });
});
Enter fullscreen mode Exit fullscreen mode

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(); // Verify no outstanding requests
  });

  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' });
  });

  it('should create business', () => {
    const newBusiness = { name: 'New Business' };
    const createdBusiness = { id: 1, ...newBusiness };

    service.SaveBusiness(newBusiness, 0).subscribe(business => {
      expect(business).toEqual(createdBusiness);
    });

    const req = httpMock.expectOne(`${environment.ApiUrl}business/0/details`);
    expect(req.request.method).toBe('POST');
    expect(req.request.body).toEqual(newBusiness);
    req.flush(createdBusiness);
  });
});
Enter fullscreen mode Exit fullscreen mode

Testing Services with Dependencies

describe('UserService', () => {
  let service: UserService;
  let httpMock: HttpTestingController;
  let loggerService: jasmine.SpyObj<LoggerService>;

  beforeEach(() => {
    const loggerSpy = jasmine.createSpyObj('LoggerService', ['log', 'error']);

    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [
        UserService,
        { provide: LoggerService, useValue: loggerSpy }
      ]
    });

    service = TestBed.inject(UserService);
    httpMock = TestBed.inject(HttpTestingController);
    loggerService = TestBed.inject(LoggerService) as jasmine.SpyObj<LoggerService>;
  });

  it('should log errors', () => {
    service.GetUser(1).subscribe(
      () => fail('should have failed'),
      error => {
        expect(loggerService.error).toHaveBeenCalled();
      }
    );

    const req = httpMock.expectOne('/api/users/1');
    req.flush('Error', { status: 500, statusText: 'Server Error' });
  });
});
Enter fullscreen mode Exit fullscreen mode

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');
  });

  it('should handle null values', () => {
    const result = pipe.transform(null, 10);
    expect(result).toBe('');
  });
});
Enter fullscreen mode Exit fullscreen mode

Testing Async Pipes

import { CurrencyPipe } from '@angular/common';
import { TestBed } from '@angular/core/testing';

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

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [CurrencyPipe]
    });
    pipe = TestBed.inject(CurrencyPipe);
  });

  it('should format currency', () => {
    const result = pipe.transform(1000, 'USD');
    expect(result).toContain('1,000');
  });
});
Enter fullscreen mode Exit fullscreen mode

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 create directive', () => {
    expect(element).toBeTruthy();
  });

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

  it('should remove highlight on mouseleave', () => {
    element.triggerEventHandler('mouseenter', null);
    fixture.detectChanges();

    element.triggerEventHandler('mouseleave', null);
    fixture.detectChanges();
    expect(element.nativeElement.style.backgroundColor).toBe('');
  });
});
Enter fullscreen mode Exit fullscreen mode

Async Testing

Test asynchronous operations:

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

describe('Async Operations', () => {
  it('should handle async operations with fakeAsync', fakeAsync(() => {
    let value = false;

    setTimeout(() => {
      value = true;
    }, 1000);

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

  it('should handle multiple timers', fakeAsync(() => {
    let value = 0;

    setTimeout(() => value++, 100);
    setTimeout(() => value++, 200);
    setTimeout(() => value++, 300);

    expect(value).toBe(0);
    tick(300);
    expect(value).toBe(3);
  }));

  it('should flush all timers', fakeAsync(() => {
    let value = false;

    setTimeout(() => {
      value = true;
    }, 1000);

    flush(); // Flush all pending timers
    expect(value).toBe(true);
  }));

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

  it('should test observables with fakeAsync', fakeAsync(() => {
    let result: any;

    service.getData().subscribe(data => {
      result = data;
    });

    tick();
    expect(result).toBeDefined();
  }));
});
Enter fullscreen mode Exit fullscreen mode

Testing RxJS Operators

import { of, throwError } from 'rxjs';
import { delay, catchError } from 'rxjs/operators';

describe('RxJS Testing', () => {
  it('should test delayed observables', fakeAsync(() => {
    let result: any;

    of('data').pipe(delay(1000)).subscribe(data => {
      result = data;
    });

    tick(1000);
    expect(result).toBe('data');
  }));

  it('should test error handling', (done) => {
    throwError(() => new Error('Test error'))
      .pipe(
        catchError(error => {
          expect(error.message).toBe('Test error');
          done();
          return of(null);
        })
      )
      .subscribe();
  });
});
Enter fullscreen mode Exit fullscreen mode

Testing Guards

Test route guards:

import { TestBed } from '@angular/core/testing';
import { Router } from '@angular/router';
import { AuthGuard } from './auth.guard';
import { AuthService } from './auth.service';

describe('AuthGuard', () => {
  let guard: AuthGuard;
  let authService: jasmine.SpyObj<AuthService>;
  let router: jasmine.SpyObj<Router>;

  beforeEach(() => {
    const authSpy = jasmine.createSpyObj('AuthService', ['isAuthenticated']);
    const routerSpy = jasmine.createSpyObj('Router', ['navigate']);

    TestBed.configureTestingModule({
      providers: [
        AuthGuard,
        { provide: AuthService, useValue: authSpy },
        { provide: Router, useValue: routerSpy }
      ]
    });

    guard = TestBed.inject(AuthGuard);
    authService = TestBed.inject(AuthService) as jasmine.SpyObj<AuthService>;
    router = TestBed.inject(Router) as jasmine.SpyObj<Router>;
  });

  it('should allow access when authenticated', () => {
    authService.isAuthenticated.and.returnValue(true);
    expect(guard.canActivate()).toBe(true);
  });

  it('should redirect when not authenticated', () => {
    authService.isAuthenticated.and.returnValue(false);
    expect(guard.canActivate()).toBe(false);
    expect(router.navigate).toHaveBeenCalledWith(['/login']);
  });
});
Enter fullscreen mode Exit fullscreen mode

Testing Interceptors

Test HTTP interceptors:

import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
import { AuthInterceptor } from './auth.interceptor';

describe('AuthInterceptor', () => {
  let http: HttpClient;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [
        {
          provide: HTTP_INTERCEPTORS,
          useClass: AuthInterceptor,
          multi: true
        }
      ]
    });

    http = TestBed.inject(HttpClient);
    httpMock = TestBed.inject(HttpTestingController);
  });

  it('should add authorization header', () => {
    http.get('/api/data').subscribe();

    const req = httpMock.expectOne('/api/data');
    expect(req.request.headers.has('Authorization')).toBeTruthy();
    expect(req.request.headers.get('Authorization')).toBe('Bearer token');
  });
});
Enter fullscreen mode Exit fullscreen mode

Testing Best Practices

  1. Write tests for each component, service, pipe, and directive
  2. Use TestBed - For component and service testing
  3. Mock dependencies - With Jasmine spies
  4. Use HttpClientTestingModule - For HTTP testing
  5. Test both success and error scenarios
  6. Use fakeAsync and tick - For async operations
  7. Query DOM elements - With debugElement
  8. Test component inputs, outputs, and events
  9. Maintain high test coverage - Aim for 80%+
  10. Keep tests isolated and independent
  11. Use descriptive test names - Clear what is being tested
  12. Clean up after each test - Use afterEach for cleanup
  13. Use beforeEach for setup - Common test configuration
  14. Test user interactions - Click, input, form submissions
  15. Test edge cases - Null values, empty arrays, errors

Common Testing Patterns

Test Setup Helper

export function createComponentFixture<T>(
  component: Type<T>,
  imports: any[] = [],
  providers: any[] = []
): ComponentFixture<T> {
  TestBed.configureTestingModule({
    declarations: [component],
    imports: [HttpClientTestingModule, ...imports],
    providers
  }).compileComponents();

  return TestBed.createComponent(component);
}
Enter fullscreen mode Exit fullscreen mode

Mock Factory

export function createMockService<T>(methods: string[]): jasmine.SpyObj<T> {
  return jasmine.createSpyObj<T>(methods);
}
Enter fullscreen mode Exit fullscreen mode

Test Data Builders

export class BusinessBuilder {
  private business: any = {
    id: 1,
    name: 'Test Business',
    isActive: true
  };

  withId(id: number): BusinessBuilder {
    this.business.id = id;
    return this;
  }

  withName(name: string): BusinessBuilder {
    this.business.name = name;
    return this;
  }

  inactive(): BusinessBuilder {
    this.business.isActive = false;
    return this;
  }

  build(): any {
    return { ...this.business };
  }
}

// Usage
const business = new BusinessBuilder()
  .withId(1)
  .withName('Test')
  .build();
Enter fullscreen mode Exit fullscreen mode

Karma Configuration

Configure Karma for your tests:

// 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'),
      require('@angular-devkit/build-angular/plugins/karma')
    ],
    client: {
      clearContext: false
    },
    coverageReporter: {
      dir: require('path').join(__dirname, './coverage'),
      subdir: '.',
      reporters: [
        { type: 'html' },
        { type: 'text-summary' }
      ]
    },
    reporters: ['progress', 'kjhtml'],
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch: true,
    browsers: ['Chrome'],
    singleRun: false,
    restartOnFileChange: true
  });
};
Enter fullscreen mode Exit fullscreen mode

Resources and Further Reading

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.

Key Takeaways:

  • Jasmine - Behavior-driven testing framework
  • Karma - Test runner for browser-based testing
  • TestBed - Configure test modules and create components
  • Component Testing - Test components in isolation
  • Service Testing - Test business logic and HTTP calls
  • Pipe & Directive Testing - Test custom transformations
  • Async Testing - Use fakeAsync and tick for async operations
  • Mocking - Use Jasmine spies for dependencies
  • Best Practices - High coverage, isolated tests, descriptive names

Whether you're building a simple dashboard or a complex enterprise application, comprehensive testing provides the confidence you need to refactor and deploy with peace of mind.


What's your experience with Angular Testing? Share your tips and tricks in the comments below! 🚀


💡 Looking for more details? This is a condensed version of my comprehensive guide. Read the full article on my blog for additional examples, advanced patterns, troubleshooting tips, and more in-depth explanations.

If you found this guide helpful, consider checking out my other articles on Angular development and frontend development best practices.

Top comments (0)