angular - 使用 Jasmine 模拟 Angular 9 服务

标签 angular typescript unit-testing jasmine angular9

使用 Angular 7,我可以 mock 我的 SearchService与 Jasmine 一起创建一些类。第一个是helper.ts具有可以扩展的类的文件。

/// <reference path="../../../../../node_modules/@types/jasmine/index.d.ts"‌​/>

export interface GuinessCompatibleSpy extends jasmine.Spy {
  /** By chaining the spy with and.returnValue, all calls to the function will return a specific
   * value. */
  andReturn(val: any): void;
  /** By chaining the spy with and.callFake, all calls to the spy will delegate to the supplied
   * function. */
  andCallFake(fn: Function): GuinessCompatibleSpy;
  /** removes all recorded calls */
  reset();
}

export class SpyObject {
  static stub(object = null, config = null, overrides = null) {
    if (!(object instanceof SpyObject)) {
      overrides = config;
      config = object;
      object = new SpyObject();
    }

    const m = {};
    Object.keys(config).forEach((key) => m[key] = config[key]);
    Object.keys(overrides).forEach((key) => m[key] = overrides[key]);
    for (const key in m) {
      object.spy(key).andReturn(m[key]);
    }
    return object;
  }

  constructor(type = null) {
    if (type) {
      for (const prop in type.prototype) {
        let m = null;
        try {
          m = type.prototype[prop];
        } catch (e) {
          // As we are creating spys for abstract classes,
          // these classes might have getters that throw when they are accessed.
          // As we are only auto creating spys for methods, this
          // should not matter.
        }
        if (typeof m === 'function') {
          this.spy(prop);
        }
      }
    }
  }

  spy(name) {
    if (!this[name]) {
      this[name] = this._createGuinnessCompatibleSpy(name);
    }
    return this[name];
  }

  prop(name, value) { this[name] = value; }

  /** @internal */
  _createGuinnessCompatibleSpy(name): GuinessCompatibleSpy {
    const newSpy: GuinessCompatibleSpy = <any>jasmine.createSpy(name);
    newSpy.andCallFake = <any>newSpy.and.callFake;
    newSpy.andReturn = <any>newSpy.and.returnValue;
    newSpy.reset = <any>newSpy.calls.reset;
    // revisit return null here (previously needed for rtts_assert).
    newSpy.and.returnValue(null);
    return newSpy;
  }
}

这是 search.service.ts我正在尝试测试:

@Injectable({
  providedIn: 'root'
})
export class SearchService {

  constructor(private http: HttpClient) { }

  getAll() {
    return this.http.get('assets/data/people.json');
  }

  search(q: string): Observable<any> {
    // implementation
  }

  get(id: number) {
    // implementation
  }

  save(person: Person) {
    // implementation
  }
}

这是我的 search.service.mock.ts :

import { SpyObject } from './helper';
import { SearchService } from '../search.service';
import Spy = jasmine.Spy;

export class MockSearchService extends SpyObject {
  getAllSpy: Spy;
  getByIdSpy: Spy;
  searchSpy: Spy;
  saveSpy: Spy;
  fakeResponse: any;

  constructor() {
    super(SearchService);

    this.fakeResponse = null;
    this.getAllSpy = this.spy('getAll').andReturn(this);
    this.getByIdSpy = this.spy('get').andReturn(this);
    this.searchSpy = this.spy('search').andReturn(this);
    this.saveSpy = this.spy('save').andReturn(this);
  }

  subscribe(callback: any) {
    callback(this.fakeResponse);
  }

  setResponse(json: any): void {
    this.fakeResponse = json;
  }
}

