Skip to main content

Testing

This document provides a brief overview on how to set up unit tests with jest when using the @rx-angular/state package.

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.

RxState

There are two ways you can test RxState. 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.

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.

Components

it is recommended to read the official component testing docs

RxState can be used in different styles which will affect the way how we can actually test, modify and access the respective component:

  • local provider
  • inheritance
  • local creation

Local Provider

Providing a local instance of RxState for a component is the recommended and most common way to use RxState in your component. It is recommended because it is the only way to actually make use of angulars Dependency Injection system when testing your component. You'll see why in the next section.

@Component({
selector: 'rx-angular-state-local-provider-test',
template: ` <span>{{ value$ | async }}</span> `,
providers: [RxState],
})
export class RxStateInjectionComponent {
value$ = this.state.select();

constructor(public state: RxState<{ foo: string }>) {}
}

Inheritance

You can also use RxState by extending from it in your component. The downside of this approach is, that you cannot replace the instance of RxState in a test environment.

@Component({
selector: 'rx-angular-state-inheritance-test',
template: ` <span>{{ value$ }}</span> `,
})
export class RxStateInheritanceComponent extends RxState<{ foo: string }> {
value$ = this.select();

constructor() {
super();
}
}

Local Creation

You can also use RxState by creating a local instance from it in your component. The downside of this approach is, that you cannot replace the instance of RxState in a test environment.

@Component({
selector: 'rx-angular-state-creation-test',
template: ` <span>{{ value$ }}</span> `,
})
export class RxStateCreationComponent {
state = new RxState<{ foo: string }>();
value$ = this.state.select();
}

Setup the test environment

The steps to set up a test environment for component testing involving RxState are no different to any other component tests.

describe('MyComponent', () => {
let component: MyComponent;
let fixture: ComponentFixture<MyComponent>;

beforeEach(() => {
TestBed.configureTestingModule({
declarations: [MyComponent],
teardown: { destroyAfterEach: true },
});
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});

Mock the state

If you want to mock the instance of RxState used in your component while testing, you can make use of the providers property in the TestBed configuration.

import { RxState } from './rx-state.service';

describe('MyComponent', () => {
let component: MyComponent;
let fixture: ComponentFixture<MyComponent>;
let mockState: RxState<{ foo: string }>;

beforeEach(() => {
// create a mock for your test environment
mockState = new RxState();
TestBed.configureTestingModule({
declarations: [MyComponent],
// this is only possible when going the `local provider` way
providers: [
{
provide: RxState,
useValue: mockState,
},
],
teardown: { destroyAfterEach: true },
});
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
});

Now you are able to use your mocked state instance in order to manipulate data in the test environment.

it('should work', () => {
// modify your components state before testing it
mockState.set({ foo: 'im running in a test' });
expect(component).toBeTruthy();
});

State

There are cases where you want to unit test your state transformations instead of a component.

It is advisable that you make yourself familiar with the concept of rxjs marble testing

Ideally, you already have decoupled your RxState from your component in your application.

In order to create a fully decoupled RxState instance, you can simply create an @Injectable() and extend from RxState.

@Injectable()
export class MyState extends RxState<{ foo: string }> {
state$ = this.select();

setFoo(foo: string): void {
this.set({ foo });
}
}

The MyState service now can be used as local provided instance for your component.

@Component({
selector: 'rx-angular-state-local-provider-test',
template: ` <span>{{ state$ | async }}</span> `,
providers: [MyState],
})
export class RxStateInjectionComponent {
state$ = this.state.state$;

constructor(public state: MyState) {}
}

This is the most sophisticated setup you could implement RxState in your application. It is especially useful for large ViewModels and SmartComponents and provides the easiest testing experience.

In your jest setup you are now able to test your Service completely decoupled from the component.

You can find more information about the jestMatcher here.

describe('MyState', () => {
let service: MyState;
let testScheduler: TestScheduler;

beforeEach(() => {
// create a new instance for each test
service = new MyState();
// create a new TestScheduler to run marble tests
/**
* you need to implement your own `jestMatcher` for rxjs marble tests to work in your
* jest environment.
*/
testScheduler = new TestScheduler(jestMatcher);
});
// destroy the service after each test
afterEach(() => {
service.ngOnDestroy();
});
});

Now that we have everything setup, we can start testing our state transitions with rxjs marble tests.

it('state should emit foo', () => {
testScheduler.run(({ expectObservable }) => {
service.setFoo('in a test');
expectObservable(service.select('foo')).toBe('(a)', {
a: 'in a test',
});
});
});