使用一个返回值的方法创建模拟服务(对象)

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

在 Angular 环境中,如何在 Jest 环境中轻松地为服务创建 mocked 服务返回特定值的对象?这可能是通过 ng-mocks 的 Jest 等

一个过于简单的例子:

// 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");

我研究了 Jest 和 ng-mocks 文档,但没有找到一个非常简单的方法。您可以在下面找到 2 种工作方法。可以改进版本吗?

我简化的 Angular 组件:

@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();
  }
}

这是简化的服务:

@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;
  }
}

尝试 1:可行的解决方案: 和 'jest.genMockFromModule'。

缺点是我只能在每个系列测试开始时创建一个returnValue,所以在每个设置时间之前。

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

尝试 2: 将 'jest.genMockFromModule' 替换为 'jest.createMockFromModule':同样有效。

缺点仍然是我只能在每个系列测试开始时创建一个 returnValue,所以在 beforeEach 设置时间。

尝试 3:预先创建模拟对象:无效

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

这行不通,报错:

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

尝试4:用.fn()。缺点是原来的class可能会改变,那么测试对象必须改变。

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

这一次,错误是:

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

你能帮助改进这种工作方式吗?

您需要使用MockBuilder to mock the service, and MockInstance进行自定义。

另外 getCount 是一个可观察的,因此它的模拟应该 return Subject,我们可以操纵它。

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