Working with Nx and NgRx component store, there is not much of a guide to see how to implement jest testing for components so I found this guide in one of Beeman's videos with Chau, check the video here .
In order to test faster and more efficiently we are going to use SPECTATOR with jest so we need to install the dev dependency first.
npm install -D @ngneat/spectator
Remeber to set this properties in ts.config.json in order to use jest:
"compilerOptions": {
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"allowJs": true,
Testing the component
We are going to test a simple component wich implements component store to fetch users and keep them in the state
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [AppStore],
})
export class AppComponent implements OnInit {
vm$ = this.appStore.vm$;
constructor(private appStore: AppStore) {}
ngOnInit(): void {
this.appStore.getUsers();
}
updateOne(user: any): void {
this.appStore.updateUser(user);
}
}
And the store looks like this:
export interface AppState {
users: User[];
loading: boolean;
}
const DEFAULT_STATE: AppState = {
users: [],
loading: false,
};
@Injectable()
export class AppStore extends ComponentStore<AppState> {
constructor(private readonly dataService: DataService) {
super(DEFAULT_STATE);
}
readonly vm$ = this.select(({ users, loading }) => ({ users, loading }));
readonly getUsers = this.effect(trigger$ =>
trigger$.pipe(
tap(() => this.patchState({ loading: true })),
switchMap(() =>
this.dataService.getData().pipe(
tapResponse(
users => this.addUsers(users),
error => console.log(error),
),
),
),
),
);
readonly updateUser = this.effect<User>(user$ =>
user$.pipe(
exhaustMap(user =>
this.dataService.updateOne(user).pipe(
tapResponse(
() => this.getUsers(),
error => console.log(error),
),
),
),
),
);
readonly addUsers = this.updater((state, users: User[]) => ({
...state,
users,
loading: false,
}));
}
So, let's get our hands dirty with the spec file:
describe('AppComponent', () => {
let spectator: Spectator<AppComponent>;
let store: SpyObject<AppStore>;
// createComponentFactory returns a functions wich
// we can execute to instaciate the component
const createComponent = createComponentFactory({
component: AppComponent,
imports: [RouterTestingModule],
mocks: [DataService],
// componentProviders helps us to mock the store where we
// set the view model and its methods
componentProviders: [
mockProvider(AppStore, {
vm$: of({ users: [], loading: false }),
getUsers: jest.fn(),
updateUser: jest.fn(),
}),
],
// With shallow set to true we isolate the component tests
// to not care about other nested components
shallow: true,
});
beforeEach(() => {
spectator = createComponent();
store = spectator.inject(AppStore, true);
});
describe('initialize', () => {
it('should create the component', () => {
expect(spectator.component).toBeTruthy();
expect(spectator.component.vm$).toBe(store.vm$);
});
it('should call store.getUsers on init', () => {
expect(store.getUsers).toHaveBeenCalled();
});
});
it('should call store.updateUser on updateOne', () => {
const user: User = {
id: 1,
name: 'foo',
username: 'foo',
email: 'foo'
};
spectator.component.updateOne(user);
expect(store.updateUser).toHaveBeenCalledWith(user);
});
});
Testing the store
In order to make it easier to test observables and rxjs in general we are going to use another library
npm install -D @hirez_io/observer-spy
And the testing file:
import { subscribeSpyTo } from '@hirez_io/observer-spy';
import { createServiceFactory, SpectatorService, SpyObject } from '@ngneat/spectator/jest';
import { of } from 'rxjs';
import { AppStore } from './app.store';
import { User } from './models/User';
import { DataService } from './services/data.service';
describe('WorkflowListStore', () => {
let spectator: SpectatorService<AppStore>;
let vm$: typeof AppStore.prototype['vm$'];
const getVm = (
partial: Partial<{
users: User[];
loading: boolean;
}> = {},
) => ({
users: [],
loading: false,
...partial,
});
const createService = createServiceFactory({
service: AppStore,
mocks: [DataService],
});
beforeEach(() => {
spectator = createService();
vm$ = spectator.service.vm$;
});
describe('initialize', () => {
it('should create', () => {
expect(spectator.service).toBeTruthy();
expect(vm$).toBeTruthy();
});
it('should vm$ emit default value', () => {
const observerSpy = subscribeSpyTo(vm$);
expect(observerSpy.getValues()).toEqual([getVm()]);
});
});
describe('effects', () => {
let service: SpyObject<DataService>;
const user: User = {
id: 1,
name: 'foo',
username: 'foo',
email: 'foo',
},
};
const mockedUsers = of<User[]>([user, user]);
beforeEach(() => {
service = spectator.inject(DataService);
});
describe('loadUsersEffect', () => {
it('should call service and get users properly', () => {
const observerSpy = subscribeSpyTo(vm$);
service.getData.mockReturnValueOnce(mockedUsers);
spectator.service.getUsers();
expect(service.getData).toHaveBeenCalled();
expect(observerSpy.getLastValue()).toEqual(getVm({ users: [user, user], loading: false }));
});
});
it('should call service methods properly on update user effect', () => {
const observerSpy = subscribeSpyTo(vm$);
service.updateOne.mockReturnValueOnce(of(user));
const updateUserInput: User = user;
spectator.service.updateUser(updateUserInput);
expect(observerSpy.getLastValue()).toEqual(getVm({ users: [], loading: true }));
expect(service.updateOne).toHaveBeenCalledWith(updateUserInput);
expect(service.getData).toHaveBeenCalled();
});
});
});
In the video sourced before you will find more complex examples with more explanation of use cases testing component store.