Testing
This document provides a brief overview on how to set up unit tests with jest
when using the @rx-angular/state
package.
info
Make sure you are familiar with the basics of how to test Angular applications, you can read more about it in the official testing docs. It is also recommended to read the official component testing docs.
Basic Testing
There are two ways you can test your state. Depending on your use case, you maybe want to test the behavior of a whole component, or test the state and it's transformations on its own.
tip
- Note that you want your tests to be unrelated to implementation details as much as possible.
- Keep in mind that testing a component with DOM nodes instead of component instance is considered as a best practice.
- counter.component.ts
- counter.component.spec.ts
/src/counter.component.ts
import { rxState } from '@rx-angular/state';
import { Component } from '@angular/core';
import { Subject, map, merge } from 'rxjs';
import { AsyncPipe } from '@angular/common';
@Component({
selector: 'rx-counter',
standalone: true,
imports: [AsyncPipe],
template: `
<button id="increment" (click)="increment.next()">Increment</button>
<button id="decrement" (click)="decrement.next()">Decrement</button>
<div id="counter">{{ count$ | async }}</div>
`,
})
export class CounterComponent {
readonly increment = new Subject<void>();
readonly decrement = new Subject<void>();
readonly state = rxState<{ count: number }>(({ connect, set }) => {
set({ count: 0 });
connect(
'count',
merge(
this.increment.pipe(map(() => 1)),
this.decrement.pipe(map(() => -1))
),
({ count }, slice) => count + slice
);
});
readonly count$ = this.state.select('count');
}
/src/counter.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { CounterComponent } from './counter.component';
describe('CounterComponent', () => {
let fixture: ComponentFixture<CounterComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CounterComponent],
});
fixture = TestBed.createComponent(CounterComponent);
});
it('should increment', () => {
const incrementButton = fixture.debugElement.query(By.css('#increment'));
incrementButton.triggerEventHandler('click', null);
fixture.detectChanges();
expect(
fixture.debugElement.query(By.css('#counter')).nativeElement.textContent
).toBe('1');
});
it('should decrement', () => {
const decrementButton = fixture.debugElement.query(By.css('#decrement'));
decrementButton.triggerEventHandler('click', null);
fixture.detectChanges();
expect(
fixture.debugElement.query(By.css('#counter')).nativeElement.textContent
).toBe('-1');
});
});
Testing State with Marble Diagrams
If you want to test the state and its transformations on its own, you can use the TestScheduler
from rxjs/testing
to test your state with marble diagrams.
- counter.state.ts
- counter.state.spec.ts
/src/counter.state.ts
import { rxState } from '@rx-angular/state';
import { Injectable } from '@angular/core';
import { Subject, map, merge } from 'rxjs';
@Injectable()
export class CounterService {
readonly increment = new Subject<void>();
readonly decrement = new Subject<void>();
private readonly state = rxState<{ count: number }>(({ connect, set }) => {
set({ count: 0 });
connect(
'count',
merge(
this.increment.pipe(map(() => 1)),
this.decrement.pipe(map(() => -1))
),
({ count }, slice) => count + slice
);
});
readonly count$ = this.state.select('count');
}
/src/counter.state.spec.ts
import { TestScheduler } from 'rxjs/testing';
import { TestBed } from '@angular/core/testing';
import { CounterService } from './counter.service';
const testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
describe('CounterService', () => {
let service: CounterService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [CounterService],
});
service = TestBed.inject(CounterService);
});
it('should increment', () => {
testScheduler.run(({ expectObservable }) => {
service.increment.next();
expectObservable(service.count$).toBe('a', { a: 1 });
});
});
it('should decrement', () => {
testScheduler.run(({ expectObservable }) => {
service.decrement.next();
expectObservable(service.count$).toBe('a', { a: -1 });
});
});
});