Skip to main content

Overview

ScreenPulse uses Jasmine as the testing framework and Karma as the test runner. This guide covers testing patterns, configuration, and best practices for both components and services.

Testing Stack

Jasmine

Behavior-driven testing framework

Karma

Test runner for Angular

Angular Testing Utilities

TestBed, fixtures, and mocks

Running Tests

NPM Scripts

From package.json, tests can be run with:
npm test
This executes the Angular test command:
package.json
{
  "scripts": {
    "test": "ng test",
    "watch": "ng build --watch --configuration development"
  }
}
Use npm test to run tests in watch mode, which automatically re-runs tests when files change.

Test File Structure

Naming Convention

All test files follow the .spec.ts naming pattern:
src/app/
├── core/
│   ├── services/
│   │   ├── auth.service.ts
│   │   └── auth.service.spec.ts
│   └── guards/
│       ├── auth.guard.ts
│       └── auth.guard.spec.ts
├── shared/
│   ├── components/
│   │   └── loading-spinner/
│   │       ├── loading-spinner.component.ts
│   │       └── loading-spinner.component.spec.ts
│   └── services/
│       └── favorites/
│           ├── favorites.service.ts
│           └── favorites.service.spec.ts
└── pages/
    └── auth/
        └── components/
            └── auth-form/
                ├── auth-form.component.ts
                └── auth-form.component.spec.ts

Service Testing

Basic Service Test

Here’s the basic structure for testing an Angular service:
import { TestBed } from '@angular/core/testing';
import { AuthService } from './auth.service';

describe('AuthService', () => {
  let service: AuthService;

  beforeEach(() => {
    TestBed.configureTestingModule({});
    service = TestBed.inject(AuthService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });
});
1

Import TestBed

import { TestBed } from '@angular/core/testing';
TestBed is the primary Angular testing utility for configuring and creating an Angular testing module.
2

Configure TestBed

beforeEach(() => {
  TestBed.configureTestingModule({});
  service = TestBed.inject(AuthService);
});
Set up the testing module and inject the service before each test.
3

Write test specs

it('should be created', () => {
  expect(service).toBeTruthy();
});
Use Jasmine’s it() and expect() to write test assertions.

Testing HTTP Services

Services that make HTTP calls require HttpClientModule or HttpClientTestingModule:
import { TestBed } from '@angular/core/testing';
import { FavoritesService } from './favorites.service';
import { HttpClientModule } from '@angular/common/http';

describe('FavoritesService', () => {
  let service: FavoritesService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports:[HttpClientModule]
    });
    service = TestBed.inject(FavoritesService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });
});
The HttpClientModule is imported in the TestBed configuration to provide HTTP dependencies for the service.

Advanced HTTP Testing with HttpClientTestingModule

For more controlled testing, use HttpClientTestingModule:
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { FavoritesService } from './favorites.service';
import { MediaItem } from '../../models/movie.model';

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

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

  afterEach(() => {
    httpMock.verify(); // Ensure no outstanding requests
  });

  it('should fetch favorites', () => {
    const mockResponse = {
      favorites: [],
      total: 0,
      page: 1
    };

    service.getFavorites(1, 10).subscribe((response) => {
      expect(response).toEqual(mockResponse);
    });

    const req = httpMock.expectOne((request) => 
      request.url.includes('favorites')
    );
    expect(req.request.method).toBe('GET');
    req.flush(mockResponse);
  });

  it('should delete a favorite', () => {
    const mediaId = '123';
    const mockResponse = { message: 'Deleted successfully' };

    service.deleteMediaItem(mediaId).subscribe((response) => {
      expect(response.message).toBe('Deleted successfully');
    });

    const req = httpMock.expectOne((request) => 
      request.url.includes(`favorites/${mediaId}`)
    );
    expect(req.request.method).toBe('DELETE');
    req.flush(mockResponse);
  });
});

Component Testing

Basic Component Test