然后我在测试中模拟它。

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SearchComponent } from './search.component';
import { MockSearchService } from '../shared/search/mocks/search.service';
import { MockActivatedRoute, MockRouter } from '../shared/search/mocks/routes';
import { SearchService } from '../shared';
import { ActivatedRoute, Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { FormsModule } from '@angular/forms';

describe('SearchComponent', () => {
  let component: SearchComponent;
  let fixture: ComponentFixture<SearchComponent>;
  let mockSearchService: MockSearchService;
  let mockActivatedRoute: MockActivatedRoute;

  beforeEach(async(() => {
    mockSearchService = new MockSearchService();
    mockActivatedRoute = new MockActivatedRoute({'term': 'peyton'});

    TestBed.configureTestingModule({
      declarations: [ SearchComponent ],
      providers: [
        {provide: SearchService, useValue: mockSearchService},
        {provide: ActivatedRoute, useValue: mockActivatedRoute}
      ],
      imports: [FormsModule, RouterTestingModule]
    }).compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(SearchComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});

这适用于 Angular 7。但是,当我尝试使用 Angular 9 时,我必须删除 // <reference path="..."/>helper.ts 的顶部修复一些编译器错误。

ERROR in node_modules/@types/jasmine/index.d.ts:25:1 - error TS6200: Definitions of the following identifiers conflict with those in another file: ImplementationCallback, Func, Constructor, ExpectedRecursive, Expected, SpyObjMethodNames, CustomEqualityTester, CustomMatcherFactory, ExpectationFailed, SpecFunction, SpyObj, jasmine

25 type ImplementationCallback = jasmine.ImplementationCallback;
   ~~~~

然后我收到两个错误:

Chrome 78.0.3904 (Mac OS X 10.15.1) SearchComponent should create FAILED
    Failed: this.getSpy is not a function
        at <Jasmine>

还有:

NullInjectorError: R3InjectorError(DynamicTestModule)[SearchService -> HttpClient -> HttpClient]:
  NullInjectorError: No provider for HttpClient!
error properties: Object({ ngTempTokenPath: null, ngTokenPath: [ 'SearchService', 'HttpClient', 'HttpClient' ] })

知道为什么这在 Angular 7 中有效,但在 Angular 9 中无效吗?

与 Angular 7 配合使用的应用程序位于 GitHub 上 https://github.com/mraible/ng-demo .

最佳答案

我认为摆脱 helper.ts 并模拟服务返回的内容会更容易。我还更改为导入 HttpClientTestingModule,以便可以实例化该服务,即使其 HttpClient 从未使用过。这是重构后的 search.component.spec.ts:

import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { SearchComponent } from './search.component';
import { MockActivatedRoute } from '../shared/search/mocks/routes';
import { SearchService } from '../shared';
import { ActivatedRoute } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
import { of } from 'rxjs';
import { HttpClientTestingModule } from '@angular/common/http/testing';

describe('SearchComponent', () => {
  let component: SearchComponent;
  let fixture: ComponentFixture<SearchComponent>;
  let mockActivatedRoute: MockActivatedRoute;
  let mockSearchService: SearchService;

  beforeEach(async(() => {
    mockActivatedRoute = new MockActivatedRoute({term: 'nikola'});

    TestBed.configureTestingModule({
      declarations: [SearchComponent],
      providers: [
        {provide: ActivatedRoute, useValue: mockActivatedRoute}
      ],
      imports: [FormsModule, RouterTestingModule, HttpClientTestingModule]
    }).compileComponents();
  }));

  beforeEach(() => {
    // mock response
    mockSearchService = TestBed.inject(SearchService);
    mockSearchService.search = jasmine.createSpy().and.returnValue(of([]));

    // initialize component
    fixture = TestBed.createComponent(SearchComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should search when a term is set and search() is called', () => {
    component = fixture.debugElement.componentInstance;
    component.query = 'J';
    component.search();
    expect(mockSearchService.search).toHaveBeenCalledWith('J');
  });

  it('should search automatically when a term is on the URL', () => {
    fixture.detectChanges();
    expect(mockSearchService.search).toHaveBeenCalledWith('nikola');
  });
});

对于另一项测试,我做了类似的事情,从服务返回预期数据。

import { EditComponent } from './edit.component';
import { TestBed } from '@angular/core/testing';
import { Address, Person, SearchService } from '../shared';
import { MockRouter, MockActivatedRoute } from '../shared/search/mocks/routes';
import { ActivatedRoute, Router } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { of } from 'rxjs';
import { HttpClientTestingModule } from '@angular/common/http/testing';

describe('EditComponent', () => {
  let mockSearchService: SearchService;
  let mockActivatedRoute: MockActivatedRoute;
  let mockRouter: MockRouter;

  beforeEach(() => {
    mockActivatedRoute = new MockActivatedRoute({id: 1});
    mockRouter = new MockRouter();

    TestBed.configureTestingModule({
      declarations: [EditComponent],
      providers: [
        {provide: ActivatedRoute, useValue: mockActivatedRoute},
        {provide: Router, useValue: mockRouter}
      ],
      imports: [FormsModule, HttpClientTestingModule]
    }).compileComponents();

    mockSearchService = TestBed.inject(SearchService);
  });

  it('should fetch a single record', () => {
    const fixture = TestBed.createComponent(EditComponent);

    const person = new Person({id: 1, name: 'Gary Harris'});
    person.address = new Address({city: 'Denver'});

    // mock response
    spyOn(mockSearchService, 'get').and.returnValue(of(person));

    // initialize component
    fixture.detectChanges();

    // verify service was called
    expect(mockSearchService.get).toHaveBeenCalledWith(1);

    // verify data was set on component when initialized
    const editComponent = fixture.debugElement.componentInstance;
    expect(editComponent.editAddress.city).toBe('Denver');

    // verify HTML renders as expected
    const compiled = fixture.debugElement.nativeElement;
    expect(compiled.querySelector('h3').innerHTML).toBe('Gary Harris');
  });
});

关于angular - 使用 Jasmine 模拟 Angular 9 服务,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58782024/

相关文章:

Angular9 + NX + Storybook : Why are my Scss styles getting ignored in Storybook ? 没有错误

javascript - 如何在解析器 Angular 中访问 queryParams

Angular 2 : Chain Observables in Resolver

reactjs - NextJS/Typescript/Apollo 错误;属性在类型上不存在

javascript - 如何将 Typescript 与原生 ES6 Promises 结合使用

typescript - 使用 Angular 2 连接 Google Maps Nativescript 插件

cocoa - 用于单元测试的外部数据文件

javascript - 如何将 8 位字符串数组作为 Angular 的输入并传递到 Node.js 后端?

javascript - 如何使用 jest 在 supertest 中模拟模块?

java - 渲染时定义的 Spring Webflow 操作未在单元测试中调用