Angular - 测试 Routerguard

Angular - Testing Routerguard

我目前正在努力对 Routerguard 服务中的 canActivate() 方法进行单元测试。该服务如下所示:

import {Injectable} from '@angular/core';
import {ActivatedRouteSnapshot, CanActivate, Router} from '@angular/router';
import {AuthService} from '../../auth/auth.service';
import {Observable, of} from 'rxjs';
import {NotificationService} from '../../../../shared/services/notification.service';
import {concatMap, map, take, tap} from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class ProfileGuard implements CanActivate {

  constructor(private auth: AuthService, private router: Router,
              private notification: NotificationService) {
  }

  canActivate(next: ActivatedRouteSnapshot): Observable<boolean> {
    // checks for user if not - page not found
    return this.auth.getUserEntity(next.params.uid).pipe(concatMap(user => {
      if (user) {
        // checks for permission if not - redirect to user overview
          return this.auth.currentUser.pipe(
            take(1),
            map(current => this.auth.canEditProfile(current, next.params)),
            tap(canEdit => {
              if (!canEdit) {
                this.router.navigate([`/profile/${next.params.uid}`]).then(() =>
                  this.notification.danger('Access denied. Must have permission to edit profile.'));
              }
            })
          );
      } else {
        this.router.navigate(['/page-not-found']);
        return of(false);
      }
    }));
  }
}

实际上看起来比实际要复杂: 第一个观察者检查数据库中是否有用户将 params 值作为唯一标识符。然后第二个观察者检查编辑该用户的权限。现在关于单元测试部分:

describe('RouterGuardService', () => {

  const routerStub: Router = jasmine.createSpyObj('Router', ['navigate']);
  const authStub: AuthService = jasmine.createSpyObj('AuthService', ['getUserEntity', 'currentUser', 'canEditProfile']);
  const notificationStub: NotificationService = jasmine.createSpyObj('NotificationService', ['danger']);

  function createInputRoute(url: string): ActivatedRouteSnapshot {
    const route: ActivatedRouteSnapshot = new ActivatedRouteSnapshot();
    const urlSegs: UrlSegment[] = [];
    urlSegs.push(new UrlSegment(url, {}));
    route.url = urlSegs;
    route.params = {
      uid: url.replace('/profile/', '')
        .replace('/edit', '')
    };
    return route;
  }

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        {provide: AuthService, useValue: authStub},
        {provide: Router, useValue: routerStub},
        {provide: NotificationService, useValue: notificationStub},
        ProfileGuard]
    });
  });

 it('should redirect to user overview - if has not permission', inject([ProfileGuard], (service: ProfileGuard) => {
  (<jasmine.Spy>authStub.canEditProfile).and.returnValue(false);
  authStub.currentUser = of(<any>{uid: 'jdkffdjjfdkls', role: Role.USER});
  (<jasmine.Spy>authStub.getUserEntity).and.returnValue(of({uid: 'jdkffdjjfdkls', role: Role.USER}));

  const spy = (<jasmine.Spy>routerStub.navigate).and.stub();
  const notifySpy = (<jasmine.Spy>notificationStub.danger).and.stub();

  const url: ActivatedRouteSnapshot = createInputRoute('/profile/BBB/edit');
  service.canActivate(url).subscribe(res => {
    console.log(res);
    expect(spy).toHaveBeenCalledWith(['/BBB']);
    expect(notifySpy).toHaveBeenCalledWith('Access denied. Must have permission to edit profile.');
    expect(res).toBe(false);
  }, err => console.log(err));
}));
});

但是我的测试没有检查我的预期方法,而是控制台记录了错误。也许有人可以帮助我吗?

第一个 问题 - 当您创建 authStub:

const authStub: AuthService = jasmine.createSpyObj('AuthService', ['getUserEntity', 'currentUser', 'canEditProfile']);

在这种情况下,您添加 currentUser 作为方法而不是 属性。 create jasmine spyObj both with methods and properties的正确方法:

  const authStub = {
    ...jasmine.createSpyObj('authStub', ['getUserEntity', 'canEditProfile']),
    currentUser: of(<any>{ uid: 'jdkffdjjfdkls', role: Role.USER })
  } as jasmine.SpyObj<AuthService>;

注意,在你的例子中 - 测试中的这个对象突变不会影响任何东西:

authStub.currentUser = of(<any>{uid: 'jdkffdjjfdkls', role: Role.USER});

原因是您在向 TestBed 提供服务时使用了 useValue,这意味着测试已经获得了没有 currentUser 的授权服务实例属性。这就是为什么在 运行 configureTestingModule 方法之前初始化它很重要。

第二个问题——因为你的守卫代码是异步的,你必须异步编写你的单元测试(你可以使用donesyncfakeAsync&tick).

这是最终的解决方案:

describe('RouterGuardService', () => {
  const routerStub: Router = jasmine.createSpyObj('Router', ['navigate']);

  const authStub = {
    ...jasmine.createSpyObj('authStub', ['getUserEntity', 'canEditProfile']),
    currentUser: of(<any>{ uid: 'jdkffdjjfdkls', role: Role.USER })
  } as jasmine.SpyObj<AuthService>;

  const notificationStub: NotificationService = jasmine.createSpyObj('NotificationService', ['danger']);

  let profileGuardService: ProfileGuard;

  function createInputRoute(url: string): ActivatedRouteSnapshot {
    // ...
  }

  beforeEach(() => {
    TestBed.configureTestingModule({
      // ...
    });
    profileGuardService = TestBed.get(ProfileGuard);
  });

  it('should redirect to user overview - if has not permission', fakeAsync(() => {
    (<jasmine.Spy>authStub.canEditProfile).and.returnValue(false);
    (<jasmine.Spy>authStub.getUserEntity).and.returnValue(of({ uid: 'jdkffdjjfdkls', role: Role.USER }));

    const spy = (<jasmine.Spy>routerStub.navigate).and.callFake(() => Promise.resolve());
    const notifySpy = (<jasmine.Spy>notificationStub.danger).and.stub();

    const url: ActivatedRouteSnapshot = createInputRoute('/profile/BBB/edit');
    let expectedRes;
    profileGuardService.canActivate(url).subscribe(res => {
      expectedRes = res;
    }, err => console.log(err));

    tick();
    expect(spy).toHaveBeenCalledWith(['/profile/BBB']);
    expect(notifySpy).toHaveBeenCalledWith('Access denied. Must have permission to edit profile.');
    expect(expectedRes).toBe(false);
  }));
});

如果你想为每个测试动态设置不同的 currentUser-s,你可以做这个技巧 - 使用 BehaviorSubject 在 authStub 中初始化 currentUser 属性:

const authStub = {
  ...jasmine.createSpyObj('authStub', ['getUserEntity', 'canEditProfile']),
  currentUser: new BehaviorSubject({})
} as jasmine.SpyObj<AuthService>;

然后在单元测试内部你可以调用 next 方法来设置必要的当前用户模拟:

it('should redirect to user overview - if has not permission', fakeAsync(() => {
  (<BehaviorSubject<any>>authStub.currentUser).next(<any>{ uid: 'jdkffdjjfdkls', role: Role.USER });
  // ...