【问题标题】:Angular 6 - how to mock router.events url response in unit testAngular 6 - 如何在单元测试中模拟 router.events url 响应
【发布时间】:2019-03-24 20:35:48
【问题描述】:

如标题所示,我需要在单元测试中模拟router.events

在我的组件中,我做了一些正则表达式来获取 url 中斜线之间的第一个文本实例;例如,/pdp/

  constructor(
    private route: ActivatedRoute,
    private router: Router,
  }

this.router.events.pipe(takeUntil(this.ngUnsubscribe$))
      .subscribe(route => {
        if (route instanceof NavigationEnd) {
        // debugger
          this.projectType = route.url.match(/[a-z-]+/)[0];
        }
      });

构建组件时我的单元测试错误:Cannot read property '0' of null。当我在调试器中注释时,route 似乎没有正确设置,但我在单元测试本身中设置它。我已经用几种不同的方式完成了(受这篇文章的启发:Mocking router.events.subscribe() Angular2 和其他人)。

第一次尝试:

providers: [
        {
          provide: Router,
          useValue: {
            events: of(new NavigationEnd(0, '/pdp/project-details/4/edit', 'pdp/project-details/4/edit'))
          }
        },
        // ActivatedRoute also being mocked
        {
          provide: ActivatedRoute,
          useValue: {
            snapshot: { url: [{ path: 'new' }, { path: 'edit' }] },
            parent: {
              parent: {
                snapshot: {
                  url: [
                    { path: 'pdp' }
                  ]
                }
              }
            }
          }
        }
]

第二次尝试(基于上述帖子):

class MockRouter {
  public events = of(new NavigationEnd(0, '/pdp/project-details/4/edit', '/pdp/project-details/4/edit'))
}

providers: [
        {
          provide: Router,
          useClass: MockRouter
        }
]

第三次尝试(同样基于上述帖子):

class MockRouter {
  public ne = new NavigationEnd(0, '/pdp/project-details/4/edit', '/pdp/project-details/4/edit');
  public events = new Observable(observer => {
    observer.next(this.ne);
    observer.complete();
  });
}

providers: [
        {
          provide: Router,
          useClass: MockRouter
        }
]

第四次尝试:

beforeEach(() => {
    spyOn((<any>component).router, 'events').and.returnValue(of(new NavigationEnd(0, '/pdp/project-details/4/edit', 'pdp/project-details/4/edit')))
...

第五次尝试:

beforeEach(() => {
    spyOn(TestBed.get(Router), 'events').and.returnValue(of({ url:'/pdp/project-details/4/edit' }))
...

在上述所有情况下,route 都没有被设置; NavigationEnd 对象等于:

{ id: 1, url: "/", urlAfterRedirects: "/" }

想法?

【问题讨论】:

    标签: angular typescript unit-testing


    【解决方案1】:

    可以这么简单:

    TestBed.configureTestingModule({
      imports: [RouterTestingModule]
    }).compileComponents()
        
    ...
        
    const event = new NavigationEnd(42, '/', '/');
    (TestBed.inject(Router).events as Subject<Event>).next(event);
    

    【讨论】:

    • TS2339 属性 'next' 不存在于类型 'Observable'
    • @RichardCollette:您在执行测试时将其视为衬里错误还是实际错误?您正在运行哪个 Angular 版本? (如果出现 liniting 错误,可以忽略(转换为 any
    • 这是一个打字稿编译器错误,而不是 linting 错误。虽然返回的 Router.events 类型可以实现 Observer 和 Observable 接口,但 events 属性的公共接口是 Observable ,它不公开 next()。如果将来返回的对象不是 Observer,则将来存在重大更改的风险。使用 Angular 7、TS 3.2.4
    • @RichardCollette 对于未来发生重大变化的风险,您是对的。关于编译错误:我无法理解 - TestBed.get 方法的返回类型为 any,因此不能进行任何编译时检查(除非您专门将结果转换为 Router)..
    • 在我的案例中,组件中的订阅确实被这段代码触发,但是,我的测试不等待订阅解决,fixture.whenStable 永远不会解决,我尝试了一切异步,fakeAsync 等 ...
    【解决方案2】:

    这是我想出的答案。我认为我只是没有将第一种方法(以上)扩展得足够远:

    import { of } from 'rxjs';
    import { NavigationEnd, Router } from '@angular/router';
    
    providers: [
        {
          provide: Router,
          useValue: {
            url: '/non-pdp/phases/8',
            events: of(new NavigationEnd(0, 'http://localhost:4200/#/non-pdp/phases/8', 'http://localhost:4200/#/non-pdp/phases/8')),
            navigate: jasmine.createSpy('navigate')
          }
        }
    ]
    

    【讨论】:

    • 但是这个路由器不能导航,所以你不能测试例如导航到“”应该重定向到/home的场景
    【解决方案3】:

    认为这可能会有所帮助...在模板中有一个带有 routerLinks 的页脚组件。正在获取未提供根的错误....必须使用“真实”路由器...但有一个间谍变量..并将 router.events 指向我的测试 observable 以便我可以控制导航事件

    export type Spied<T> = { [Method in keyof T]: jasmine.Spy };
    
    describe("FooterComponent", () => {
       let component: FooterComponent;
       let fixture: ComponentFixture<FooterComponent>;
       let de: DebugElement;
       const routerEvent$ = new BehaviorSubject<RouterEvent>(null);
       let router: Spied<Router>;
    
     beforeEach(async(() => {
        TestBed.configureTestingModule({
            providers: [provideMock(SomeService), provideMock(SomeOtherService)],
            declarations: [FooterComponent],
            imports: [RouterTestingModule]
        }).compileComponents();
    
        router = TestBed.get(Router);
        (<any>router).events = routerEvent$.asObservable();
    
        fixture = TestBed.createComponent(FooterComponent);
        component = fixture.componentInstance;
        de = fixture.debugElement;
    }));
    // component subscribes to router nav event...checking url to hide/show a link
    it("should return false for showSomeLink()", () => {
        routerEvent$.next(new NavigationEnd(1, "/someroute", "/"));
        component.ngOnInit();
        expect(component.showSomeLink()).toBeFalsy();
        fixture.detectChanges();
        const htmlComponent = de.query(By.css("#someLinkId"));
        expect(htmlComponent).toBeNull();
    });
    

    【讨论】:

    • 这行得通,但是连同这个解决方案,我所有基于 Router.navigate(...) 的测试都失败了。你怎么能同时测试 Router.navigate 和 router.event.subscribe() ?
    【解决方案4】:

    我看到了这个替代方案,它适用于 RouterTestingModule,就像 @daniel-sc 的回答一样

       const events = new Subject<{}>();
       const router = TestBed.get(Router);
       spyOn(router.events, 'pipe').and.returnValue(events);
       events.next('Result of pipe');
    

    【讨论】:

    • get() 已弃用。从 v9.0.0 开始,推荐使用 inject()。
    【解决方案5】:

    希望这会有所帮助,这就是我所做的:

    const eventsStub = new BehaviorSubject<Event>(null);
        export class RouterStub {
          events = eventsStub;
        }
    
        beforeEach(async(() => {
            TestBed.configureTestingModule({
              declarations: [AppComponent],
              imports: [RouterTestingModule],
              providers: [
                { provide: HttpClient, useValue: {} },
                { provide: Router, useClass: RouterStub },
                Store,
                CybermetrieCentralizedService,
                TileMenuComponentService,
                HttpErrorService
              ],
              schemas: [NO_ERRORS_SCHEMA]
            }).compileComponents();
            fixture = TestBed.createComponent(AppComponent);
            router = TestBed.get(Router);
            tileMenuService = TestBed.get(TileMenuComponentService);
            component = fixture.componentInstance;
          }));
    

    然后在测试中我这样做了:

         it('should close the menu when navigation to dashboard ends', async(() => {
            spyOn(tileMenuService.menuOpened, 'next');
            component.checkRouterEvents();
            router.events.next(new NavigationEnd (1, '/', '/'));
            expect(tileMenuService.menuOpened.next).toHaveBeenCalledWith(false);
          }));
    

    这是为了验证在导航到路由“/”后我执行了预期的指令

    【讨论】:

      【解决方案6】:
      • 使用ReplaySubject&lt;RouterEvent&gt;模拟router.events
      • 将其导出到服务中,以便您可以更独立于组件对其进行测试,并且其他组件可能也希望使用此服务;)
      • 使用filter 而不是instanceof

      源代码

      import {Injectable} from '@angular/core';
      import {NavigationEnd, Router, RouterEvent} from '@angular/router';
      import {filter, map} from 'rxjs/operators';
      import {Observable} from 'rxjs';
      
      @Injectable({
        providedIn: 'root'
      })
      export class RouteEventService {
        constructor(private router: Router) {
        }
      
        subscribeToRouterEventUrl(): Observable<string> {
          return this.router.events
            .pipe(
              filter(event => event instanceof NavigationEnd),
              map((event: RouterEvent) => event.url)
            );
        }
      }
      
      

      测试代码

      import {TestBed} from '@angular/core/testing';
      
      import {RouteEventService} from './route-event.service';
      import {NavigationEnd, NavigationStart, Router, RouterEvent} from '@angular/router';
      import {Observable, ReplaySubject} from 'rxjs';
      
      describe('RouteEventService', () => {
        let service: RouteEventService;
        let routerEventRelaySubject: ReplaySubject<RouterEvent>;
        let routerMock;
      
        beforeEach(() => {
          routerEventRelaySubject = new ReplaySubject<RouterEvent>(1);
          routerMock = {
            events: routerEventRelaySubject.asObservable()
          };
      
          TestBed.configureTestingModule({
            providers: [
              {provide: Router, useValue: routerMock}
            ]
          });
          service = TestBed.inject(RouteEventService);
        });
      
        it('should be created', () => {
          expect(service).toBeTruthy();
        });
      
        describe('subscribeToEventUrl should', () => {
          it('return route equals to mock url on firing NavigationEnd', () => {
            const result: Observable<string> = service.subscribeToRouterEventUrl();
            const url = '/mock';
      
            result.subscribe((route: string) => {
              expect(route).toEqual(url);
            });
      
            routerEventRelaySubject.next(new NavigationEnd(1, url, 'redirectUrl'));
          });
      
          it('return route equals to mock url on firing NavigationStart', () => {
            const result: Observable<string> = service.subscribeToRouterEventUrl();
            const url = '/mock';
      
            result.subscribe((route: string) => {
              expect(route).toBeNull();
            });
      
            routerEventRelaySubject.next(new NavigationStart(1, url, 'imperative', null));
          });
        });
      });
      
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2021-05-27
        • 2021-10-04
        • 2018-11-24
        • 1970-01-01
        • 2021-09-18
        • 1970-01-01
        • 2020-11-19
        • 1970-01-01
        相关资源
        最近更新 更多