Component tests require additional setup for templates and dependencies:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AuthFormComponent } from './auth-form.component';
import { HttpClientModule } from '@angular/common/http';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { ReactiveFormsModule } from '@angular/forms';
import { MatInputModule } from '@angular/material/input';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

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

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [
        HttpClientModule, 
        MatFormFieldModule,
        MatIconModule,
        ReactiveFormsModule,
        MatInputModule,
        BrowserAnimationsModule
      ],
      declarations: [AuthFormComponent]
    });
    fixture = TestBed.createComponent(AuthFormComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

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

Component Testing Anatomy

Purpose: Wrapper around component instance for testingUsage:
fixture = TestBed.createComponent(AuthFormComponent);
component = fixture.componentInstance;
Methods:
  • detectChanges(): Trigger change detection
  • debugElement: Access DOM elements
  • nativeElement: Access native DOM
Purpose: Configure testing module with dependenciesProperties:
  • imports: Import required modules (FormsModule, Material, etc.)
  • declarations: Declare components to test
  • providers: Provide services and mocks
TestBed.configureTestingModule({
  imports: [HttpClientModule, ReactiveFormsModule],
  declarations: [AuthFormComponent],
  providers: [AuthService]
});
Purpose: Trigger Angular change detectionWhen to use:
  • After component creation
  • After changing component properties
  • Before querying DOM elements
component.username = 'test';
fixture.detectChanges(); // Update view

Testing Component with Material Dependencies

Components using Angular Material require specific module imports:
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatIconModule } from '@angular/material/icon';
import { ReactiveFormsModule } from '@angular/forms';

beforeEach(() => {
  TestBed.configureTestingModule({
    imports: [
      BrowserAnimationsModule,  // Required for Material animations
      MatFormFieldModule,       // Material form fields
      MatInputModule,           // Material inputs
      MatIconModule,            // Material icons
      ReactiveFormsModule       // Reactive forms
    ],
    declarations: [AuthFormComponent]
  });
});
Always import BrowserAnimationsModule when testing components with Angular Material to prevent animation-related errors.

Testing AuthService

Here’s a complete example testing the AuthService with session storage:
auth.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { AuthService } from './auth.service';
import { AuthUser } from 'src/app/shared/models/auth.model';

describe('AuthService', () => {
  let service: AuthService;

  beforeEach(() => {
    TestBed.configureTestingModule({});
    service = TestBed.inject(AuthService);
    sessionStorage.clear(); // Clear before each test
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });

  it('should set auth token', () => {
    service.setAuthToken('test-token');
    expect(service.getAuthToken()).toBe('test-token');
    expect(sessionStorage.getItem('authToken')).toBe('test-token');
  });

  it('should set user session', () => {
    const user: AuthUser = {
      email: '[email protected]',
      name: 'Test User'
    };
    service.setUserSession(user, 'token123');
    
    expect(service.getUserMail()).toBe('[email protected]');
    expect(service.getUserName()).toBe('Test User');
    expect(service.getAuthToken()).toBe('token123');
  });

  it('should observe login state', (done) => {
    service.isLoggedInObservable().subscribe((isLoggedIn) => {
      if (isLoggedIn) {
        expect(isLoggedIn).toBe(true);
        done();
      }
    });
    
    service.setAuthToken('test-token');
  });

  it('should clear session on logout', () => {
    service.setAuthToken('test-token');
    service.setUserMail('[email protected]');
    
    service.logOut();
    
    expect(service.getAuthToken()).toBeNull();
    expect(service.getUserMail()).toBeNull();
  });
});

Test Patterns

AAA Pattern (Arrange, Act, Assert)

it('should add item to favorites', () => {
  // Arrange
  const mockItem: MediaItem = {
    title: 'Test Movie',
    imdbID: '123'
  };
  
  // Act
  service.addToFavorites(mockItem).subscribe((response) => {
    // Assert
    expect(response.title).toBe('Test Movie');
  });
  
  const req = httpMock.expectOne((request) => 
    request.url.includes('favorites')
  );
  req.flush(mockItem);
});

