如何测试使用 toObservable() 注入的 Angular 17 计算信号效果?

问题描述 投票:0回答:1

我有一个私有函数,它从我的构造函数中调用,并在计算信号上使用 toObservable 。 它在实践中效果很好,但尝试编写单元测试一直是一个 PITA。 我确信这对我来说只是简单的事情,但我无法让所有注入上下文对齐,或者让 jasmine 创建一个合适的间谍。

验证服务:

// check token state action
    private expiredStatus$ = toObservable(this.permissionStore.expiredStatus)

constructor() {
        console.log("Constructor started....");
        this.expiredStatus$
            .pipe(
                tap(() => {
                    return console.log("Subscription Active");
                })
            )
            .subscribe({
                next: (tokenStatus) => {
                    console.log("Token refreshed successfully, new status:", tokenStatus);
                },
                error: (error) => {
                    console.error("Failed to refresh token:", error);
                },
            });
    }

PermissionStore服务:

    // true if we have an Expired state on the token
    expiredStatus: Signal<boolean> = computed(() => this.permissionsState().token_status === TokenStatus.Expired);

我正在尝试通过模拟

expiredStatus$
来测试
this.permissionStore.expiredStatus
,并在 AuthService 初始化后将其从 false 变为 true。

由于在 AuthService 构造函数中订阅了 expiredStatus$ ,我遇到了我的模拟位于不同注入上下文中或奇怪的问题。 jasmine.createSpy/spyOn/spyObj 也无法识别

