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

3.3k Views Asked by At

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?

1

There are 1 best solutions below

0
On BEST ANSWER

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');
});