Angular 路由守卫单元测试

344 阅读5分钟

在Angular应用开发中,路由守卫是一种非常有用的机制,它允许开发者控制路由的激活和导航。单元测试是确保代码质量的重要手段,对于路由守卫的测试尤为重要,因为它们直接影响用户的导航体验。本文将通过分析SafeChangeGuard守卫的单元测试,详细介绍如何在Angular中进行路由守卫的单元测试。

1. 路由守卫概述

路由守卫是Angular路由系统中的一个接口,用于在路由激活之前或之后执行自定义逻辑。Angular提供了几种类型的守卫,如CanActivateCanActivateChildCanDeactivate等。SafeChangeGuard是一个自定义的CanDeactivate守卫,用于在用户离开当前页面时进行二次确认。

2. 路由守卫单元测试的坑点

坑点在于:如何模拟路由行为。

解决方法也很简单:我们测试的是路由守卫本身,而不是整个路由跳转的全过程,这就意味着我们只需要保证给路由守卫实例一定的输入,如果它返回符合期望的输出,那么测试就通过了。如果想太多(不要想如何通过改变 url 触发此路由守卫)就会不知如何下手。

  1. mock 组件,作为路由守卫的载体
@Component({
  selector: 'app-guard',
  template: `
      <div></div>
    `,
})
class MockComponent {
  needsDoubleCheck: boolean = false;
}

MockComponent 中有个属性为 needsDoubleCheck 表示离开此组件的时候是否需要二次确认,这个属性会被路由守卫获取并根据其值做出不同的行为。因此在 mock 的时候我们配合待测守卫将 component 做成上述样子。

  1. 在每一次测试之前配置编译环境 配置环境的时候需要注意,我们的路由守卫是配置在 providers 中的。
TestBed.configureTestingModule({
  declarations: [MockComponent],
  providers: [SafeChangeGuard],
});
  1. 通过 inject 得到注入的路由守卫实例 获取到路由守卫的实例,就可以直接调用其上的诸如 canDeactivate 等方法了。获取的方式如下:
describe('Test safe value gaurd', () => {
  ...
  let guard: SafeChangeGuard;


  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [MockComponent],
      providers: [SafeChangeGuard],
    });
    ...
    guard = TestBed.inject(SafeChangeGuard);
    ...
  })
  ...
    it('Should handle async logic correctly', fakeAsync(() => {
    ...
    const result = guard.canDeactivate(...);
    tick(); // 模拟异步操作完成
    expect(result).toBe(false); // 根据实际逻辑调整预期结果
  }));

})
  1. 触发路由守卫实例方法所需要的实参 最重要的是 component 实例,其它材料我们可以将 {} 断言成所需要的格式。
  let fixture: ComponentFixture<MockComponent>;
  let component: MockComponent;
  let activatedRouteSnapshot: ActivatedRouteSnapshot;
  let routerStateSnapshot: RouterStateSnapshot;
  let guard: SafeChangeGuard;


  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [MockComponent],
      providers: [SafeChangeGuard],
    });
    fixture = TestBed.createComponent(MockComponent);
    component = fixture.componentInstance;
    guard = TestBed.inject(SafeChangeGuard);
    fixture.detectChanges();
    activatedRouteSnapshot = {} as ActivatedRouteSnapshot;
    routerStateSnapshot = {} as RouterStateSnapshot;
  })
  
    it('Should deactivate to be true', () => {
    expect(
      guard.canDeactivate(
        component,
        activatedRouteSnapshot,
        routerStateSnapshot,
      )).toBe(true);
  })

3. 测试环境搭建

在Angular中进行单元测试,首先需要搭建测试环境。这包括创建一个测试模块,配置相关的服务和组件,并导入所需的Angular测试库。

TestBed.configureTestingModule({
  declarations: [MockComponent],
  providers: [SafeChangeGuard],
});

4. 守卫逻辑测试

SafeChangeGuard守卫的核心逻辑是检查组件是否需要二次确认。如果组件的needsDoubleCheck属性为true,则弹出确认对话框;否则,允许导航继续。

5. 测试用例编写

5.1. 守卫存在性测试

首先,我们需要验证守卫是否被正确创建。

it('Should guard to be true', () => {
  expect(guard).toBeTruthy();
});

这个测试用例简单地检查SafeChangeGuard实例是否存在。

5.2. 守卫逻辑测试

接下来,我们需要测试守卫的逻辑是否正确。这包括在不需要二次确认和需要二次确认的情况下,守卫的返回值。

