Angular - 测试 Routerguard

标签 angular jasmine karma-jasmine angular-routing angular-test

我目前正在努力对 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,这意味着测试已经获得了 auth 服务的实例,但没有 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,你可以做这个技巧 - 在 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 });
  // ...

关于Angular - 测试 Routerguard,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52164199/

相关文章:

angular - 如何在 angular4 单元测试中忽略第三方组件的 html 标签?

angular - Internet Explorer 11 - 对象不支持属性或方法 'includes'

arrays - Angular 2 pipe - 计算对象数组的摘要

javascript - jasmine.js expect() 在异步回调中不起作用

angularjs - $HttpBackend (AngularJS) 出现意外请求错误

Angular/Jasmine - 如果在 ngOnInit 上调用,Spies 是否工作?

angular - 在 angular 2 中使用 store (ngrx) 有什么好处

html - 如何在angular2的两个不同页面中有不同的背景图片?

javascript - 如何修改失败的 Jasmine 测试的标题以在每个描述标题后包含分隔符?

typescript - Ionic + Jasmine + Tslint - 类型上不存在属性 'and'