【问题标题】:How to unit test a component that depends on parameters from ActivatedRoute?如何对依赖于 ActivatedRoute 参数的组件进行单元测试?
【发布时间】:2016-11-16 07:13:56
【问题描述】:

我正在对用于编辑对象的组件进行单元测试。该对象具有唯一的id,用于从服务中托管的对象数组中获取特定对象。特定的id 是通过路由传递的参数获取的,特别是通过ActivatedRoute 类。

构造函数如下:

constructor(private _router:Router, private _curRoute:ActivatedRoute, private _session:Session) {}
    
ngOnInit() {
  this._curRoute.params.subscribe(params => {
    this.userId = params['id'];
    this.userObj = this._session.allUsers.filter(user => user.id.toString() === this.userId.toString())[0];

我想在这个组件上运行基本的单元测试。但是,我不确定如何注入id 参数,而组件需要这个参数。

顺便说一句:我已经模拟了Session 服务,所以不用担心。

【问题讨论】:

    标签: unit-testing angular angular2-routing angular2-testing


    【解决方案1】:

    最简单的方法是使用useValue 属性并提供一个你想要模拟的值的Observable。

    RxJS

    import { Observable } from 'rxjs/Observable';
    import 'rxjs/add/observable/of';
    ...
    {
      provide: ActivatedRoute,
      useValue: {
        params: Observable.of({id: 123})
      }
    }
    

    RxJS >= 6

    import { of } from 'rxjs';
    ...
    {
      provide: ActivatedRoute,
      useValue: {
        params: of({id: 123})
      }
    }
    

    【讨论】:

    • Observable.of 对我来说不存在! :S
    • 从 rxjs/Observable 导入 Observable
    • 此代码在我的项目中出现此错误:Uncaught NetworkError: Failed to execute 'send' on 'XMLHttpRequest': Failed to load 'ng:///DynamicTestModule/HomeContentComponent.ngfactory.js'. at http://localhost:9876/_karma_webpack_/polyfills.bundle.js:2605
    • RxJs 6 of 应该单独使用。此外,您可能会使用 RouterTestingModule 而不是此答案的代码。
    • @BenRacicot 这个答案是在 RxJs 6 存在之前给出的。也改为说“改为这样做”提供了一个可以直接投票的答案。
    【解决方案2】:

    在 Angular 8+ 中有 RouterTestingModule,您可以使用它来访问组件的 ActivatedRouteRouter。您也可以将路由传递给RouterTestingModule,并为请求的路由方法创建间谍。

    例如在我的组件中:

    ngOnInit() {
        if (this.route.snapshot.paramMap.get('id')) this.editMode()
        this.titleService.setTitle(`${this.pageTitle} | ${TAB_SUFFIX}`)
    }
    

    在我的测试中,我有:

      beforeEach(async(() => {
        TestBed.configureTestingModule({
          declarations: [ ProductLinePageComponent ],
          schemas: [NO_ERRORS_SCHEMA],
          imports: [
            RouterTestingModule.withRoutes([])
          ],
        })
        .compileComponents()
      }))
    
      beforeEach(() => {
        router = TestBed.get(Router)
        route = TestBed.get(ActivatedRoute)
      })
    

    稍后在“它”部分:

      it('should update', () => {
        const spyRoute = spyOn(route.snapshot.paramMap, 'get')
        spyRoute.and.returnValue('21')
        fixture = TestBed.createComponent(ProductLinePageComponent)
        component = fixture.componentInstance
        fixture.detectChanges()
        expect(component).toBeTruthy()
        expect(component.pageTitle).toBe('Edit Product Line')
        expect(component.formTitle).toBe('Edit Product Line')
        // here you can test the functionality which is triggered by the snapshot
      })
    

    以类似的方式,我认为您可以通过 jasmine 的 spyOnProperty 方法直接测试 paramMap,方法是返回一个可观察对象或使用 rxjs 弹珠。它可能会节省一些时间,也不需要维护额外的模拟类。 希望它有用并且有意义。

    【讨论】:

    • 比维护一个额外的模拟要好得多,您可以轻松地在测试中设置不同的参数。谢谢!
    • 这有帮助。你知道如何监视不同的参数: const dirName = this.route.snapshot.paramMap.get('dirName');常量 actionType = this.route.snapshot.paramMap.get('actionType');哪个机器人会监视 spyOn(route.snapshot.paramMap, 'get') ?我可以指定要听的键吗?
    • 正如我上面提到的,我认为您可以使用 spyOnProperty 而不是 spyOn,例如 spyOnProperty(route.snapshot.paramMap.get, 'dirName')。如果我还没有完全回答你的问题,请不要犹豫告诉我。谢谢。
    • 谢谢你,这很有帮助 ☺
    【解决方案3】:

    我已经想出了如何做到这一点!

    由于ActivatedRoute是一个服务,可以为它建立一个模拟服务。让我们将此模拟服务称为MockActivatedRoute。我们将ActivatedRoute扩展成MockActivatedRoute,如下:

    class MockActivatedRoute extends ActivatedRoute {
        constructor() {
            super(null, null, null, null, null);
            this.params = Observable.of({id: "5"});
        }
    

    super(null, ....) 行初始化超类,它有四个强制参数。但是,在这种情况下,我们不需要任何这些参数,因此我们将它们初始化为 null 值。我们需要的只是params 的值,即Observable<>。因此,使用this.params,我们覆盖params 的值并将其初始化为测试对象所依赖的参数的Observable<>

    然后,与任何其他模拟服务一样,只需对其进行初始化并覆盖组件的提供者。

    祝你好运!

    【讨论】:

    • 我现在正面临这个问题!但是,当我尝试使用 superObservable 时出现错误。这些是从哪里来的?
    • super() 是内置的。Observable 来自rxjs/Observable 或只是rxjs,具体取决于您的版本。您可以使用import {Observable} from 'rxjs' 来获取它。
    • 您接受了一个答案并发布了另一个答案...如果这是汉兰达(而且只能是一个),您“真正”选择了哪个答案,为什么?也就是说,我认为这基本上与您接受的 zmanc 的答案相同。您是否从设置这个 [稍微] 更复杂的模拟中发现了额外的价值?
    【解决方案4】:

    这是我在 Angular 2.0 最新版本中测试它的方法...

    import { ActivatedRoute, Data } from '@angular/router';
    

    在提供者部分

    {
      provide: ActivatedRoute,
      useValue: {
        data: {
          subscribe: (fn: (value: Data) => void) => fn({
            yourData: 'yolo'
          })
        }
      }
    }
    

    【讨论】:

    • 能否提供提供程序部分的完整代码?
    • 这是一个完整的单元测试类。 plnkr.co/edit/UeCKnJ2sCCpLLQcWqEGX?p=catalogue
    • 如何在 ngOnDestroy 中测试退订
    • 这将打破现实生活中的用例,因为您没有返回订阅并且您将无法在 ngOnDestroy 中使用调用 .unsubscribe()。
    • data: Observable.of({yourData: 'yolo'}) 会起作用。
    【解决方案5】:

    只需添加一个 ActivatedRoute 的模拟:

    providers: [
      { provide: ActivatedRoute, useClass: MockActivatedRoute }
    ]
    

    ...

    class MockActivatedRoute {
      // here you can add your mock objects, like snapshot or parent or whatever
      // example:
      parent = {
        snapshot: {data: {title: 'myTitle ' } },
        routeConfig: { children: { filter: () => {} } }
      };
    }
    

    【讨论】:

      【解决方案6】:

      对于一些在 Angular > 5 上工作的人,if Observable.of();不起作用,那么他们可以通过从 'rxjs' 导入 import { of } 来使用 of();

      【讨论】:

        【解决方案7】:

        在为路由路径创建测试套件时遇到了同样的问题:

        {
           path: 'edit/:property/:someId',
           component: YourComponent,
           resolve: {
               yourResolvedValue: YourResolver
           }
        }
        

        在组件中,我将传递的属性初始化为:

        ngOnInit(): void {    
           this.property = this.activatedRoute.snapshot.params.property;
           ...
        }
        

        运行测试时,如果您没有在模拟的 ActivatedRoute“useValue”中传递属性值,那么在使用“fixture.detectChanges()”检测更改时,您将得到未定义。这是因为 ActivatedRoute 的模拟值不包含属性 params.property。然后,模拟 useValue 需要具有这些参数,以便夹具初始化组件中的“this.property”。您可以将其添加为:

          let fixture: ComponentFixture<YourComponent>;
          let component: YourComponent;
          let activatedRoute: ActivatedRoute; 
        
          beforeEach(done => {
                TestBed.configureTestingModule({
                  declarations: [YourComponent],
                  imports: [ YourImportedModules ],
                  providers: [
                    YourRequiredServices,
                    {
                      provide: ActivatedRoute,
                      useValue: {
                        snapshot: {
                          params: {
                            property: 'yourProperty',
                            someId: someId
                          },
                          data: {
                            yourResolvedValue: { data: mockResolvedData() }
                          }
                        }
                      }
                    }
                  ]
                })
                  .compileComponents()
                  .then(() => {
                    fixture = TestBed.createComponent(YourComponent);
                    component = fixture.debugElement.componentInstance;
                    activatedRoute = TestBed.get(ActivatedRoute);
                    fixture.detectChanges();
                    done();
                  });
              });
        

        您可以开始测试,例如:

        it('should ensure property param is yourProperty', async () => {
           expect(activatedRoute.snapshot.params.property).toEqual('yourProperty');
           ....
        });
        

        现在,假设您想测试一个不同的属性值,那么您可以将模拟的 ActivatedRoute 更新为:

          it('should ensure property param is newProperty', async () => {
            activatedRoute.snapshot.params.property = 'newProperty';
            fixture = TestBed.createComponent(YourComponent);
            component = fixture.debugElement.componentInstance;
            activatedRoute = TestBed.get(ActivatedRoute);
            fixture.detectChanges();
        
            expect(activatedRoute.snapshot.params.property).toEqual('newProperty');
        });
        

        希望这会有所帮助!

        【讨论】:

        • 它正在工作,我只是不知道为什么当我只将activatedRoute.snapshot 和fixture.detect.. 放入其中时它不起作用。但是当我复制你的代码时它的工作
        • 你有什么想法,为什么我必须再次初始化它块中的夹具和组件,即使它已经在 beforeeach 中?
        【解决方案8】:

        角度 11: 将此添加到您的规范文件中

        imports: [
           RouterTestingModule.withRoutes([])
        ],
        

        这只需一行就可以帮助我,其他你需要模拟提供者

        【讨论】:

          【解决方案9】:

          在测试类中添加提供者为:

          {
            provide: ActivatedRoute,
            useValue: {
              paramMap: of({ get: v => { return { id: 123 }; } })
            } 
          }
          

          【讨论】:

            【解决方案10】:

            到目前为止,所有其他答案仅提供路由参数的值。如果您想测试路由更改触发器本身怎么办?您可以在测试中为 ActivatedRoute 提供 Subject 及其 Observable,这样您就可以使用 source.next() 触发路由更改。

            被测代码:

                constructor(private readonly route: ActivatedRoute) {}
            
                ngOnInit(): void {
                  this.routeParamSubscription = this.route.params.subscribe((params) => {
                    if (params['id']) {
                      this.loadDetails(params['id']);
                    }
                  });
                }
            

            测试代码:

                let routeChangeSource: BehaviorSubject<Params>;
                // In TestBed.configureTestingMethod
                ...
                  providers: [
                    {
                      provide: ActivatedRoute,
                      useValue: {
                        params: routeChangeSource.asObservable()
                      }
                    }
                  ]
                ...
                it('loads data on route change', fakeAsync(() => {
                  const spy = spyOn(component, 'loadDetails').and.callThrough();
                  routeChangeSource.next({ id: 99 });
                  tick();
                  expect(spy).toHaveBeenCalledOnceWith(99);
                }));
            

            这会测试路由更改后触发的操作并确保它被激活。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2016-11-03
              • 1970-01-01
              • 2014-05-29
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多