You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular中Auth0服务Jasmine单元测试无覆盖率问题排查

问题排查

你单元测试无覆盖率的核心原因有3个:

  • 测试对象替换错误:你把需要被测试的AuthService直接替换成了自己写的Auth0SecurityServiceStub,测试全程跑的都是存根代码,原服务的代码完全没有被执行,自然没有覆盖率。
  • 存根不符合Auth0原生API规范:auth0.WebAuth的parseHash是回调式API,不会直接返回结果,你写的存根直接返回对象,就算你不替换AuthService,原服务内部的parseHash回调逻辑也完全不会触发,核心流程跑不通。
  • 无分支覆盖设计:你没有针对handleLoginCallback的错误分支、getUserInfo的元数据判断分支、角色判断分支、authenticated的false分支做测试设计,就算主流程跑通也拿不到全量覆盖率。

全覆盖率测试实现方案

测试环境配置

你需要保留真实的AuthService作为被测对象,仅Mock它的外部依赖即可,以下是Angular+Jasmine环境下的测试模块配置示例:

import { TestBed, fakeAsync, tick } from '@angular/core/testing';
import { Router } from '@angular/router';
import { AuthService } from './auth.service';
import * as auth0 from 'auth0-js';
import jwt_decode from 'jwt-decode';

describe('AuthService', () => {
  let service: AuthService;
  let routerSpy: jasmine.SpyObj<Router>;
  let auth0Mock: any;
  let userInfoSpy: jasmine.Spy;

  beforeEach(() => {
    // Mock Router
    const mockRouter = jasmine.createSpyObj('Router', ['navigate']);
    // Mock Auth0 WebAuth 实例,符合原生API规范
    userInfoSpy = jasmine.createSpy('userInfo').and.callFake((accessToken, callback) => {
      callback(null, { name: 'test user' });
    });
    auth0Mock = {
      authorize: jasmine.createSpy('authorize'),
      parseHash: jasmine.createSpy('parseHash'),
      logout: jasmine.createSpy('logout'),
      client: {
        userInfo: userInfoSpy
      }
    };
    // Spy 掉 auth0.WebAuth 构造函数,返回我们的mock实例
    spyOn(auth0, 'WebAuth').and.returnValue(auth0Mock);
    // Spy jwt_decode
    spyOn(window, 'jwt_decode').and.returnValue({
      "https://api.test.net/roles": ["COMMON_EXCEPTIONHANDLER_UPDATER"]
    });
    // 清理localStorage避免用例污染
    localStorage.clear();

    TestBed.configureTestingModule({
      providers: [
        AuthService,
        { provide: Router, useValue: mockRouter }
      ]
    });
    service = TestBed.inject(AuthService);
    routerSpy = TestBed.inject(Router) as jasmine.SpyObj<Router>;
  });

全分支测试用例

接下来按照覆盖所有方法、所有分支的原则写测试用例:

// 测试login方法
  it('login 应该调用auth0的authorize方法', () => {
    service.login();
    expect(auth0Mock.authorize).toHaveBeenCalledWith({ connection: 'value ....' });
  });

  // 测试handleLoginCallback 成功分支
  it('handleLoginCallback 拿到有效authResult时应该执行登录成功逻辑', fakeAsync(() => {
    const mockAuthResult = {
      accessToken: 'test-token',
      expiresIn: 3600,
      idTokenPayload: {
        user_metadata: { username: 'test' },
        name: 'test.com'
      }
    };
    // 让parseHash调用成功回调
    auth0Mock.parseHash.and.callFake((callback) => {
      callback(null, mockAuthResult);
    });
    spyOn(service, 'getUserInfo');

    service.handleLoginCallback();
    tick();

    expect(window.location.hash).toBe('');
    expect(service.accessToken).toBe('test-token');
    expect(service.getUserInfo).toHaveBeenCalledWith(mockAuthResult);
    expect(routerSpy.navigate).toHaveBeenCalledWith(['/home']);
  }));

  // 测试handleLoginCallback 错误分支
  it('handleLoginCallback 拿到错误时应该打印错误日志', () => {
    const mockErr = { error: 'login failed' };
    auth0Mock.parseHash.and.callFake((callback) => {
      callback(mockErr, null);
    });
    spyOn(console, 'error');

    service.handleLoginCallback();
    expect(console.error).toHaveBeenCalledWith(`Error: ${mockErr.error}`);
  });

  // 测试handleLoginCallback 无authResult也无错误分支
  it('handleLoginCallback 无有效authResult时应该打印提示', () => {
    auth0Mock.parseHash.and.callFake((callback) => {
      callback(null, null);
    });
    spyOn(console, 'log');

    service.handleLoginCallback();
    expect(console.log).toHaveBeenCalledWith('Authentication not successful');
  });

  // 测试getUserInfo 有用户元数据且有对应角色分支
  it('getUserInfo 有用户元数据时应该设置权限和用户信息', () => {
    const mockAuthResult = {
      accessToken: 'test-token',
      idTokenPayload: {
        user_metadata: { username: 'test' },
        name: 'test.com'
      }
    };

    service.getUserInfo(mockAuthResult);
    expect(userInfoSpy).toHaveBeenCalledWith('test-token', jasmine.any(Function));
    expect(localStorage.getItem('perm')).toBe('write');
    expect(localStorage.getItem('loggedInUser')).toBe('test@test.com');
  });

  // 测试getUserInfo 无用户元数据分支
  it('getUserInfo 无用户元数据时不设置本地存储', () => {
    const mockAuthResult = {
      accessToken: 'test-token',
      idTokenPayload: {}
    };

    service.getUserInfo(mockAuthResult);
    expect(localStorage.getItem('perm')).toBeNull();
    expect(localStorage.getItem('loggedInUser')).toBeNull();
  });

  // 测试_setSession方法
  it('_setSession 应该正确设置登录状态', () => {
    const mockAuthResult = { accessToken: 'test-token', expiresIn: 3600 };
    const mockProfile = { name: 'test user' };
    const now = Date.now();
    spyOn(Date, 'now').and.returnValue(now);
    spyOn(service['loggedIn$'], 'next');

    service['_setSession'](mockAuthResult, mockProfile);
    expect(service.expiresAt).toBe(3600 * 1000 + now);
    expect(service.accessToken).toBe('test-token');
    expect(service.userProfile).toEqual(mockProfile);
    expect(service['loggedIn$'].next).toHaveBeenCalledWith(true);
    expect(service.loggedIn).toBeTrue();
  });

  // 测试logout方法
  it('logout 应该调用auth0登出并清理本地存储', () => {
    localStorage.setItem('loggedInUser', 'test@test.com');
    service.logout();
    expect(auth0Mock.logout).toHaveBeenCalled();
    expect(localStorage.getItem('loggedInUser')).toBeNull();
  });

  // 测试authenticated getter分支
  it('authenticated 未过期且已登录时返回true', () => {
    service['expiresAt'] = Date.now() + 3600 * 1000;
    service['loggedIn'] = true;
    expect(service.authenticated).toBeTrue();
  });

  it('authenticated 已过期时返回false', () => {
    service['expiresAt'] = Date.now() - 1000;
    service['loggedIn'] = true;
    expect(service.authenticated).toBeFalse();
  });

  it('authenticated 未登录时返回false', () => {
    service['expiresAt'] = Date.now() + 3600 * 1000;
    service['loggedIn'] = false;
    expect(service.authenticated).toBeFalse();
  });
});

额外优化建议

如果想让测试更易维护,可以把auth0.WebAuth的实例改成依赖注入的方式,不用在AuthService内部直接new,后续Mock不需要spy构造函数,配置更简单。

内容的提问来源于stack exchange,提问作者user2273499

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.09 14:00:04