Skip to content

Latest commit

 

History

History
377 lines (297 loc) · 8.28 KB

File metadata and controls

377 lines (297 loc) · 8.28 KB

Testing Guide

This project uses Jasmine and Karma for unit testing Angular components and services.

Running Tests

Run All Tests (Single Run)

npm test

This runs all tests once in headless Chrome and exits. Perfect for CI/CD pipelines.

Watch Mode (Development)

npm run test:watch

Runs tests in watch mode with a browser window. Tests re-run automatically when files change.

Code Coverage

npm run test:coverage

Generates a code coverage report in the coverage/ directory.

View the report:

open coverage/clinical-trial-portal/index.html

Alternative Browsers

# Use Firefox instead of Chrome
npm run test:firefox

# Chrome with CI optimizations
npm run test:chrome

# Full CI test with coverage
npm run test:ci

Having issues in headless environments? See HEADLESS_TESTING.md for detailed setup instructions.

Test Structure

Current Test Coverage

Components:

  • AppComponent - 4 tests
    • App creation
    • Title property
    • Header rendering
    • Router outlet rendering

Services:

  • TrialService - 4 tests

    • Service creation
    • Fetching trials
    • Fetching single trial
    • Calculating trial stats
  • ParticipantService - 3 tests

    • Service creation
    • Fetching participants
    • Fetching participants by trial

Total: 11 tests passing

Writing Tests

Component Test Example

import { TestBed } from '@angular/core/testing';
import { provideRouter } from '@angular/router';
import { MyComponent } from './my.component';

describe('MyComponent', () => {
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [MyComponent],
      providers: [provideRouter([])]
    }).compileComponents();
  });

  it('should create', () => {
    const fixture = TestBed.createComponent(MyComponent);
    const component = fixture.componentInstance;
    expect(component).toBeTruthy();
  });

  it('should render title', () => {
    const fixture = TestBed.createComponent(MyComponent);
    fixture.detectChanges();
    const compiled = fixture.nativeElement as HTMLElement;
    expect(compiled.querySelector('h1')?.textContent).toContain('My Title');
  });
});

Service Test with HTTP

import { TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { MyService } from './my.service';

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

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        MyService,
        provideHttpClient(),
        provideHttpClientTesting()
      ]
    });
    service = TestBed.inject(MyService);
    httpMock = TestBed.inject(HttpTestingController);
  });

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

  it('should fetch data', () => {
    const mockData = [{ id: 1, name: 'Test' }];

    service.getData().subscribe(data => {
      expect(data).toEqual(mockData);
    });

    const req = httpMock.expectOne('http://api.example.com/data');
    expect(req.request.method).toBe('GET');
    req.flush(mockData);
  });
});

Testing Best Practices

1. Test Structure (AAA Pattern)

it('should do something', () => {
  // Arrange - Set up test data and conditions
  const fixture = TestBed.createComponent(MyComponent);
  const component = fixture.componentInstance;
  
  // Act - Perform the action
  component.doSomething();
  fixture.detectChanges();
  
  // Assert - Verify the result
  expect(component.result).toBe('expected');
});

2. Mock Dependencies

const mockService = jasmine.createSpyObj('MyService', ['getData']);
mockService.getData.and.returnValue(of(mockData));

TestBed.configureTestingModule({
  providers: [{ provide: MyService, useValue: mockService }]
});

3. Test Async Operations

it('should handle async data', fakeAsync(() => {
  component.loadData();
  tick(); // Simulate passage of time
  expect(component.data).toBeDefined();
}));

4. Test User Interactions

it('should handle button click', () => {
  const button = fixture.nativeElement.querySelector('button');
  button.click();
  fixture.detectChanges();
  expect(component.clicked).toBe(true);
});

Common Testing Patterns

Testing Components with Router

import { provideRouter } from '@angular/router';

TestBed.configureTestingModule({
  imports: [MyComponent],
  providers: [provideRouter(routes)]
});

Testing Components with Angular Material

import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';

TestBed.configureTestingModule({
  imports: [MyComponent],
  providers: [provideAnimationsAsync()]
});

Testing Forms

it('should validate form', () => {
  component.form.setValue({ name: '', email: 'invalid' });
  expect(component.form.valid).toBe(false);
  
  component.form.setValue({ name: 'John', email: 'john@example.com' });
  expect(component.form.valid).toBe(true);
});

Testing Observables

it('should emit values', (done) => {
  service.data$.subscribe(value => {
    expect(value).toBe('test');
    done();
  });
  
  service.updateData('test');
});

Debugging Tests

Run Specific Test File

ng test --include='**/my.component.spec.ts'

Run Tests Matching Pattern

ng test --grep='MyComponent'

Debug in Browser

  1. Run npm run test:watch
  2. Click "DEBUG" button in Karma browser
  3. Open browser DevTools
  4. Set breakpoints in your test code

Common Issues

Issue: Tests timing out

// Increase timeout for specific test
it('should handle slow operation', (done) => {
  // test code
}, 10000); // 10 second timeout

Issue: Async test not completing

// Use fakeAsync and tick()
it('should complete async', fakeAsync(() => {
  component.asyncMethod();
  tick(1000);
  expect(component.done).toBe(true);
}));

Issue: HTTP requests not mocked

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

CI/CD Integration

GitHub Actions Example

- name: Run tests
  run: npm test

- name: Generate coverage
  run: npm run test:coverage

- name: Upload coverage
  uses: codecov/codecov-action@v3
  with:
    files: ./coverage/clinical-trial-portal/lcov.info

Code Coverage Goals

  • Statements: > 80%
  • Branches: > 75%
  • Functions: > 80%
  • Lines: > 80%

Expanding Test Coverage

Priority Areas to Test

  1. Dashboard Component

    • Chart data rendering
    • Stats calculation
    • Loading states
  2. Trial List Component

    • Table rendering
    • Search functionality
    • Pagination
    • Sorting
  3. Trial Detail Component

    • Data loading
    • Participant list
    • Enrollment progress
  4. Custom Pipes

    • StatusColorPipe transformations
  5. Error Handling

    • HTTP error responses
    • Empty states
    • Loading states

Migration to React Testing

When migrating to React, you'll use different testing tools:

Angular → React Testing Equivalents

Angular/Jasmine React
Jasmine Jest
Karma Jest (built-in runner)
TestBed React Testing Library
HttpTestingController MSW (Mock Service Worker)
fixture.detectChanges() render()
fixture.nativeElement screen queries

React Testing Example

import { render, screen, waitFor } from '@testing-library/react';
import { MyComponent } from './MyComponent';

test('renders component', () => {
  render(<MyComponent />);
  expect(screen.getByText('Hello')).toBeInTheDocument();
});

test('handles click', async () => {
  render(<MyComponent />);
  const button = screen.getByRole('button');
  fireEvent.click(button);
  await waitFor(() => {
    expect(screen.getByText('Clicked')).toBeInTheDocument();
  });
});

Resources


Well-tested code is easier to migrate! 🧪