Angular unit testing with BreakpointObservers
In this article we are going to test a service which depends on the CDK BreakpointObserver. Imagine the service is called LayoutService and has got a member named sidenavMode$ of type Observable<string>
pushing over if the screen is small and side if the screen is large. This is where you can use CDKs internal breakpointobserver which fires when the state of an observed string changes. For our purpose the string to observe is "min-width: 700px". See the cdk documentation for all possible breakpoint observations.
The layout service simply maps the breakpoint observer state to a string beeing either side or over.
@Injectable()
export class LayoutService {
sidenavMode$: Observable<string>;
constructor(private breakpointObserver: BreakpointObserver) {
this.sidenavMode$ = this.breakpointObserver
.observe('(min-width: 700px)')
.pipe(
map((breakpointState) => (breakpointState.matches ? 'side' : 'over'))
);
}
}
If we want to test the service it would be really nice to set the screen size manually for each test case. To realize that we can create a mock BreakpointObserver which also contains a resize method. It internally has a state which is a RxJS BehaviorSubject. BehaviorSubjects provides a next method which lets you inject data manually.
class MockBreakpointObserver {
private state: BehaviorSubject<BreakpointState> = new BehaviorSubject(
undefined
);
resize(size: number) {
this.state.next({ matches: size >= 700 ? true : false, breakpoints: {} });
}
observe(): Observable<BreakpointState> {
return this.state.asObservable().pipe(skip(1));
}
}
You can see that the resize method uses the next method to push a new state. The observe method returns the state as an observable. It skips the first element because behaviorsubjects initially emit their current state which we want to prevent to get an observable like behavior. Lets create the testing environment. As usual you use Jasmines describe function to set up a section for testing the service. In that environment we create a service and a breakpointObserver variable. Before each test we want to create a TestBed which provides the service and the observer. But instead using the original BreakpointObserver we use the mocking class we created before.
describe('LayoutService', () => {
let service: LayoutService;
let breakpointObserver: MockBreakpointObserver;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
LayoutService,
{ provide: BreakpointObserver, useClass: MockBreakpointObserver },
],
});
service = TestBed.get(LayoutService);
breakpointObserver = TestBed.get(BreakpointObserver);
});
// Add the tests here
});
After the TestBed configuration we store the references of the services. Now we can use the observer to simulate the screen size of the application and we can test the behavior of the sidenavMode$ observable. And here we go, writing the test.
it('should push sidenavMode side when window width >= 700', (done) => {
breakpointObserver.resize(400);
service.sidenavMode$.subscribe((mode) => {
expect(mode).toBe('side');
done();
});
breakpointObserver.resize(700);
});
This is a very simple test case and it just checks the mapping we implemented in the LayoutService. But you also can use this technique to test more complex template behavior.