Part 14: Angular Testing – Comprehensive Strategies for Reliable Apps

Welcome to our complete guide on Angular testing! Ensuring your application works as expected requires a solid testing strategy. Let’s explore the Angular testing ecosystem in depth.

1. Angular Testing Pyramid

A balanced testing approach:

text

        E2E (5%)
       /      \
   Integration (15%)
     /          \
Unit Tests (80%)

2. Unit Testing Components

Basic Component Test

typescript

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

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

  beforeEach(() => {
    fixture = TestBed.createComponent(ButtonComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

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

  it('should emit on click', () => {
    spyOn(component.clicked, 'emit');
    const button = fixture.nativeElement.querySelector('button');
    button.click();
    expect(component.clicked.emit).toHaveBeenCalled();
  });
});

Testing Component Templates

typescript

it('should display the input label', () => {
  component.label = 'Test Label';
  fixture.detectChanges();
  const label = fixture.nativeElement.querySelector('label');
  expect(label.textContent).toContain('Test Label');
});

3. Testing Services

HTTP Service Testing

typescript

describe('DataService', () => {
  let service: DataService;
  let httpTestingController: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule]
    });
    service = TestBed.inject(DataService);
    httpTestingController = TestBed.inject(HttpTestingController);
  });

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

  it('should fetch data', () => {
    const testData = { id: 1, name: 'Test' };
    
    service.getData(1).subscribe(data => {
      expect(data).toEqual(testData);
    });

    const req = httpTestingController.expectOne('api/data/1');
    expect(req.request.method).toBe('GET');
    req.flush(testData); // Simulate response
  });
});

4. Integration Testing

Testing Component Interactions

typescript

describe('UserListComponent', () => {
  let fixture: ComponentFixture<UserListComponent>;
  let userService: UserService;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [UserListComponent, UserCardComponent],
      providers: [UserService],
      imports: [SharedModule]
    }).compileComponents();

    userService = TestBed.inject(UserService);
    spyOn(userService, 'getUsers').and.returnValue(of([
      { id: 1, name: 'Test User' }
    ]));
  });

  it('should display users from service', () => {
    fixture.detectChanges();
    const cards = fixture.nativeElement.querySelectorAll('app-user-card');
    expect(cards.length).toBe(1);
    expect(cards[0].textContent).toContain('Test User');
  });
});

5. End-to-End (E2E) Testing with Cypress

typescript

describe('Login Flow', () => {
  it('should login successfully', () => {
    cy.visit('/login');
    cy.get('[data-cy=email]').type('test@example.com');
    cy.get('[data-cy=password]').type('password123');
    cy.get('[data-cy=submit]').click();
    cy.url().should('include', '/dashboard');
    cy.get('[data-cy=welcome-message]').should('contain', 'Welcome');
  });
});

6. Advanced Testing Techniques

Testing RxJS Streams

typescript

it('should debounce search input', fakeAsync(() => {
  const searchService = TestBed.inject(SearchService);
  spyOn(searchService, 'search');
  
  component.searchControl.setValue('test');
  tick(300); // Wait for debounce
  expect(searchService.search).toHaveBeenCalledWith('test');
}));

Component Harnesses (for Material)

typescript

describe('MatSelectHarness', () => {
  let loader: HarnessLoader;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [MatSelectModule],
      declarations: [SelectComponent]
    }).compileComponents();
    loader = TestbedHarnessEnvironment.loader(fixture);
  });

  it('should select option', async () => {
    const select = await loader.getHarness(MatSelectHarness);
    await select.open();
    const options = await select.getOptions();
    await options[1].click();
    expect(await select.getValueText()).toBe('Option 2');
  });
});

7. Code Coverage Reports

Generate reports with:

bash

ng test --code-coverage

Coverage thresholds in karma.conf.js:

javascript

coverageReporter: {
  check: {
    global: {
      statements: 80,
      branches: 75,
      functions: 85,
      lines: 80
    }
  }
}

8. Continuous Integration Setup

Example GitHub Actions config (.github/workflows/test.yml):

yaml

name: Angular Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - run: npm ci
      - run: npm run test -- --watch=false --browsers=ChromeHeadless
      - run: npm run e2e

Testing Best Practices

  1. Test Behavior, Not Implementation
  2. Keep Tests Isolated
  3. Use Descriptive Test Names
  4. Follow the Arrange-Act-Assert Pattern
  5. Maintain Fast, Deterministic Tests
  6. Test Edge Cases and Error Conditions

Hands-On Testing Challenge

For a simple counter component:

  1. Write unit tests for all public methods
  2. Test template bindings
  3. Verify emitted events
  4. Create an integration test with a parent component
  5. Write a Cypress E2E test

What’s Next?

In Part 15, we’ll explore Angular Deployment Strategies – how to get your app production-ready!

Leave a Comment

Your email address will not be published. Required fields are marked *