jestjsangular-jestng-mocks

Create mocked service (object) with one method returning a value


In an Angular environment, how can I very easily create in a Jest environment a mocked service for a service object returning a specific value? This could be via Jest of ng-mocks, etc.

An oversimplified example:

// beforeEach: 
// setup an Angular component with a service component myMockService

// Test 1: 
// fake "myMockService.doSomething" to return value 10
// expect(myComponent.getBalance()).toEqual( "Your balance: 10"); 

// Test 2: 
// fake "myMockService.doSomething" to return value 20
// expect(myComponent.getBalance()).toEqual( "Your balance: 20");

I have studied the Jest and ng-mocks docs but didn't find a very easy approach. Below you find 2 working approaches. Can you improve the version?

My simplified Angular component:

@Component({
  selector: 'app-servicecounter',
  templateUrl: './servicecounter.component.html'
})
export class ServicecounterComponent {
  private myValue: number = 1;
  constructor(private counterService: CounterService) { }
  public doSomething(): void {
    // ...
    myValue = this.counterService.getCount();
  }
}

This is the simplified service:

@Injectable()
export class CounterService {
  private count = 0;
  constructor() { }
  public getCount(): Observable<number> {
    return this.count;
  }
  public increment(): void { 
    this.count++;
  }
  public decrement(): void {
    this.count--;
  }
  public reset(newCount: number): void {
    this.count = newCount;
  }
}

Try 1: a working solution: with 'jest.genMockFromModule'.

The disadvantage is that I can only create a returnValue only at the start of each series of tests, so at beforeEach setup time.

beforeEach(async () => {
  mockCounterService = jest.genMockFromModule( './counterservice.service');
  mockCounterService.getCount = jest.fn( () => 3);
  mockCounterService.reset = jest.fn(); // it was called, I had to mock.fn it. 
  await TestBed.configureTestingModule({
    declarations: [ServicecounterComponent],
    providers: [ { provide: CounterService, useValue: mockCounterService }],
  }).compileComponents();
  fixture = TestBed.createComponent(ServicecounterComponent);
  component = fixture.componentInstance;
  fixture.detectChanges();
});

it('shows the count', () => {
  setFieldValue(fixture, 'reset-input', String(currentCount));
  click(fixture, 'reset-button');
  expect(mockCounterService.getCount()).toEqual( 3);
  expect( mockCounterService.getCount).toBeCalled();
});

Try 2: replace 'jest.genMockFromModule' with 'jest.createMockFromModule': works equally well.

The disadvantage is still that I can create a returnValue only at the start of each series of tests, so at beforeEach setup time.

Try 3: create a mock object upfront: didn't work

jest.mock( "./counterservice.service");

beforeEach(async () => {
  // Create fake
  mockCounterService = new CounterService();
  (mockCounterService.getCount() as jest.Mock).mockReturnValue( 0);
  await TestBed.configureTestingModule({
    declarations: [ServicecounterComponent],
    providers: [{ provide: CounterService, useValue: mockCounterService }],
  }).compileComponents();

  fixture = TestBed.createComponent(ServicecounterComponent);
  component = fixture.componentInstance;
  fixture.detectChanges();
}); 

it('shows the count', () => {
  // do something that will trigger the mockCountService getCount method.  
  expect(mockCounterService.getCount).toEqual( 0);
});

This doesn't work, giving the error:

> (mockCounterService.getCount() as jest.Mock).mockReturnValue( 0); 
> Cannot read property 'mockReturnValue' of undefined

Try 4: with .fn(). The disadvantage is that the original class may change, then the test object MUST change.

beforeEach(async () => {
  mockCounterService = {
    getCount: jest.fn().mockReturnValue( 0),
    increment: jest.fn,
    decrement: jest.fn(),
    reset: jest.fn
  };
  await TestBed.configureTestingModule({
    declarations: [ServicecounterComponent],
    providers: [{ provide: CounterService, useValue: mockCounterService }],
  }).compileComponents();
});
  
it( '... ', () => {
  // ... 
  expect(mockCounterService.reset).toHaveBeenCalled();
});

This time, the error is:

> Matcher error: received value must be a mock or spy function ...
> expect(mockCounterService.reset).toHaveBeenCalled();

Can you help improving this way of working?


Solution

  • You need to use MockBuilder to mock the service, and MockInstance to customize it.

    Also getCount is an observable, therefore its mock should return Subject, which we can manipulate.

    // to reset MockInstance customizations after tests
    MockInstance.scope();
    
    // to use jest.fn on all mocks https://ng-mocks.sudo.eu/extra/auto-spy
    beforeEach(() => ngMocks.autoSpy('jest'));
    afterEach(() => ngMocks.autoSpy('reset'));
    
    beforeEach(() => MockBuilder(ServicecounterComponent, CounterService));
    
    it('testing', () => {
      // this is our control of observable of getCount 
      const getCount$ = new Subject<number>();
      // now we need to return it when getCount is called
      const getCount = MockInstance(CounterService, 'getCount', jest.fn())
        .mockReturnValue(getCount$);
    
      // now we can use it in our test.
      const fixture = MockRender(ServicecounterComponent);
      ngMocks.click('.reset-button');
      expect(getCount).toHaveBeenCalled();
    
      getCount$.next(3);
      expect(ngMocks.formatText(fixture)).toContain('3');
    });