Angular
06 / 10

Testing with Jasmine & Karma

Testing Angular Applications

Angular uses Jasmine as the testing framework and Karma as the test runner. Testing is built into the Angular CLI from the start.

Unit Testing Components

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CounterComponent } from './counter.component';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';

describe('CounterComponent', () => {
  let component: CounterComponent;
  let fixture: ComponentFixture<CounterComponent>;
  let compiled: HTMLElement;
  
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [CounterComponent]
    }).compileComponents();
    
    fixture = TestBed.createComponent(CounterComponent);
    component = fixture.componentInstance;
    compiled = fixture.nativeElement;
    fixture.detectChanges();
  });
  
  it('should create', () => {
    expect(component).toBeTruthy();
  });
  
  it('should display initial count', () => {
    const countElement = compiled.querySelector('.count');
    expect(countElement?.textContent).toContain('0');
  });
  
  it('should increment count when button clicked', () => {
    const button = compiled.querySelector('.increment-btn') as HTMLButtonElement;
    button.click();
    fixture.detectChanges();
    
    expect(component.count).toBe(1);
    expect(compiled.querySelector('.count')?.textContent).toContain('1');
  });
  
  it('should decrement count', () => {
    component.count = 5;
    fixture.detectChanges();
    
    component.decrement();
    fixture.detectChanges();
    
    expect(component.count).toBe(4);
  });
});

Testing Services

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

describe('UserService', () => {
  let service: UserService;
  let httpMock: HttpTestingController;
  
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [UserService]
    });
    
    service = TestBed.inject(UserService);
    httpMock = TestBed.inject(HttpTestingController);
  });
  
  afterEach(() => {
    httpMock.verify();
  });
  
  it('should fetch users', () => {
    const mockUsers = [
      { id: 1, name: 'John' },
      { id: 2, name: 'Jane' }
    ];
    
    service.getUsers().subscribe(users => {
      expect(users.length).toBe(2);
      expect(users).toEqual(mockUsers);
    });
    
    const req = httpMock.expectOne('/api/users');
    expect(req.request.method).toBe('GET');
    req.flush(mockUsers);
  });
  
  it('should handle error', () => {
    service.getUsers().subscribe(
      () => fail('should have failed'),
      (error) => {
        expect(error.status).toBe(404);
      }
    );
    
    const req = httpMock.expectOne('/api/users');
    req.flush('Not found', { status: 404, statusText: 'Not Found' });
  });
});

Testing with Dependencies

describe('UserListComponent', () => {
  let component: UserListComponent;
  let fixture: ComponentFixture<UserListComponent>;
  let userService: jasmine.SpyObj<UserService>;
  
  beforeEach(async () => {
    // Create spy
    const userServiceSpy = jasmine.createSpyObj('UserService', ['getUsers']);
    
    await TestBed.configureTestingModule({
      declarations: [UserListComponent],
      providers: [
        { provide: UserService, useValue: userServiceSpy }
      ]
    }).compileComponents();
    
    fixture = TestBed.createComponent(UserListComponent);
    component = fixture.componentInstance;
    userService = TestBed.inject(UserService) as jasmine.SpyObj<UserService>;
  });
  
  it('should load users on init', () => {
    const mockUsers = [{ id: 1, name: 'John' }];
    userService.getUsers.and.returnValue(of(mockUsers));
    
    component.ngOnInit();
    
    expect(userService.getUsers).toHaveBeenCalled();
    expect(component.users).toEqual(mockUsers);
  });
});

Testing Directives

@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective {
  @Input() appHighlight = '';
  
  constructor(private el: ElementRef) { }
  
  @HostListener('mouseenter') onMouseEnter() {
    this.highlight(this.appHighlight || 'yellow');
  }
  
  @HostListener('mouseleave') onMouseLeave() {
    this.highlight('');
  }
  
  private highlight(color: string) {
    this.el.nativeElement.style.backgroundColor = color;
  }
}

describe('HighlightDirective', () => {
  let fixture: ComponentFixture<TestComponent>;
  let des: DebugElement[];
  
  @Component({
    template: `
      <p appHighlight>Default</p>
      <p appHighlight="red">Red</p>
    `
  })
  class TestComponent { }
  
  beforeEach(() => {
    fixture = TestBed.configureTestingModule({
      declarations: [HighlightDirective, TestComponent]
    }).createComponent(TestComponent);
    
    fixture.detectChanges();
    des = fixture.debugElement.queryAll(By.directive(HighlightDirective));
  });
  
  it('should have 2 highlighted elements', () => {
    expect(des.length).toBe(2);
  });
  
  it('should color first element yellow on mouseenter', () => {
    des[0].triggerEventHandler('mouseenter', null);
    expect(des[0].nativeElement.style.backgroundColor).toBe('yellow');
  });
});

Testing Async Operations

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

describe('Async Tests', () => {
  // fakeAsync - control time
  it('should work with fakeAsync', fakeAsync(() => {
    let value = false;
    
    setTimeout(() => {
      value = true;
    }, 1000);
    
    expect(value).toBe(false);
    tick(1000); // Advance time by 1000ms
    expect(value).toBe(true);
  }));
  
  // waitForAsync - wait for promises
  it('should work with async', waitForAsync(() => {
    const promise = new Promise(resolve => {
      setTimeout(() => resolve('done'), 1000);
    });
    
    promise.then(value => {
      expect(value).toBe('done');
    });
  }));
  
  // Test observables
  it('should handle observables', fakeAsync(() => {
    let result: string;
    
    of('test').pipe(delay(1000)).subscribe(value => {
      result = value;
    });
    
    tick(1000);
    expect(result!).toBe('test');
  }));
});

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free