如果我强制计算实际信号值,我可以让构造函数运行。

      TestBed.configureTestingModule({
            providers: [
                { provide: HttpClient, useValue: httpClientMock }, // Used for jwtHttp
                { provide: HttpBackend, useValue: httpBackendMock }, // Used for cleanHttp
                {
                    provide: PermissionsStoreService,
                    useValue: {
                        // create a real computed signal parameter to use with changing
                        // effect() subscriptions
                        expiredStatus: computed(() => signalMock()),
                    },
                });

如果我尝试强制 Mock 使用正确的类型,我可以运行测试,但该值不会通过 Testbed.flushEffects() 报告为已更改,这是 Angular 17.2.0 中的新功能。


       expiredStatusMockSignal = signal(true);
        expiredStatusMockComputed = computed(() => expiredStatusMockSignal());

        // Define the type for a Signal that emits boolean values
        type BooleanSignal = Signal<boolean>;

        // Assuming permissionsStoreServiceMock is a mock object and signalMock is a mock function
        const permissionsStoreServiceMock = {
            expiredStatus: jasmine.createSpy("expiredStatus") as jasmine.Spy<() => BooleanSignal>,
        };

        // Create a spy on the expiredStatus method and provide a fake implementation
        permissionsStoreServiceMock.expiredStatus.and.callFake(() => {
            // If signalMock is a function that returns a value, you can return that value here
            return expiredStatusMockComputed as BooleanSignal;
        });

我已经尝试了这个测试的十几个版本,我认为我需要在 AuthService 的构造函数最初运行时解决注入上下文,但我不知道如何。


    it("should react to token expiration", () => {
        // Replace with your specific assertion logic
        const tokenRefreshCalled = spyOn(authService, "refreshToken").and.callFake(() => {
            // Do nothing or perform simple actions, depending on your needs
            console.debug("Called the faker...");
            return of(TokenStatus.Valid); // Or some mock response if needed
        });
        expiredStatusMockSignal.update(() => false);
        // flush the signal change
        TestBed.flushEffects();
        authService.expiredStatus$.subscribe((next) => console.log("Expired Status: ", next));

        console.log("starting shit show....");

        expiredStatusMockSignal.update(() => true);

        // flush the signal change
        TestBed.flushEffects();

        console.log("Status: ", expiredStatusMockSignal());

        // Assert that the component reacted as expected
        expect(tokenRefreshCalled).toHaveBeenCalled();
    });
angular angular17 angular-signals
1个回答
0
投票

好的,对于你的情况,我会尝试测试效果而不是内部实现。在Testbed自己的注入上下文中,您可以访问测试模块中涉及的所有变量。

只要您可以控制服务发出的值,您就可以对初始值和您决定发出的另一个值进行检查。

您的流程如下:

service => emits (reactive) value => gets caught by the entity => gets transformed into observable => gets consumed in some way, because I assume console log is not your end goal

您的测试用例可能是:

  1. 期望公共效果的初始值与转换后服务的初始值相匹配
    f(x)=> y
    其中x是服务中的值,f是消费者管道上下文中可能发生的任何转换。
  2. 在服务中发出不同的值,并期望暴露的公共效果与相同的转换相匹配
    f(x1)=>y1

在我看来,在这种情况下,您不需要检查

toObservable
是否被调用或类似的事情,因为您真正需要确保的是服务的公共接口的行为符合设计。

关于单元测试有不同的理念,有些人认为最小的单元是一个类成员甚至一个声明,看看我刚才建议的“集成”。其他人关注实体(又名类??)的公共接口,并将它们视为一个单元。

我个人尝试使用一种混合方法,其主要目标是使用简单、不可变的测试用例来测试所有可能的分支,并且对于某些边缘情况(例如您提到的情况),我发现更容易测试对公众的影响接口,再加上尝试测试实现(在私有接口上)将使您的单元测试变得脆弱,并且您将在几乎每次更改时结束对其进行修改,这可能并不理想。

我不会提供完整的实现,因为我不知道你的消费者是什么样的实体,具体的实现从服务到组件、到守卫都有所不同...... 但这可能是一个粗略的方法。


import { provideTestingAuthService, AuthServiceMock } from 'auth/testing';


let service: MyConsumerService;
let authServiceMock: AuthServiceMock;

beforeEach(async () => {
    TestBed.configureTestingModule({
        providers: [
            ...
            provideTestingAuthService('class'),
            ],
        });
        service = TestBed.inject(MyConsumerService);
        authServiceMock = TestBed.inject(AuthService) as unknown as AuthServiceMock ; // casting in case you want to use entities present only in the mock.
    });
    

// ...

describe('public interface', ()=>{

  it('should be default value if condition a meets',()=>{
     expect(service.publicInterface).toEqual(defaultValue)
  });

  // one per every branch in your logic
  it('should be x value if condition b meets',fakeAsync(()=>{
     service.desiredProperty.set(newValue);
     expect(service.publicInterface).toEqual(transformedValue)
  });

});

您可以模拟任何您想要的东西,从身份验证服务到您的

permissionStore
,因此,如果值的实际直接来源是商店而不是身份验证服务,请为不依赖于任何人的商店创建一个模拟,并直接从该源发出值。

该模式在任何情况下都不会改变,建议在单元测试中我们仅检查直接交互,并模拟所有依赖项,以避免进行跨部门调用以获得我们的主题所需的结果。

作为旁注,我通常创建与我计划注入的依赖项的接口相匹配的模拟类,这可以帮助您使模块声明可读并在测试中重用它。

provideTestingThisOrThat
是一种模式,后面跟有角度来提供依赖关系,我个人建议人们使用相同的模式来提供自己的实体,因为进入该项目的新开发人员会熟悉它的含义,并且它增加了可读性我们的代码,在这种情况下,提供者的一个示例可能是:


export class AuthServiceMock{...}

export const provideTestingAuthService = (...initialValue: ConstructorParameters<typeof AuthServiceMock>): Provider =>({
   provide: AuthService,
   useValue: new AuthServiceMock(...initialValue)

})

我很抱歉没有提供更准确的代码示例,但我缺乏实体的上下文并且不想推测

两个重要的旁注。

我不确定您的消费者的实体类型,但我看到您的消费者代码中有未订阅的订阅,这可能会导致内存泄漏,如果它是一个组件,我建议移动任何逻辑在构造函数之外,考虑到角度组件的实例化方式,因为在构造函数中放入的逻辑越多,实例化该组件的负担就越重。如果您是这种情况,那么注意这些细节可能会有回报

祝你好运,希望这个答案有帮助。

© www.soinside.com 2019 - 2024. All rights reserved.