简体   繁体   English

Mocking router.events.subscribe() Angular2

[英]Mocking router.events.subscribe() Angular2

In my app.component.ts I have the following ngOnInit function:在我的 app.component.ts 中,我有以下 ngOnInit function:

ngOnInit() {
    this.sub = this.router.events.subscribe(e => {
      if (e instanceof NavigationEnd) {
        if (!e.url.includes('login')) {
          this.loggedIn = true;
        } else {
          this.loggedIn = false;
        }
      }
    });
  }

Currently I'm testing if the sub is not null but I want to test the function with a 100% coverage.目前我正在测试 sub 是否不是 null,但我想测试 function 的覆盖率为 100%。

I want to mock the router object so that I can simulate the URL and then test if the this.loggedIn is correctly set.我想模拟路由器 object 以便我可以模拟 URL 然后测试 this.loggedIn 是否设置正确。

How would I proceed to mock this function?我将如何继续模拟这个 function? I tried it but I don't know how I would take this on with the callback involved and with the NavigationEnd.我试过了,但我不知道如何处理涉及的回调和 NavigationEnd。

I have found the answer, if someone is looking for it:我找到了答案,如果有人正在寻找它:

import { NavigationEnd } from '@angular/router';
import { Observable } from 'rxjs/Observable';

class MockRouter {
  public ne = new NavigationEnd(0, 'http://localhost:4200/login', 'http://localhost:4200/login');
  public events = new Observable(observer => {
    observer.next(this.ne);
    observer.complete();
  });
}

class MockRouterNoLogin {
  public ne = new NavigationEnd(0, 'http://localhost:4200/dashboard', 'http://localhost:4200/dashboard');
  public events = new Observable(observer => {
    observer.next(this.ne);
    observer.complete();
  });
}

I created a version of the router stub from Angular docs that uses this method to implement NavigationEnd event for testing:我从 Angular 文档创建了一个路由器存根版本,它使用此方法实现 NavigationEnd 事件以进行测试:

 import {Injectable} from '@angular/core'; import { NavigationEnd } from '@angular/router'; import {Subject} from "rxjs"; @Injectable() export class RouterStub { public url; private subject = new Subject(); public events = this.subject.asObservable(); navigate(url: string) { this.url = url; this.triggerNavEvents(url); } triggerNavEvents(url) { let ne = new NavigationEnd(0, url, null); this.subject.next(ne); } }

The accepted answer is correct but this is a bit simpler, you can replace接受的答案是正确的,但这有点简单,您可以替换

public ne = new NavigationEnd(0, 'http://localhost:4200/login', 'http://localhost:4200/login');
public events = new Observable(observer => {
    observer.next(this.ne);
    observer.complete();
  });

by:经过:

public events = Observable.of( new NavigationEnd(0, 'http://localhost:4200/login', 'http://localhost:4200/login'));

And find below a full test file to test the function in the question:并在下面找到一个完整的测试文件来测试问题中的功能:

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

/**
 * Load the implementations that should be tested
 */
import { AppComponent } from './app.component';

import { NavigationEnd, Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';


class MockServices {
  // Router
  public events = Observable.of( new NavigationEnd(0, 'http://localhost:4200/login', 'http://localhost:4200/login'));
}

describe(`App`, () => {
  let comp: AppComponent;
  let fixture: ComponentFixture<AppComponent>;
  let router: Router;

  /**
   * async beforeEach
   */
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ AppComponent ],
      schemas: [NO_ERRORS_SCHEMA],
      providers: [
        { provide: Router, useClass: MockServices },
      ]
    })
    /**
     * Compile template and css
     */
    .compileComponents();
  }));

  /**
   * Synchronous beforeEach
   */
  beforeEach(() => {
    fixture = TestBed.createComponent(AppComponent);
    comp    = fixture.componentInstance;

    router = fixture.debugElement.injector.get( Router);

    /**
     * Trigger initial data binding
     */
    fixture.detectChanges();
  });

  it(`should be readly initialized`, () => {
    expect(fixture).toBeDefined();
    expect(comp).toBeDefined();
  });

  it('ngOnInit() - test that this.loggedIn is initialised correctly', () => {
    expect(comp.loggedIn).toEqual(true);
  });

});

This is a really old question but I just came across it looking for something better than what i have and in my case i need to test several different events.这是一个非常古老的问题,但我只是在寻找比我拥有的更好的东西时遇到它,就我而言,我需要测试几个不同的事件。 My basic approach was just to change the Router.events to a non read only value like我的基本方法是将 Router.events 更改为非只读值,例如

 (router as any).events = new BehaviorSubject<any>(null);
 fixture.detectChanges();
 router.events.next(new NavigationEnd(0, 'http://localhost:4200/login', 
 'http://localhost:4200/login'));
  expect(comp.loggedIn).toEqual(true);