it('Should deactivate to be true', () => {
  expect(
    guard.canDeactivate(
      component,
      activatedRouteSnapshot,
      routerStateSnapshot,
    )).toBe(true);
});

it('Should deactivate to be false', () => {
  component.needsDoubleCheck = true;
  fixture.detectChanges();
  const result = guard.canDeactivate(component, activatedRouteSnapshot, routerStateSnapshot);
  expect(result).toBe(false); // 这里需要根据实际逻辑调整预期结果
});

在第二个测试用例中,我们模拟了组件需要二次确认的情况,并验证守卫是否返回了正确的值。注意,由于SafeChangeGuard中的confirm函数返回的是一个Promise,我们需要根据实际情况调整测试用例以适应异步逻辑。

6. 异步逻辑处理

如果守卫中包含异步逻辑,如调用confirm函数,我们需要使用Jasmine的异步测试支持。

it('Should handle async logic correctly', fakeAsync(() => {
  component.needsDoubleCheck = true;
  fixture.detectChanges();
  const result = guard.canDeactivate(component, activatedRouteSnapshot, routerStateSnapshot);
  tick(); // 模拟异步操作完成
  expect(result).toBe(false); // 根据实际逻辑调整预期结果
}));

7. 测试用例的完整性

在编写测试用例时,我们需要确保覆盖所有可能的情况,包括正常情况和边界情况。此外,我们还应该测试守卫对组件属性变化的响应。

8. 测试的可维护性

测试代码应该易于理解和维护。使用清晰的描述和一致的命名约定可以帮助其他开发者快速理解测试用例的目的。

9. 结语

单元测试是确保Angular路由守卫按预期工作的关键。通过本文的示例,我们可以看到如何搭建测试环境、编写测试用例、处理异步逻辑以及确保测试的完整性和可维护性。通过遵循这些最佳实践,开发者可以构建出可靠且易于维护的测试套件,从而提高代码质量和用户体验。

附录 -- 路由守卫代码和测试代码

image.png

// safe-change.guard.ts
import { Injectable } from "@angular/core";
import { ActivatedRouteSnapshot, CanDeactivate, RouterStateSnapshot, UrlTree } from "@angular/router";
import { Observable } from "rxjs";

@Injectable({
    providedIn: 'root',
})
export class SafeChangeGuard implements CanDeactivate<any> {
    canDeactivate(
        component: any,
        currentRoute: ActivatedRouteSnapshot,
        currentState: RouterStateSnapshot,
        nextState?: RouterStateSnapshot,
    ): boolean | UrlTree | Observable<boolean | UrlTree> | Promise<boolean | UrlTree> {
        if (component.needsDoubleCheck) {
            return confirm(`Are you sure to leave this page?`)
        }
        return true;
    }
}
// safe-change.guard.spec.ts
import { Component } from "@angular/core";
import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing";
import { SafeChangeGuard } from "./safe-change.guard";
import { ActivatedRouteSnapshot, RouterStateSnapshot } from "@angular/router";

@Component({
  selector: 'app-guard',
  template: `
      <div></div>
    `,
})
class MockComponent {
  needsDoubleCheck: boolean = false;
}

describe('Test safe value gaurd', () => {
  let fixture: ComponentFixture<MockComponent>;
  let component: MockComponent;
  let activatedRouteSnapshot: ActivatedRouteSnapshot;
  let routerStateSnapshot: RouterStateSnapshot;
  let guard: SafeChangeGuard;


  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [MockComponent],
      providers: [SafeChangeGuard],
    });
    fixture = TestBed.createComponent(MockComponent);
    component = fixture.componentInstance;
    guard = TestBed.inject(SafeChangeGuard);
    fixture.detectChanges();
    activatedRouteSnapshot = {} as ActivatedRouteSnapshot;
    routerStateSnapshot = {} as RouterStateSnapshot;
  })

  it('Should guard to be true', () => {
    expect(guard).toBeTruthy();
  })

  it('Should deactivate to be true', () => {
    expect(
      guard.canDeactivate(
        component,
        activatedRouteSnapshot,
        routerStateSnapshot,
      )).toBe(true);
  })

  it('Should handle async logic correctly', fakeAsync(() => {
    component.needsDoubleCheck = true;
    fixture.detectChanges();
    const result = guard.canDeactivate(component, activatedRouteSnapshot, routerStateSnapshot);
    tick(); // 模拟异步操作完成
    expect(result).toBe(false); // 根据实际逻辑调整预期结果
  }));


  afterEach(() => {
    // fixture = null;
  })
})