无法用 Karma 测试简单的 Angular 12 守卫
Can't test simple Angular 12 guard with Karma
我已经尝试了几次,但似乎我无法为 Angular 12 中的非常基本的 Guard 创建单元测试,其中有
- 可以激活
- canActivateChild
作为其主要方法。
请找到以下代码:
@Injectable({
providedIn: 'root'
})
export class IsAuthenticatedGuard implements CanActivate, CanActivateChild {
constructor(private authService: AuthService, private router: Router) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
return this.authService.getIsAuthenticated().pipe(
tap(isAuth => {
if (!isAuth) {
// Redirect to login
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(['/login']);
}
})
);
}
canActivateChild(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
return this.canActivate(route, state);
}
}
canActivate
方法中的 authService
调用应 return 一个由 BehaviourSubject 对象使用 asObservable()
调用获得的 Observable。
我已经尝试了所有可能的测试,但似乎没有执行比较(toBe
、toEqual
等)适用于这两种方法,执行重定向时也不会触发导航间谍.
以下是示例 spec.ts
class 我根据网络上的一些指南创建的:
function mockRouterState(url: string): RouterStateSnapshot {
return {
url
} as RouterStateSnapshot;
}
describe('IsAuthenticatedGuard', () => {
let guard: IsAuthenticatedGuard;
let authServiceStub: AuthService;
let routerSpy: jasmine.SpyObj<Router>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [SharedModule, RouterTestingModule]
});
authServiceStub = new AuthService();
routerSpy = jasmine.createSpyObj<Router>('Router', ['navigate']);
guard = new IsAuthenticatedGuard(authServiceStub, routerSpy);
});
it('should be created', () => {
expect(guard).toBeTruthy();
});
const dummyRoute = {} as ActivatedRouteSnapshot;
const mockUrls = ['/', '/dtm', '/drt', '/reporting'];
describe('when the user is logged in', () => {
beforeEach(() => {
authServiceStub.setIsAuthenticated(true);
});
mockUrls.forEach(mockUrl => {
describe('and navigates to a guarded route configuration', () => {
it('grants route access', () => {
const canActivate = guard.canActivate(dummyRoute, mockRouterState(mockUrl));
expect(canActivate).toEqual(of(true));
});
it('grants child route access', () => {
const canActivateChild = guard.canActivateChild(dummyRoute, mockRouterState(mockUrl));
expect(canActivateChild).toEqual(of(true));
});
});
});
});
describe('when the user is logged out', () => {
beforeEach(() => {
authServiceStub.setIsAuthenticated(false);
});
mockUrls.forEach(mockUrl => {
describe('and navigates to a guarded route configuration', () => {
it('does not grant route access', () => {
const canActivate = guard.canActivate(dummyRoute, mockRouterState(mockUrl));
expect(canActivate).toEqual(of(false));
});
it('does not grant child route access', () => {
const canActivateChild = guard.canActivateChild(dummyRoute, mockRouterState(mockUrl));
expect(canActivateChild).toEqual(of(false));
});
it('navigates to the login page', () => {
// eslint-disable-next-line @typescript-eslint/unbound-method
expect(routerSpy.navigate).toHaveBeenCalledWith(['/login'], jasmine.any(Object));
});
});
});
});
});
当我 运行 测试文件时,我得到这样的东西:
Expected object to have properties
_subscribe: Function
Expected object not to have properties
source: Observable({ _isScalar: false, source: BehaviorSubject({ _isScalar: false, observers: [ ], closed: false, isStopped: false, hasError: false, thrownE
rror: null, _value: false }) })
operator: MapOperator({ project: Function, thisArg: undefined })
Error: Expected object to have properties
_subscribe: Function ...
显然,Karma 需要某种 ScalarObservable,而且未检测到朝向 ['/login']
的导航。
你介意给我一些关于如何进行这个测试的建议吗?
提前谢谢你。
下面是我将如何配置 TestBed 模块和测试守卫:
describe('IsAuthenticatedGuard', () => {
const mockRouter = {
navigate: jasmine.createSpy('navigate'),
};
const authService = jasmine.createSpyObj('AuthService', ['getIsAuthenticated']);
let guard: IsAuthenticatedGuard;
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
providers: [
IsAuthenticatedGuard,
{ provide: Router, useValue: mockRouter },
{ provide: AuthService, useValue: authService },
],
}).compileComponents();
}),
);
beforeEach(() => {
guard = TestBed.inject(IsAuthenticatedGuard);
});
describe('when the user is logged in', () => {
beforeEach(() => {
authService.setIsAuthenticated.and.returnValue(of(true));
});
it('grants route access', () => {
guard.canActivate({} as ActivatedRouteSnapshot, {} as RouterStateSnapshot).subscribe((result) => {
expect(result).toBeTrue();
});
});
it('grants child route access', () => {
guard.canActivateChild({} as ActivatedRouteSnapshot, {} as RouterStateSnapshot).subscribe((result) => {
expect(result).toBeTrue();
});
});
});
});
谢谢@vitaliy。
我在守卫本身和测试文件中调整了一些东西,并设法通过了。
这是最终的测试文件:
describe('IsAuthenticatedGuard', () => {
const mockRouter = {
navigate: jasmine.createSpy('navigate')
};
const authService = jasmine.createSpyObj<AuthService>('AuthService', ['getIsAuthenticated']);
let guard: IsAuthenticatedGuard;
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
providers: [
IsAuthenticatedGuard,
{
provide: Router,
useValue: mockRouter
},
{
provide: AuthService,
useValue: authService
}
]
}).compileComponents();
})
);
beforeEach(() => {
guard = TestBed.inject(IsAuthenticatedGuard);
});
describe('when the user is logged in', () => {
beforeEach(() => {
authService.getIsAuthenticated.and.returnValue(of(true));
});
it('grants route access', () => {
void guard.canActivate({} as ActivatedRouteSnapshot, {} as RouterStateSnapshot).subscribe(result => {
expect(result).toBeTrue();
});
});
it('grants child route access', () => {
guard.canActivateChild({} as ActivatedRouteSnapshot, {} as RouterStateSnapshot).subscribe(result => {
expect(result).toBeTrue();
});
});
});
describe('when the user is logged out', () => {
beforeEach(() => {
authService.getIsAuthenticated.and.returnValue(of(false));
});
it('does not grant route access', () => {
void guard.canActivate({} as ActivatedRouteSnapshot, {} as RouterStateSnapshot).subscribe(result => {
expect(result).toBeFalse();
expect(mockRouter.navigate).toHaveBeenCalledWith(['/login']);
});
});
it('does not grant child route access', () => {
guard.canActivateChild({} as ActivatedRouteSnapshot, {} as RouterStateSnapshot).subscribe(result => {
expect(result).toBeFalse();
expect(mockRouter.navigate).toHaveBeenCalledWith(['/login']);
});
});
});
});
您不需要 mockRouter
,因为您可以在 imports
数组中添加 RouterTestingModule
并执行
const router: Router;
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
imports: [RouterTestingModule], //ADD THIS HERE
providers: [
IsAuthenticatedGuard,
{
provide: AuthService,
useValue: authService
}
]
}).compileComponents();
})
);
beforeEach(() => {
guard = TestBed.inject(IsAuthenticatedGuard);
router = TestBed.inject(Router); //ADD THIS HERE
});
当您订阅测试时,您需要添加 waitForAsync()
因为测试应该等到每个可观察对象都已执行并完成。
例如
it('does not grant child route access', waitForAsync(() => {
guard.canActivateChild({} as ActivatedRouteSnapshot, {} as RouterStateSnapshot).subscribe(result => {
expect(result).toBeFalse();
expect(mockRouter.navigate).toHaveBeenCalledWith(['/login']);
});
}));
否则可能是调用订阅之前测试已经结束,而你expect
什么都没有。
我已经尝试了几次,但似乎我无法为 Angular 12 中的非常基本的 Guard 创建单元测试,其中有
- 可以激活
- canActivateChild
作为其主要方法。 请找到以下代码:
@Injectable({
providedIn: 'root'
})
export class IsAuthenticatedGuard implements CanActivate, CanActivateChild {
constructor(private authService: AuthService, private router: Router) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
return this.authService.getIsAuthenticated().pipe(
tap(isAuth => {
if (!isAuth) {
// Redirect to login
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(['/login']);
}
})
);
}
canActivateChild(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
return this.canActivate(route, state);
}
}
canActivate
方法中的 authService
调用应 return 一个由 BehaviourSubject 对象使用 asObservable()
调用获得的 Observable。
我已经尝试了所有可能的测试,但似乎没有执行比较(toBe
、toEqual
等)适用于这两种方法,执行重定向时也不会触发导航间谍.
以下是示例 spec.ts
class 我根据网络上的一些指南创建的:
function mockRouterState(url: string): RouterStateSnapshot {
return {
url
} as RouterStateSnapshot;
}
describe('IsAuthenticatedGuard', () => {
let guard: IsAuthenticatedGuard;
let authServiceStub: AuthService;
let routerSpy: jasmine.SpyObj<Router>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [SharedModule, RouterTestingModule]
});
authServiceStub = new AuthService();
routerSpy = jasmine.createSpyObj<Router>('Router', ['navigate']);
guard = new IsAuthenticatedGuard(authServiceStub, routerSpy);
});
it('should be created', () => {
expect(guard).toBeTruthy();
});
const dummyRoute = {} as ActivatedRouteSnapshot;
const mockUrls = ['/', '/dtm', '/drt', '/reporting'];
describe('when the user is logged in', () => {
beforeEach(() => {
authServiceStub.setIsAuthenticated(true);
});
mockUrls.forEach(mockUrl => {
describe('and navigates to a guarded route configuration', () => {
it('grants route access', () => {
const canActivate = guard.canActivate(dummyRoute, mockRouterState(mockUrl));
expect(canActivate).toEqual(of(true));
});
it('grants child route access', () => {
const canActivateChild = guard.canActivateChild(dummyRoute, mockRouterState(mockUrl));
expect(canActivateChild).toEqual(of(true));
});
});
});
});
describe('when the user is logged out', () => {
beforeEach(() => {
authServiceStub.setIsAuthenticated(false);
});
mockUrls.forEach(mockUrl => {
describe('and navigates to a guarded route configuration', () => {
it('does not grant route access', () => {
const canActivate = guard.canActivate(dummyRoute, mockRouterState(mockUrl));
expect(canActivate).toEqual(of(false));
});
it('does not grant child route access', () => {
const canActivateChild = guard.canActivateChild(dummyRoute, mockRouterState(mockUrl));
expect(canActivateChild).toEqual(of(false));
});
it('navigates to the login page', () => {
// eslint-disable-next-line @typescript-eslint/unbound-method
expect(routerSpy.navigate).toHaveBeenCalledWith(['/login'], jasmine.any(Object));
});
});
});
});
});
当我 运行 测试文件时,我得到这样的东西:
Expected object to have properties _subscribe: Function Expected object not to have properties source: Observable({ _isScalar: false, source: BehaviorSubject({ _isScalar: false, observers: [ ], closed: false, isStopped: false, hasError: false, thrownE rror: null, _value: false }) }) operator: MapOperator({ project: Function, thisArg: undefined }) Error: Expected object to have properties _subscribe: Function ...
显然,Karma 需要某种 ScalarObservable,而且未检测到朝向 ['/login']
的导航。
你介意给我一些关于如何进行这个测试的建议吗?
提前谢谢你。
下面是我将如何配置 TestBed 模块和测试守卫:
describe('IsAuthenticatedGuard', () => {
const mockRouter = {
navigate: jasmine.createSpy('navigate'),
};
const authService = jasmine.createSpyObj('AuthService', ['getIsAuthenticated']);
let guard: IsAuthenticatedGuard;
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
providers: [
IsAuthenticatedGuard,
{ provide: Router, useValue: mockRouter },
{ provide: AuthService, useValue: authService },
],
}).compileComponents();
}),
);
beforeEach(() => {
guard = TestBed.inject(IsAuthenticatedGuard);
});
describe('when the user is logged in', () => {
beforeEach(() => {
authService.setIsAuthenticated.and.returnValue(of(true));
});
it('grants route access', () => {
guard.canActivate({} as ActivatedRouteSnapshot, {} as RouterStateSnapshot).subscribe((result) => {
expect(result).toBeTrue();
});
});
it('grants child route access', () => {
guard.canActivateChild({} as ActivatedRouteSnapshot, {} as RouterStateSnapshot).subscribe((result) => {
expect(result).toBeTrue();
});
});
});
});
谢谢@vitaliy。
我在守卫本身和测试文件中调整了一些东西,并设法通过了。
这是最终的测试文件:
describe('IsAuthenticatedGuard', () => {
const mockRouter = {
navigate: jasmine.createSpy('navigate')
};
const authService = jasmine.createSpyObj<AuthService>('AuthService', ['getIsAuthenticated']);
let guard: IsAuthenticatedGuard;
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
providers: [
IsAuthenticatedGuard,
{
provide: Router,
useValue: mockRouter
},
{
provide: AuthService,
useValue: authService
}
]
}).compileComponents();
})
);
beforeEach(() => {
guard = TestBed.inject(IsAuthenticatedGuard);
});
describe('when the user is logged in', () => {
beforeEach(() => {
authService.getIsAuthenticated.and.returnValue(of(true));
});
it('grants route access', () => {
void guard.canActivate({} as ActivatedRouteSnapshot, {} as RouterStateSnapshot).subscribe(result => {
expect(result).toBeTrue();
});
});
it('grants child route access', () => {
guard.canActivateChild({} as ActivatedRouteSnapshot, {} as RouterStateSnapshot).subscribe(result => {
expect(result).toBeTrue();
});
});
});
describe('when the user is logged out', () => {
beforeEach(() => {
authService.getIsAuthenticated.and.returnValue(of(false));
});
it('does not grant route access', () => {
void guard.canActivate({} as ActivatedRouteSnapshot, {} as RouterStateSnapshot).subscribe(result => {
expect(result).toBeFalse();
expect(mockRouter.navigate).toHaveBeenCalledWith(['/login']);
});
});
it('does not grant child route access', () => {
guard.canActivateChild({} as ActivatedRouteSnapshot, {} as RouterStateSnapshot).subscribe(result => {
expect(result).toBeFalse();
expect(mockRouter.navigate).toHaveBeenCalledWith(['/login']);
});
});
});
});
您不需要 mockRouter
,因为您可以在 imports
数组中添加 RouterTestingModule
并执行
const router: Router;
beforeEach(
waitForAsync(() => {
void TestBed.configureTestingModule({
imports: [RouterTestingModule], //ADD THIS HERE
providers: [
IsAuthenticatedGuard,
{
provide: AuthService,
useValue: authService
}
]
}).compileComponents();
})
);
beforeEach(() => {
guard = TestBed.inject(IsAuthenticatedGuard);
router = TestBed.inject(Router); //ADD THIS HERE
});
当您订阅测试时,您需要添加 waitForAsync()
因为测试应该等到每个可观察对象都已执行并完成。
例如
it('does not grant child route access', waitForAsync(() => {
guard.canActivateChild({} as ActivatedRouteSnapshot, {} as RouterStateSnapshot).subscribe(result => {
expect(result).toBeFalse();
expect(mockRouter.navigate).toHaveBeenCalledWith(['/login']);
});
}));
否则可能是调用订阅之前测试已经结束,而你expect
什么都没有。