Testing Observables

it('should emit user email changes', (done) => {
  service.getUserMailObservable().subscribe((email) => {
    expect(email).toBe('[email protected]');
    done(); // Signal async test completion
  });
  
  service.setUserMail('[email protected]');
});

Spying on Methods

it('should call authService.getAuthToken', () => {
  const authService = TestBed.inject(AuthService);
  spyOn(authService, 'getAuthToken').and.returnValue('mock-token');
  
  service.getFavorites(1, 10);
  
  expect(authService.getAuthToken).toHaveBeenCalled();
});

Best Practices

1. Isolate Tests

beforeEach(() => {
  sessionStorage.clear();
  localStorage.clear();
});

afterEach(() => {
  httpMock.verify(); // No outstanding HTTP requests
});

2. Test One Thing Per Spec

// ✅ Good - Single responsibility
it('should set auth token', () => {
  service.setAuthToken('token');
  expect(service.getAuthToken()).toBe('token');
});

it('should update login state when token is set', () => {
  service.setAuthToken('token');
  service.isLoggedInObservable().subscribe((state) => {
    expect(state).toBe(true);
  });
});

// ❌ Bad - Testing multiple things
it('should handle authentication', () => {
  service.setAuthToken('token');
  expect(service.getAuthToken()).toBe('token');
  service.logOut();
  expect(service.getAuthToken()).toBeNull();
});

3. Use Descriptive Test Names

// ✅ Good
it('should return null when user is not logged in', () => {});
it('should emit error when token is expired', () => {});

// ❌ Bad
it('should work', () => {});
it('test 1', () => {});

4. Mock External Dependencies

const mockAuthService = {
  getAuthToken: () => 'mock-token',
  isLoggedInObservable: () => of(true)
};

TestBed.configureTestingModule({
  providers: [
    { provide: AuthService, useValue: mockAuthService }
  ]
});

5. Test Error Cases

it('should handle HTTP error', () => {
  service.getFavorites(1, 10).subscribe({
    next: () => fail('Should have failed'),
    error: (error) => {
      expect(error.status).toBe(401);
      expect(error.message).toContain('Unauthorized');
    }
  });
  
  const req = httpMock.expectOne((request) => 
    request.url.includes('favorites')
  );
  req.flush('Unauthorized', { status: 401, statusText: 'Unauthorized' });
});

Common Testing Scenarios

Testing Forms

it('should validate email format', () => {
  const emailControl = component.loginForm.get('email');
  
  emailControl?.setValue('invalid-email');
  expect(emailControl?.hasError('email')).toBe(true);
  
  emailControl?.setValue('[email protected]');
  expect(emailControl?.hasError('email')).toBe(false);
});

Testing Event Emitters

it('should emit favoriteDeleted event', (done) => {
  service.favoriteDeleted.subscribe((id: string) => {
    expect(id).toBe('123');
    done();
  });
  
  service.favoriteDeleted.emit('123');
});

Testing DOM Interaction

it('should display loading spinner', () => {
  component.isLoading = true;
  fixture.detectChanges();
  
  const spinner = fixture.debugElement.query(By.css('.loading-spinner'));
  expect(spinner).toBeTruthy();
});

Testing Dependencies

From package.json:
"devDependencies": {
  "@types/jasmine": "~4.3.0",
  "jasmine-core": "~4.6.0",
  "karma": "~6.4.0",
  "karma-chrome-launcher": "~3.2.0",
  "karma-coverage": "~2.2.0",
  "karma-jasmine": "~5.1.0",
  "karma-jasmine-html-reporter": "~2.1.0"
}
ScreenPulse uses Karma 6.4.0 with Jasmine 4.6.0 and Chrome launcher for running tests in a real browser environment.

Jasmine Documentation

Official Jasmine testing framework docs

Angular Testing Guide

Comprehensive Angular testing guide

Error Handling

Testing error interceptors and handlers

Authentication

Testing auth services and guards

Build docs developers (and LLMs) love