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
方法之前初始化它很重要。
第二个问题——因为你的守卫代码是异步的,你必须异步编写你的单元测试(你可以使用done
、sync
或fakeAsync&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 });
// ...
我目前正在努力对 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
方法之前初始化它很重要。
第二个问题——因为你的守卫代码是异步的,你必须异步编写你的单元测试(你可以使用done
、sync
或fakeAsync&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 });
// ...