Angular course · Module 11: Testing

Mocks and Spies - Techniques of Deception

2 min read ·
In this lesson4

Mocks are like ninja messengers - they impersonate real objects but are under the control of tests.

jasmine.createSpyObj

describe('DojoComponent with Mock', () => {
  let component: DojoComponent;
  let fixture: ComponentFixture<DojoComponent>;
  let samuraiServiceSpy: jasmine.SpyObj<SamuraiService>;

  beforeEach(async () => {
    // Create mock with methods
    samuraiServiceSpy = jasmine.createSpyObj('SamuraiService', [
      'getAll',
      'getById',
      'create',
      'update'
    ]);

    // Set default return values
    samuraiServiceSpy.getAll.and.returnValue(of([
      { id: 1, name: 'Musashi' }
    ]));

    await TestBed.configureTestingModule({
      imports: [DojoComponent],
      providers: [
        { provide: SamuraiService, useValue: samuraiServiceSpy }
      ]
    }).compileComponents();

    fixture = TestBed.createComponent(DojoComponent);
    component = fixture.componentInstance;
  });

  it('should load samurai on init', () => {
    fixture.detectChanges();  // ngOnInit

    expect(samuraiServiceSpy.getAll).toHaveBeenCalled();
    expect(component.samuraiList.length).toBe(1);
  });

  it('should call create when adding samurai', () => {
    const newSamurai = { name: 'Hanzo', clan: 'Iga' };
    samuraiServiceSpy.create.and.returnValue(of({ id: 2, ...newSamurai }));

    component.addSamurai(newSamurai);

    expect(samuraiServiceSpy.create).toHaveBeenCalledWith(newSamurai);
  });
});

spyOn - Spying on Methods

describe('BattleService', () => {
  let service: BattleService;
  let loggerService: LoggerService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [BattleService, LoggerService]
    });

    service = TestBed.inject(BattleService);
    loggerService = TestBed.inject(LoggerService);
  });

  it('should log battle result', () => {
    // Spy on an existing method
    const logSpy = spyOn(loggerService, 'log');

    service.fight('Musashi', 'Sasaki');

    expect(logSpy).toHaveBeenCalledWith(
      jasmine.stringMatching(/Battle:/)
    );
  });

  it('should call through and track', () => {
    // Call the real method, but also track it
    spyOn(loggerService, 'log').and.callThrough();

    service.fight('Musashi', 'Sasaki');

    expect(loggerService.log).toHaveBeenCalled();
  });

  it('should fake implementation', () => {
    spyOn(loggerService, 'log').and.callFake((message) => {
      console.log('FAKE LOG:', message);
    });

    service.fight('Musashi', 'Sasaki');
  });
});

Mock Class

// mock-samurai.service.ts
export class MockSamuraiService {
  private mockData: Samurai[] = [
    { id: 1, name: 'Test Samurai', clan: 'Test Clan', level: 1 }
  ];

  getAll(): Observable<Samurai[]> {
    return of(this.mockData);
  }

  getById(id: number): Observable<Samurai | undefined> {
    return of(this.mockData.find(s => s.id === id));
  }

  create(samurai: Omit<Samurai, 'id'>): Observable<Samurai> {
    const newSamurai = { id: Date.now(), ...samurai };
    this.mockData.push(newSamurai);
    return of(newSamurai);
  }
}

// Usage in tests
TestBed.configureTestingModule({
  providers: [
    { provide: SamuraiService, useClass: MockSamuraiService }
  ]
});

Testing Dependencies with InjectionToken

import { InjectionToken } from '@angular/core';

export const API_CONFIG = new InjectionToken<ApiConfig>('api.config');

describe('ConfigurableService', () => {
  it('should use injected config', () => {
    const mockConfig: ApiConfig = {
      baseUrl: 'http://test-api',
      timeout: 1000
    };

    TestBed.configureTestingModule({
      providers: [
        ConfigurableService,
        { provide: API_CONFIG, useValue: mockConfig }
      ]
    });

    const service = TestBed.inject(ConfigurableService);
    expect(service.getBaseUrl()).toBe('http://test-api');
  });
});

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What is the difference between jasmine.createSpyObj and spyOn?

  2. 2. How do you set the return value of a mock method created with createSpyObj?

These are 2 of 3 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Code editor

    Complete the component test with a mocked service.

  • Vertical ordering

    Arrange the steps of preparing a mocked service in beforeEach (top to bottom):

  • Click in order

    Arrange the line that spies on the log method of an existing service: spyOn(loggerService, 'log')

  • Horizontal ordering

    Arrange the provider that replaces the service with a mock class (one line):

Useful articles