I am currently trying to migrate from rxjs behaviorObject to signals.
We do have some tests that test the update of a behaviorObject.
But when I try to migrate our jest tests to the new signals, I get problems.
example:
I have this service:
import { Injectable, signal } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
import { BehaviorSubject } from 'rxjs';
export interface SignalState {
foo: string;
bar: boolean | undefined;
}
@Injectable({
providedIn: 'root',
})
export class SignalService {
// private signalState$ = new BehaviorSubject<SignalState>({ foo: '', bar: undefined }); //signalState bevor
private signaltState = signal<SignalState>({ foo: '', bar: undefined });
private signalState$ = toObservable(this.signaltState);
public getValue = this.signaltState.asReadonly();
public getSignalState() {
return this.signalState$;
}
public updateFoo(foo: string) {
this.updateSignal({ foo });
}
private isChanged(currentState: SignalState, updatedState: SignalState) {
return JSON.stringify(currentState) !== JSON.stringify(updatedState);
}
private updateSignal(newState: Partial<SignalState>) {
const currentState = this.getValue();
const updatedState: SignalState = { ...currentState, ...newState };
if (this.isChanged(currentState, updatedState)) {
this.signaltState.set(updatedState);
}
}
}
I would like to test the change detection. So basically if this.signaltState.set(updatedState);
is NOT called, so change detection is not triggered.
before I had the following test
import { SpectatorService } from '@ngneat/spectator';
import { createServiceFactory } from '@ngneat/spectator/jest';
import { SignalService, SignalState } from './lead-request-state.service';
describe('SignalService', () => {
let spectator: SpectatorService<SignalService>;
let service: SignalService;
const currentState: SignalState = {
foo: 'someString',
bar: false
};
const createService = createServiceFactory({
service: SignalService,
});
beforeEach(() => {
spectator = createService();
service = spectator.inject(SignalService);
service.getSignalState().next(currentState);
});
it('should created service', () => {
expect(service).toBeTruthy();
});
it('should not update state if value is not changed', () => {
const nextSpy = jest.spyOn(service.getSignalState(), 'next');
service.updateFoo(currentState.foo);
expect(nextSpy).not.toHaveBeenCalled();
});
});
How do I do that using signals?
Actually toObservable
just converts the signal to an observable, not a BehaviourSubject
so there is no next
method to use.
Instead directly access the signal using the dot accessor method of JavaScript (can access private properties) and update the value using the set
method.
Also JSON.stringify
compare is not reliable since the order of the properties are not guaranteed, instead use isEqual
method of lodash.
Basically we check for the set
method of the signal and the test cases passes.
import { SpectatorService } from '@ngneat/spectator';
import { createServiceFactory } from '@ngneat/spectator/jest';
import { SignalService, SignalState } from './test.service';
describe('LeadRequestStateService', () => {
let spectator: SpectatorService<SignalService>;
let service: SignalService;
const currentState: SignalState = {
foo: 'someString',
bar: false,
};
const createService = createServiceFactory({
service: SignalService,
});
beforeEach(() => {
spectator = createService();
service = spectator.inject(SignalService);
service['signaltState'].set(currentState);
});
it('should created service', () => {
expect(service).toBeTruthy();
});
it('should not update state if value is not changed', () => {
const nextSpy = jest.spyOn(service['signaltState'], 'set');
service.updateFoo(currentState.foo);
expect(nextSpy).not.toHaveBeenCalled();
});
});
import { Injectable, signal } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
import { BehaviorSubject } from 'rxjs';
import { isEqual } from 'lodash';
export interface SignalState {
foo: string;
bar: boolean | undefined;
}
@Injectable({
providedIn: 'root',
})
export class SignalService {
// private signalState$ = new BehaviorSubject<SignalState>({ foo: '', bar: undefined }); //signalState bevor
private signaltState = signal<SignalState>({ foo: '', bar: undefined });
private signalState$ = toObservable(this.signaltState);
public getValue = this.signaltState.asReadonly();
public getSignalState() {
return this.signalState$;
}
public updateFoo(foo: string) {
this.updateSignal({ foo });
}
private isChanged(currentState: SignalState, updatedState: SignalState) {
return !isEqual(currentState, updatedState);
}
private updateSignal(newState: Partial<SignalState>) {
const currentState = this.getValue();
const updatedState: SignalState = { ...currentState, ...newState };
if (this.isChanged(currentState, updatedState)) {
this.signaltState.set(updatedState);
}
}
}