Hope maybe that helps someone.希望这可能对某人有所帮助。 I couldn't find an easier solution after looking around环顾四周后,我找不到更简单的解决方案

  • use ReplaySubject<RouterEvent> to mock the router.events使用ReplaySubject<RouterEvent>模拟router.events
  • export it to a service so that you can test it more independently from the component, and other components may also want to use this service ;)将其导出到服务,以便您可以更独立于组件对其进行测试,并且其他组件可能也希望使用此服务;)
  • use filter instead of instanceof使用filter代替instanceof
  • then add the tests for the component, I think that will be trivial right :)然后添加组件的测试,我认为这将是微不足道的:)

source code源代码

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

test code测试代码

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 routerEventReplaySubject: ReplaySubject<RouterEvent>;
  let routerMock;

  beforeEach(() => {
    routerEventReplaySubject = new ReplaySubject<RouterEvent>(1);
    routerMock = {
      events: routerEventReplaySubject.asObservable()
    };

    TestBed.configureTestingModule({
      providers: [
        {provide: Router, useValue: routerMock}
      ]
    });    
    service = TestBed.inject(RouteEventService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });

  describe('subscribeToEventUrl should return route equals to mock url on firing', () => {
    it('NavigationEnd', () => {
      const result: Observable<string> = service.subscribeToRouterEventUrl();
      const url = '/mock';

      result.subscribe((route: string) => {
        expect(route).toEqual(url);
      });

      routerEventReplaySubject.next(new NavigationEnd(1, url, 'redirectUrl'));
    });

    it('NavigationStart', () => {
      const result: Observable<string> = service.subscribeToRouterEventUrl();
      const url = '/mock';

      result.subscribe((route: string) => {
        expect(route).toBeNull();
      });

      routerEventReplaySubject.next(new NavigationStart(1, url, 'imperative', null));
    });
  });
});

The previous example public events = Observable.of( new NavigationEnd(0, 'http://localhost..'));前面的例子public events = Observable.of( new NavigationEnd(0, 'http://localhost..')); does not seem to work according to Karma which complains about:根据 Karma 的抱怨,它似乎不起作用:

Failed: undefined is not an object (evaluating 'router.routerState.root') rootRoute@ http://localhost:9876/_karma_webpack_/vendor.bundle.js失败:未定义不是对象(评估'router.routerState.root')rootRoute@ http://localhost:9876/_karma_webpack_/vendor.bundle.js

Despite (mocked) Router instance events ' subscription callback has been running successfully in ngOninit() of original app.component.ts, ie main application component under testing by Karma:尽管(模拟)路由器实例事件的订阅回调已在原始 app.component.ts 的ngOninit()中成功运行,即 Karma 正在测试的主要应用程序组件:

this.sub = this.router.events.subscribe(e => { // successful execution across Karma

Indeed, the way Router has been mocked sort of looks incomplete, inaccurate as a structure from Karma's prospective: because of router.routerState that turns out to be undefined at run time.事实上,Router 被嘲笑的方式看起来有点不完整,从 Karma 的预期来看是不准确的结构:因为router.routerState在运行时被证明是未定义的。

Here is how Angular Router has been "stubbed" exactly on my side, including RoutesRecognized events articifically baked as Observable s in my case:以下是 Angular Router 是如何在我这边被“存根”的,包括RoutesRecognized事件,在我的例子中被人为地烘焙Observable

class MockRouter {
    public events = Observable.of(new RoutesRecognized(2 , '/', '/',
                                  createRouterStateSnapshot()));
}

const createRouterStateSnapshot = function () {
    const routerStateSnapshot = jasmine.createSpyObj('RouterStateSnapshot', 
                                                     ['toString', 'root']);
    routerStateSnapshot.root = jasmine.createSpyObj('root', ['firstChild']);
    routerStateSnapshot.root.firstChild.data = {
        xxx: false
    };
    return <RouterStateSnapshot>routerStateSnapshot;
};

to fit what ngOnInit() body expects, requiring RoutesRecognized event with deep structure:为了满足ngOnInit()主体的期望,需要具有深层结构的RoutesRecognized事件

ngOnInit() {
   this.router.events.filter((event) => {
        return event instanceof RoutesRecognized;
    }).subscribe((event: RoutesRecognized) => {
        // if (!event.state.root.firstChild.data.xxx) {
        // RoutesRecognized event... to be baked from specs mocking strategy
   });
}

Recap / summary of my <package.json> content:回顾/总结我的 <package.json> 内容:

angular/router: 5.2.9, karma: 2.0.2, jasmine-core: 2.6.4, karma-jasmine: 1.1.2角度/路由器:5.2.9,业力:2.0.2,茉莉花核:2.6.4,业力-茉莉花:1.1.2

The Angular Testing Documentation shows how to do this using a Jasmine spy: Angular 测试文档展示了如何使用 Jasmine 间谍来做到这一点:

const routerSpy = jasmine.createSpyObj('Router', ['navigateByUrl']);
const heroServiceSpy = jasmine.createSpyObj('HeroService', ['getHeroes']);

TestBed.configureTestingModule({
  providers: [
    { provide: HeroService, useValue: heroServiceSpy },
    { provide: Router,      useValue: routerSpy }
  ]
})

... ...

it('should tell ROUTER to navigate when hero clicked', () => {

  heroClick(); // trigger click on first inner <div class="hero">

  // args passed to router.navigateByUrl() spy
  const spy = router.navigateByUrl as jasmine.Spy;
  const navArgs = spy.calls.first().args[0];

  // expecting to navigate to id of the component's first hero
  const id = comp.heroes[0].id;
  expect(navArgs).toBe('/heroes/' + id,
    'should nav to HeroDetail for first hero');
});

The easiest method is probably this:最简单的方法可能是这样的:

(router.events as any) = new BehaviorSubject(
      new NavigationEnd(0, 'http://localhost:4200/plain', `http://localhost:4200/plain`)
    );

If you are using Jest for testing and you are detecting the NavigationEnd event of the Router in ngOnInit() of a component, make sure to run the test in the waitForAsync() test method.如果您使用Jest进行测试,并且在组件的ngOnInit()中检测到RouterNavigationEnd事件,请确保在waitForAsync()测试方法中运行测试。

Here is an example:这是一个例子:

export class CatchPageComponent implements OnInit {
  constructor(private myService: MyService, private router: Router) {}

  ngOnInit(): void {
    this.router.events
      .pipe(
        filter((e): e is NavigationEnd => e instanceof NavigationEnd),
        take(1) //needed to prevent infinite loop since we are triggering more navigation events in the subscription
      )
      .subscribe((navEnd) => {
        const url = new URL(navEnd.urlAfterRedirects, 'http://localhost:8080'); //base is not relevant, just to be able to create URL to parse
        const path = url.pathname;
        if ('/fancy' === path) {
          this.myService.myFunction()
        } else {
          this.router.navigate(['plain']);
        }
      });
  }
}
describe('CatchPageComponent', () => {
  let component: CatchPageComponent;
  let fixture: ComponentFixture<CatchPageComponent>;
  let myService: MyService;
  let router: Router;

beforeEach(async () => {
    await TestBed.configureTestingModule({
      providers: [
        MyService,
      ],
      imports: [
        CatchPageModule,
        RouterTestingModule,
      ],
      declarations: [CatchPageComponent],
    }).compileComponents();

    fixture = TestBed.createComponent(CatchPageComponent);
    router = TestBed.inject(Router);
    myService = TestBed.inject(MyService);
    component = fixture.componentInstance;

    fixture.detectChanges();
  });

  it('should call myService when route is fancy', waitForAsync(() => {
   //simple mock of the NavigationEnd event
    (router.events as any) = new BehaviorSubject(
      new NavigationEnd(0, 'http://localhost:4200/fancy', `http://localhost:4200/fancy`)
    );

    const myFunctionSpy = jest.spyOn(myService, 'myFunction');

    component.ngOnInit();

    expect(myFunctionSpy).toHaveBeenCalledTimes(1);
  }));
});

EDIT 1:编辑 1:

So I have realized this.router.navigate(['plain']);所以我已经意识到this.router.navigate(['plain']); kept the second test hanging.保持第二次测试挂起。 If you need it in your implementation, you should mock it either in the beforeEach() method or in a specific test like this jest.spyOn(router, 'navigate').mockImplementation(() => Promise.resolve(true));如果您在实现中需要它,您应该在beforeEach()方法或像这样的特定测试中模拟它jest.spyOn(router, 'navigate').mockImplementation(() => Promise.resolve(true));

Here it is:这里是:

  it('should redirect to plain page', waitForAsync(() => {
    jest.spyOn(router, 'navigate').mockImplementation(() => Promise.resolve(true));  //mocking navigate

    (router.events as any) = new BehaviorSubject(
      new NavigationEnd(0, 'http://localhost:4200/plain', `http://localhost:4200/plain`)
    );

    component.ngOnInit();

    expect(router.navigate).toHaveBeenCalledWith(['plain']);
  }));

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM