出于测试目的,如何在 angular2 中模拟已激活的父路由?

Posted

技术标签:

【中文标题】出于测试目的,如何在 angular2 中模拟已激活的父路由?【英文标题】:How to mock an activatedRoute parent Route in angular2 for testing purposes? 【发布时间】:2017-04-08 22:45:57 【问题描述】:

假设我有这个

export class QuestionnaireQuestionsComponent 

    questions: Question[] = [];
    private loading:boolean = true;


    constructor(
        private route: ActivatedRoute,
        public questionnaireService:QuestionnaireService) 

    ngOnInit()
        this.route.parent.params.subscribe((params:any)=>
            this.questionnaireService.getQuestionsForQuestionnaire(params.id).subscribe((questions)=>
                this.questions = questions;
                this.loading = false;
            );
        );
    



我的组件实际上运行良好。问题是我想对其进行单元测试,但我不知道如何模拟 this.route.parent 对象。这是我失败的测试

beforeEach(()=>
    route = new ActivatedRoute();
    route.parent.params = Observable.of(id:"testId");

    questionnaireService = jasmine.createSpyObj('QuestionnaireService', ['getQuestionsForQuestionnaire']);
    questionnaireService.getQuestionsForQuestionnaire.and.callFake(() => Observable.of(undefined));
    component = new QuestionnaireQuestionsComponent(route, questionnaireService);
);


describe("on init", ()=>
    it("must call the service get questions for questionnaire",()=>
        component.ngOnInit();
        expect(questionnaireService.getQuestionsForQuestionnaire).toHaveBeenCalled();
    );  
);

测试失败并出现此错误

TypeError: undefined is not an object (evaluating 'this._routerState.parent') 

【问题讨论】:

尝试使用 parent: params: Observable.of(id:"testId") as ActivatedRoute 而不是真正的ActivatedRoute 不会编译我需要一个 ActivatedRoute 对象 你添加了as ActivatedRoute吗? 你没有在你的类定义中添加implements OnInit 【参考方案1】:

AcitvatedRoute是根据angular2docs的接口,所以我做的是实现一个MockActivatedRoute

import Observable from 'rxjs';
import Type from '@angular/core';
import ActivatedRoute,Route,ActivatedRouteSnapshot,UrlSegment,Params,Data  from '@angular/router';

export class MockActivatedRoute implements ActivatedRoute
    snapshot : ActivatedRouteSnapshot;
    url : Observable<UrlSegment[]>;
    params : Observable<Params>;
    queryParams : Observable<Params>;
    fragment : Observable<string>;
    data : Observable<Data>;
    outlet : string;
    component : Type<any>|string;
    routeConfig : Route;
    root : ActivatedRoute;
    parent : ActivatedRoute;
    firstChild : ActivatedRoute;
    children : ActivatedRoute[];
    pathFromRoot : ActivatedRoute[];
    toString() : string
        return "";
    ;

只需在我的测试中将ActivatedRoute 替换为MockActivatedRoute,就像这样

beforeEach(()=>
    route = new MockActivatedRoute();
    route.parent = new MockActivatedRoute();
    route.parent.params = Observable.of(id:"testId");

    questionnaireService = jasmine.createSpyObj('QuestionnaireService', ['getQuestionsForQuestionnaire']);
    questionnaireService.getQuestionsForQuestionnaire.and.callFake(() => Observable.of(undefined));
    component = new QuestionnaireQuestionsComponent(route, questionnaireService);
);

【讨论】:

感谢您,即使对于您不关心父母的已激活路线,它也非常有帮助。问题:您是否有理由更喜欢更新组件而不是注入路由和服务? 只是因为我没有测试组件的行为......所以真的不需要注入它,组件不应该改变我的测试用例中的任何东西 也可以参考文档:angular.io/docs/ts/latest/testing#!#create-an-observable-test-double 这两个测试文档链接都已损坏:( 我无法分配'父',因为它是常量或只读属性。还有缺少的道具和接口:paramMap 和 queryParamMap。应该为他们准备什么,这将在 Anuglar 4 中起作用吗?【参考方案2】:

使用测试床

 beforeEach(async(() => 
    TestBed.configureTestingModule(
      declarations: [YourComponent],
      imports: [],
      providers: [
        
          provide: ActivatedRoute, useValue: 
            params: Observable.of( id: 'test' )
          
        
      ]
    )
      .compileComponents();
  ));

【讨论】:

TestBed有时候编译的时候很慢,不需要的时候不用加载TestBed。 如果我想抓住这个怎么办:this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/someurl'; ? 如何更改我的 it(...) 中的 id 值?【参考方案3】:

你也可以简单的打params: searchTerm: 'this is not me' as any) as ActivatedRouteSnapshot

详细代码

  (service.resolve((params: id: 'this is id' as any) as ActivatedRouteSnapshot,
     as RouterStateSnapshot)as Observable<xyzResolveData>)
    .subscribe((data) => 
      expect((data as xyzResolveData).results).toEqual(xyzData.results);
    );

【讨论】:

【参考方案4】:

对于这个问题的新人,angular docs 涵盖了这个场景。

根据上述文档:

创建ActivatedRouteStub 类以用作ActivatedRoute 的测试替身

import  ReplaySubject  from 'rxjs/ReplaySubject';
import  convertToParamMap, ParamMap, Params  from '@angular/router';

/**
 * An ActivateRoute test double with a `paramMap` observable.
 * Use the `setParamMap()` method to add the next `paramMap` value.
 */
export class ActivatedRouteStub 
  // Use a ReplaySubject to share previous values with subscribers
  // and pump new values into the `paramMap` observable
  private subject = new ReplaySubject<ParamMap>();

  constructor(initialParams?: Params) 
    this.setParamMap(initialParams);
  

  /** The mock paramMap observable */
  readonly paramMap = this.subject.asObservable();

  /** Set the paramMap observables's next value */
  setParamMap(params?: Params) 
    this.subject.next(convertToParamMap(params));
  ;

然后在测试类中使用stub如下:

activatedRoute.setParamMap( id: '1234' );

【讨论】:

感谢您的评论。我已经用文档中的代码示例更新了答案。【参考方案5】:

要在每个“it”块中自定义模拟的 ActivatedRoute 数据,请结合上面 Kevin Black 的建议

beforeEach(async(() => 
    TestBed.configureTestingModule(
      declarations: [YourComponent],
      imports: [],
      providers: [
        
          provide: ActivatedRoute, useValue: 
            queryParams: of( id: 'test' )
          
        
      ]
    )
      .compileComponents();
  ));

在使用 fixture.detectChanges() 实例化组件之前,it('') 块中的以下代码

it('test', fakeAsync(() => 
  let activatedRoute: ActivatedRoute = fixture.debugElement.injector.get(ActivatedRoute);
  activatedRoute.queryParams = of( firstName: 'john', lastName: 'smith' );

  fixture.detectChanges(); // trigger ngOninit()
  tick();
));

【讨论】:

在 complieComponents() 调用后如何覆盖提供者值的好例子【参考方案6】:

对于您从 ActivatedRouteSnapshot 中读取查询参数的场景,例如:

this.someProperty = this.route.snapshot.data.query['paramKey'];

下面的代码将有助于将查询参数数据输入路由的测试

 
  provide: ActivatedRoute, 
  useValue: 
    snapshot: 
      data: 
        query: 
          paramKey: 'paramValue'
        
      
    
  

【讨论】:

正确解释。这一个值得正确答案 这种方法的实际测试如何?如何访问expect里面的值?【参考方案7】:

基于@S.Galarneau 接受的答案,这里是更新后的 ActivatedRoute 接口(角度 4+),带有访问修饰符,仅使用少数类型,以及使用 providers 数组并与 TestBed 集成:

这非常适合创建模拟接口并使用 providers 数组来使用您的 ActivatedRoute 版本:

export class MockActivatedRoute implements ActivatedRoute
  public snapshot;
  public url;
  public params;
  public queryParams: Observable<Params>;
  public fragment: Observable<string>;
  public data;
  public outlet;
  public component;
  public paramMap;
  public queryParamMap;
  public routeConfig;
  public root;
  public parent;
  public firstChild;
  public children;
  public pathFromRoot;
  public toString(): string 
    return '';
  ;

并与TestBed和Providers数组一起使用,只处理片段属性,像这样:

  beforeEach(async((): void => 
    const route = new MockActivatedRoute();
    route.fragment = of('#myhash?some_params=some_value'); 
    // because fragment is an Observable, we need `of(value)`
    

  TestBed.configureTestingModule(
    // declarations, imports, and providers go next:
    providers:[
         provide: ActivatedRoute, useValue: route,
    ]
  )

【讨论】:

【参考方案8】:

Angular 9 版本

    创建你的模拟:

     const mockActivatedRoute =  
       parent:  
         paramMap: of(convertToParamMap(
              organization: 'fakeOrganization' 
         ))
     ;
    

    提供它作为激活的路线

      providers: [ provide: ActivatedRoute, useValue: mockActivatedRoute ]
    

    测试一下

     describe('ngOnInit', () => 
         it('should get the route parameter', fakeAsync(() => 
           component.ngOnInit();
    
           expect(component.organization).toBe('fakeOrganization');
         ));
       );'
    

    正在测试的代码

       ngOnInit(): void 
         this.route.parent.paramMap.subscribe(params => 
           this.organization = params.get('organization');
         );
       
    

【讨论】:

以上是关于出于测试目的,如何在 angular2 中模拟已激活的父路由?的主要内容,如果未能解决你的问题,请参考以下文章

Angular2 - 在测试中模拟 RouteParams

Angular 2 单元测试组件,模拟 ContentChildren

如何在 Spring Security Bean 中模拟自定义 UserService 详细信息以进行单元测试?

Angular 2:如何在单元测试时模拟 ChangeDetectorRef

Angular2:如何将选定项目从 HTML 数据列表元素传递回组件?

Angular2(TypeScript)中的单元测试/模拟窗口属性