#angular #testing #jasmine #karma #unit-test

Day 17 β€” Unit Testing in Angular: Jasmine, Karma & Spies

πŸ“˜ Day 17 β€” Unit Testing in Angular: Jasmine, Karma & Spies

Zero to Hero β€” Hands-on Angular Tutorial

Today you will learn:

  • βœ”οΈ What is a Unit Test? (.spec.ts files)
  • βœ”οΈ Jasmine (The testing syntax) & Karma (The runner)
  • βœ”οΈ Testing Components (DOM interaction)
  • βœ”οΈ Testing Services (Mocking HTTP)
  • βœ”οΈ Spies: Fake it til you make it (spyOn)
  • βœ”οΈ Handling Signals in tests

Testing allows you to refactor code without fear. πŸ›‘οΈ


🟦 1. Anatomy of a Test File

Every component has a *.component.spec.ts file.

describe('CalculatorComponent', () => { // 1. Suite
  
  it('should add numbers correctly', () => { // 2. Spec (Test Case)
    // 3. Expectation (Assertion)
    expect(1 + 1).toEqual(2); 
    expect(true).toBeTrue();
  });

});

Run tests with:

ng test

🟩 2. Testing a Service (Logic)

Let’s test a simple MathService.

Service:

add(a: number, b: number) { return a + b; }

Test:

import { MathService } from './math.service';

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

  beforeEach(() => {
    service = new MathService();
  });

  it('should add two numbers', () => {
    const result = service.add(2, 3);
    expect(result).toBe(5);
  });
});

🟧 3. Testing Dependencies (Mocking / Spies)

Services often depend on other services (e.g., HttpClient, Router). Rule: NEVER call a real API in a unit test. Mock it.

Scenario: AuthService uses HttpClient to login.

import { TestBed } from '@angular/core/testing';
import { AuthService } from './auth.service';
import { HttpClient } from '@angular/common/http';
import { of } from 'rxjs';

describe('AuthService', () => {
  let service: AuthService;
  let httpSpy: jasmine.SpyObj<HttpClient>;

  beforeEach(() => {
    // Create a fake HTTP Client
    httpSpy = jasmine.createSpyObj('HttpClient', ['post']);

    TestBed.configureTestingModule({
      providers: [
        AuthService,
        { provide: HttpClient, useValue: httpSpy } // πŸ‘ˆ Inject the fake
      ]
    });

    service = TestBed.inject(AuthService);
  });

  it('should return token when login succeeds', (done) => {
    const mockResponse = { token: 'FAKE_TOKEN_123' };
    
    // Tell the spy what to return when called
    httpSpy.post.and.returnValue(of(mockResponse));

    service.login('test@test.com', 'pass').subscribe(res => {
      expect(res.token).toBe('FAKE_TOKEN_123');
      expect(httpSpy.post).toHaveBeenCalledTimes(1);
      done();
    });
  });
});
  • βœ”οΈ createSpyObj: Creates a fake object with methods.
  • βœ”οΈ returnValue: Defines what the fake method returns.
  • βœ”οΈ toHaveBeenCalled: Verifies the method was actually used.

πŸŸ₯ 4. Testing Components (The DOM)

We use ComponentFixture to interact with the HTML.

Component:

// .ts
title = 'Hello World';

// .html
<h1>{{ title }}</h1>
<button (click)="title = 'Clicked!'">Change</button>

Test:

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

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

  beforeEach(async () => {
    await TestBed.configureTestingModule({ imports: [MyComponent] }).compileComponents();
    fixture = TestBed.createComponent(MyComponent);
    component = fixture.componentInstance;
    fixture.detectChanges(); // Trigger Initial Render
  });

  it('should display initial title', () => {
    // Query the DOM
    const h1 = fixture.nativeElement.querySelector('h1');
    expect(h1.textContent).toContain('Hello World');
  });

  it('should change title on button click', () => {
    // 1. Find button
    const btn = fixture.debugElement.query(By.css('button'));
    
    // 2. Click it
    btn.triggerEventHandler('click', null);
    
    // 3. Update view
    fixture.detectChanges(); 

    // 4. Check result
    const h1 = fixture.nativeElement.querySelector('h1');
    expect(h1.textContent).toContain('Clicked!');
  });
});

🟫 5. Testing Signals

Signals update instantly, making them easier to test than Observables.

it('should update signal count', () => {
  component.increment();
  
  // No need for detectChanges() if checking the class directly
  expect(component.count()).toBe(1);
});

it('should reflect signal in DOM', () => {
  component.increment();
  fixture.detectChanges(); // Sync DOM
  
  const p = fixture.nativeElement.querySelector('p');
  expect(p.textContent).toBe('Count: 1');
});

πŸŽ‰ End of Day 17 β€” What You Learned

Today you became a responsible developer:

  • βœ”οΈ Unit Testing Structure: describe, it, expect.
  • βœ”οΈ Spies: Mocking complex dependencies (HttpClient).
  • βœ”οΈ DOM Testing: Clicking buttons and checking text in tests.
  • βœ”οΈ Signal Testing: Verifying reactive state changes.

πŸ§ͺ Day 17 Challenge

Write tests for your β€œCounter Component”:

Requirements:

  1. Verify the initial count is 0.
  2. Find the + button in the DOM, click it, and verify count is 1.
  3. Find the - button, click it, and verify count is -1 (or 0 if you have a min limit).
  4. Mock a LoggerService and ensure logger.log() is called when buttons are clicked.