【问题标题】:Can't test simple Angular 12 guard with Karma无法使用 Karma 测试简单的 Angular 12 防护
【发布时间】:2022-01-04 09:11:25
【问题描述】:

我已经尝试了好几次,但似乎我无法为 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 调用将返回一个由 BehaviourSubject 对象使用asObservable() 调用获得的 Observable。 我已经尝试了所有可能的测试,但似乎没有执行比较(toBetoEqual 等)适用于这两种方法,执行重定向时也不会触发导航间谍。

以下是我根据网络上的一些指南创建的spec.ts 类示例:

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

当我运行测试文件时,我会得到如下信息:

预期对象具有属性 _subscribe:函数 预期对象不具有属性 来源:Observable({ _isScalar:假,来源:BehaviorSubject({_isScalar:假,观察者:[],关闭:假,isStopped:假,hasError:假,抛出E 错误:null,_value:false }) }) 运算符:MapOperator({项目:函数,thisArg:未定义}) 错误:预期对象具有属性 _subscribe:函数...

显然,Karma 需要某种 ScalarObservable,而且未检测到指向 ['/login'] 的导航。

您能否就如何执行此测试给我一些建议?

提前谢谢你。

【问题讨论】:

  • 你可以提供``TestBed.configureTestingModule({ imports: [SharedModule, RouterTestingModule], providers: [...] }) 的providers-array 中的所有服务; ``` 然后你可以把守卫从TestBed中拿出来并使用waitForAsync让angular等待所有异步操作在这个测试中完成。
  • 你的常量 canActivatecanActivateChild 也是 Observables,所以你不能在 expect 中使用它们。相反,您应该订阅它们并在那里测试结果。 guard.canActivate(dummyRoute, mockRouterState(mockUrl)).subscribe((result) =&gt; expect(result).toBeTrue())
  • @FabianGosebrink 你介意在我的代码中直接给我你的答案样本吗?
  • @vitaliykotov 每当我尝试在那里输入 subscribe 子句时,IntelliJ 都会抱怨它说它不能在那里调用。
  • 这是因为您为 canActivatecanActivateChild 设置的返回类型。如果您的AuthService.getIsAuthenticated 返回Observable&lt;boolean&gt;,那么这些方法也应该具有这样的类型,因为它们只是返回对身份验证服务的调用

标签: angular typescript karma-jasmine


【解决方案1】:

这是我将如何配置 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();
      });
    });
  });
});

【讨论】:

    【解决方案2】:

    谢谢你,@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']);
          });
        });
      });
    });
    

    【讨论】:

      【解决方案3】:

      您不需要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(),因为测试应该等到每个 observable 都已执行并完成。

      例如

      it('does not grant child route access', waitForAsync(() => {
            guard.canActivateChild({} as ActivatedRouteSnapshot, {} as RouterStateSnapshot).subscribe(result => {
              expect(result).toBeFalse();
              expect(mockRouter.navigate).toHaveBeenCalledWith(['/login']);
            });
          }));
      

      否则可能在调用subscribe之前测试已经完成,而你expect什么都没有。

      【讨论】:

        猜你喜欢
        • 2016-10-13
        • 1970-01-01
        • 1970-01-01
        • 2018-01-24
        • 2018-10-25
        • 2020-11-16
        • 2018-12-05
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多