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

Angular + Jasmine测试:服务中BehaviorSubject转Observable加载验证问题

问题描述

我有一个可注入的UserService,用于通过HTTP获取用户角色和权限。为确保HTTP请求仅执行一次,在服务内部创建了BehaviorSubject,对外暴露公共Observable,供组件订阅以检查权限是否加载完成。

相关代码

user.service.ts

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class UserService {

  private _loaded = new BehaviorSubject<boolean>(false);
  public $loaded = this._loaded.asObservable();
  userDetailsUrl = "/serviceurl";

  constructor(private _http: HttpClient) {
    this.getUserDetails();
  }

  getUserDetails() {
    this._http.get(this.userDetailsUrl).subscribe({
      next: (data) => {
        // set permissions...
        this._loaded.next(true);
      }
    });
  }

  // getters for roles and permissions...
}

some.component.ts

@Component({...})
export class SomeComponent implements OnInit {

  loadedRoles: Subscription;

  constructor(private _user: UserService) {}

  ngOnInit() {
    this.loadedRoles = this._user.$loaded.subscribe({
      next: (data) => // get roles and permissions to restrict component functionality...
    });
  }
  // ...
}

测试问题

在测试文件中,我期望创建服务后调用getUserDetails来测试$loaded的订阅逻辑,但「should be loaded」测试失败。控制台显示订阅先收到false,再收到true,而测试直接断言数据为truthy,导致失败。

user.service.spec.ts

describe('SessionService', () => {
  let service: SessionService;
  let httpClient: HttpClient;
  let httpTestingController: HttpTestingController;
  const userDetailsUrl = "/serviceurl";
  const data1 = {
    "roles": ["TESTER"]
  };
  let firstReq;

  beforeEach(waitForAsync(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule]
    });
    service = TestBed.inject(SessionService);
    httpClient = TestBed.inject(HttpClient);
    httpTestingController = TestBed.inject(HttpTestingController);
    
    firstReq = httpTestingController.expectOne({
      'url': userDetailsUrl,
      'method': 'get'
    });
  }));
  afterEach(() => {
    httpTestingController.verify();
  });

  it('should create and get user details over HTTP', () => {
    expect(service).toBeTruthy();
    expect(firstReq.request.method).toEqual('GET');
  });

  it('should be loaded', () => {
    service.$loaded.subscribe({
      next: (data) => {
        console.log("post load", data);
        expect(data).toBeTruthy(); // since it passes true to the subscription
      },
      error: fail
    });
    service.getUserDetails();
    httpTestingController.expectOne({
      'url': userDetailsUrl,
      'method': 'get'
    }).flush(data1);
  });
  // ...
}

输出日志

Browser application bundle generation complete.
LOG: 'post load', false
Chrome 105.0.0.0 (Windows 10): Executed 1 of 2 SUCCESS (0 secs / 0.022 secs)
LOG: 'post load', true
Chrome 105.0.0.0 (Windows 10): Executed 1 of 2 SUCCESS (0 secs / 0.022 secs)
Chrome 105.0.0.0 (Windows 10) UserService should be loaded FAILED
        Error: Expected false to be truthy.
            at <Jasmine>
            at Object.next (src/app/services/user.service.spec.ts:50:22)
            at ConsumerObserver.next (node_modules/rxjs/dist/esm/internal/Subscriber.js:91:1)
            at SafeSubscriber._next (node_modules/rxjs/dist/esm/internal/Subscriber.js:60:1)
Chrome 105.0.0.0 (Windows 10): Executed 2 of 2 (1 FAILED) (0 secs / 0.032 secs)
Chrome 105.0.0.0 (Windows 10) UserService should be loaded FAILED
        Error: Expected false to be truthy.
            at <Jasmine>
            at Object.next (src/app/services/user.service.spec.ts:50:22)
            at ConsumerObserver.next (node_modules/rxjs/dist/esm/internal/Subscriber.js:91:1)

请问我忽略了什么问题?


解决方案

问题出在BehaviorSubject的核心特性上:它会在订阅时立即发送当前的最新值。你的_loaded初始值是false,所以订阅$loaded时会先收到这个false,而测试对每一次收到的值都做expect(data).toBeTruthy()断言,第一次的false直接触发了失败。

另外还有一个隐藏问题:在beforeEach里注入SessionService(应该是UserService的笔误)时,服务构造函数会自动调用getUserDetails()发起请求,测试里再次调用getUserDetails()会发起第二次不必要的HTTP请求。

可以通过以下几种方式修复测试:

方法1:过滤初始的false值

用RxJS的filter操作符只处理值为true的情况:

it('should be loaded', () => {
  service.$loaded.pipe(filter(value => value)).subscribe({
    next: (data) => {
      expect(data).toBeTruthy();
    },
    error: fail
  });
  // beforeEach中已发起请求,直接flush即可,无需重复调用getUserDetails
  firstReq.flush(data1);
});

方法2:等待最终的true值(async/await方式)

用firstValueFrom将Observable转为Promise,等待true值到来:

import { firstValueFrom } from 'rxjs';
import { filter } from 'rxjs/operators';

// ...

it('should be loaded', async () => {
  // 等待$loaded发出true
  const loadedValue = await firstValueFrom(service.$loaded.pipe(filter(v => v)));
  firstReq.flush(data1);
  expect(loadedValue).toBeTruthy();
});

方法3:用done回调控制测试完成时机

通过Jasmine的done函数,确保只有收到true时才标记测试完成:

it('should be loaded', (done) => {
  service.$loaded.subscribe({
    next: (data) => {
      if (data) {
        expect(data).toBeTruthy();
        done(); // 仅收到true时完成测试
      }
    },
    error: (err) => {
      fail(err);
      done();
    }
  });
  firstReq.flush(data1);
});

最后要修正测试里的笔误:describe('SessionService'改为describe('UserService',let service: SessionService;改为let service: UserService;,避免命名混淆。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.17 14:01:03