angularunit-testingdependency-injectionjestjsngrx-signal-store

How to force `onDestroy` hook invocation from a ngrx signal store in unit test?


I'm using a SignalStore from @ngrx/signals in an Angular project.

My store has onInit and onDestroy hooks invoking methods from a dependency injected in the store.

import { signalStore, withHooks } from '@ngrx/signals';

class SomeDependency {
    start() {
        throw new Error('real implementation not relevant');
    }
    stop() {
        throw new Error('real implementation not relevant');
    }
}

const MyStore = signalStore(
    withHooks((_store, dependency = inject(SomeDependency)) => ({
        onInit() {
            dependency.start();
        },
        onDestroy() {
            dependency.stop();
        },
    })),
);

I'm able to test that dependency.start() is properly invoked when creating the store

it('should start dependency when created', () => {
    const fakeDependency = { start: jest.fn(), stop: jest.fn() };
    TestBed.configureTestingModule({
        providers: [MyStore, { provide: SomeDependency, useValue: fakeDependency }],
    });

    TestBed.inject(MyStore);

    expect(fakeDependency.start).toHaveBeenCalledOnce(); // test pass ✅
});

I am looking for a way to test that dependency.stop() is invoked when the store is somehow destroyed, but how can I force the store destruction?

it('should stop dependency when destroyed', () => {
    const fakeDependency = { start: jest.fn(), stop: jest.fn() };
    TestBed.configureTestingModule({
        providers: [MyStore, { provide: SomeDependency, useValue: fakeDependency }],
    });
    const store = TestBed.inject(MyStore);

    // MISSING: force store descruction here

    expect(fakeDependency.stop).toHaveBeenCalledOnce(); // test fail ❌
});

Solution

  • As proposed by Naren, creating a testing host solution does the trick.

    class SomeDependency {
        stop() {
            throw new Error('real implementation not relevent');
        }
    }
    
    const MyStore = signalStore(
        withHooks((_store, dependency = inject(SomeDependency)) => ({
            onDestroy() {
                dependency.stop();
            },
        })),
    );
    
    describe('standard way, using TestBed', () => {
        @Component({
            standalone: true,
            providers: [MyStore],
        })
        class HostComponent {
            store = inject(MyStore);
        }
    
        it('should stop dependency when destroyed', () => {
            const spy = jest.fn();
            const fixture = TestBed.configureTestingModule({
                imports: [HostComponent],
                providers: [{ provide: SomeDependency, useValue: { stop: spy } }],
            }).createComponent(HostComponent);
    
            fixture.destroy();
    
            expect(spy).toHaveBeenCalledOnce();
        });
    });
    

    Also, using ng-mocks library can reduce boiler plate and also use a mocked HostComponent. That would be my preferred solution.

    describe('alternative with ng-mocks', () => {
        @Component({
            standalone: true,
            providers: [MyStore],
        })
        class HostComponent {
            store = inject(MyStore);
        }
    
        MockInstance.scope();
        beforeEach(() => MockBuilder(MyStore).mock(SomeDependency));
    
        it('should stop dependency when destroyed', async () => {
            const spy = MockInstance(SomeDependency, 'stop', jest.fn());
            const fixture = MockRender(HostComponent);
    
            fixture.destroy();
    
            expect(spy).toHaveBeenCalledOnce();
        });
    });