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

Angular Jasmine测试:覆写Keycloak服务私有成员的init方法

问题描述

我有一个Angular服务KeycloakService,它的私有成员是外部npm库keycloak-js中的Keycloak类实例。该服务的构造函数中调用了会发起HTTP请求的keycloak.init()方法。编写单元测试时,我希望避免调用真实的keycloak.init函数,同时保留服务的其他所有方法和属性。

极简代码示例如下:

外部库中的Keycloak类

// 定义在外部npm库中
export class Keycloak {
  init() {
    // 发起HTTP请求
    console.log('Obj func');
  }
}

待测试的KeycloakService

@Injectable({
  providedIn: 'root',
})
export class KeycloakService{
  private keycloak;
  constructor() {
    this.keycloak = new Keycloak();
    this.keycloak.init();
  }
  // 其他方法和属性
}

初始测试代码

describe('KeycloakService', () => {
  let service: KeycloakService;
  const objectSpy: jasmine.SpyObj<Keycloak> = jasmine.createSpyObj('Keycloak', [
    'init',
  ]);

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [KeycloakService],
      // 如何用keycloakSpy替换KeycloakService.keycloak,同时保留其他方法和属性
    });
    service = TestBed.inject(KeycloakService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });
});

我已经通过将Keycloak类包装为Angular服务的方式解决了问题,但仍想了解是否有无需包装的实现方式。


解决方案

方案一:已实现的包装服务方式

通过创建继承自Keycloak的Angular包装服务,利用依赖注入替换实例:

包装类定义

// 定义在外部npm库中
export class Keycloak {
  init() {
    // 发起HTTP请求
    console.log('Obj func');
  }
}

@Injectable({
  providedIn: 'root',
})
export class KeycloakWrapper extends Keycloak {}

修改后的KeycloakService

@Injectable({
  providedIn: 'root',
})
export class KeycloakService {
  constructor(private keycloak: KeycloakWrapper) {}
  // 其他方法和属性
}

测试代码

describe('KeycloakService', () => {
  let service: KeycloakService;
  const keycloakSpy: jasmine.SpyObj<KeycloakWrapper> = jasmine.createSpyObj(
    'KeycloakWrapper',
    ['init']
  );

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        KeycloakService,
        { provide: KeycloakWrapper, useValue: keycloakSpy },
      ],
    });
    service = TestBed.inject(KeycloakService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
    expect(keycloakSpy.init).toHaveBeenCalled(); // 可选:验证init被调用
  });
});

方案二:无需包装的实现方式

如果不想创建包装类,可以通过以下几种方式实现:

方式1:SpyOn Keycloak的构造函数

在测试前拦截Keycloak的构造函数,让它返回预设的spy对象:

describe('KeycloakService', () => {
  let service: KeycloakService;
  const keycloakSpy: jasmine.SpyObj<Keycloak> = jasmine.createSpyObj('Keycloak', ['init']);

  beforeEach(() => {
    // 拦截Keycloak构造函数,返回spy对象
    spyOn(window, 'Keycloak').and.returnValue(keycloakSpy);
    
    TestBed.configureTestingModule({
      providers: [KeycloakService],
    });
    service = TestBed.inject(KeycloakService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
    expect(keycloakSpy.init).toHaveBeenCalled();
  });
});

注意:如果Keycloak不是挂载在window上,而是通过模块导入的,需要调整拦截对象为对应的模块导出。

方式2:用工厂函数手动替换私有成员

通过工厂函数创建KeycloakService实例,手动替换私有成员keycloak:

describe('KeycloakService', () => {
  let service: KeycloakService;
  const keycloakSpy: jasmine.SpyObj<Keycloak> = jasmine.createSpyObj('Keycloak', ['init']);

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        {
          provide: KeycloakService,
          useFactory: () => {
            const serviceInstance = new KeycloakService();
            // 用类型断言绕过私有成员访问限制
            (serviceInstance as any).keycloak = keycloakSpy;
            return serviceInstance;
          }
        }
      ],
    });
    service = TestBed.inject(KeycloakService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
    // 若需要验证init调用,可手动触发或调整逻辑
  });
});

方式3:改造KeycloakService支持工厂注入(推荐长期方案)

微调KeycloakService的构造逻辑,注入Keycloak的工厂函数,测试时替换工厂实现:

@Injectable({
  providedIn: 'root',
})
export class KeycloakService{
  private keycloak;
  // 注入工厂函数,默认创建新的Keycloak实例
  constructor(@Inject('KEYCLOAK_FACTORY') keycloakFactory: () => Keycloak = () => new Keycloak()) {
    this.keycloak = keycloakFactory();
    this.keycloak.init();
  }
  // 其他方法和属性
}

对应的测试代码:

describe('KeycloakService', () => {
  let service: KeycloakService;
  const keycloakSpy: jasmine.SpyObj<Keycloak> = jasmine.createSpyObj('Keycloak', ['init']);

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        KeycloakService,
        { 
          provide: 'KEYCLOAK_FACTORY', 
          useValue: () => keycloakSpy 
        }
      ],
    });
    service = TestBed.inject(KeycloakService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
    expect(keycloakSpy.init).toHaveBeenCalled();
  });
});

这种方式无需额外包装类,同时提升了服务的可测试性,适合长期维护。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 14:15:29