Read time: 22 minutes
1. What is unit testing?
For Web Development, unit testing is deserted yet compelling for quality delivery of products. Unit testing is a testing method that is used by the developers to test the individual modules and to determine if there are any issues with the code written by them.
The objective of this blog is to provide a complete understanding of how you can test your Angular application and NgRx using Jasmine.
Jasmine is a behavior-driven framework that has a clean and accessible syntax, does not require a DOM, and is used for testing JavaScript code.de.
Find the Jasmine documentation here.
Once a new application with Angular CLI installs by default all the tools required for unit testing and already contains a test for the AppComponent. To run it from the project directory, run: ng test.
After building an application in change tracking mode, run all available tests. Test files must be named using the *.spec.ts format, but if the elements are created via Angular CLI, these files are automatically created.
Once executing the ng test command, a new browser window will open where all tests written for your application will be executed, and the result will be shown in the console and on the screen.
Let us get started with the simple structure of a test. For this, open the file /src/app/app.component.spec.ts.
General test structure
import {...} from '...'; ... describe('related tests group', () => { beforeEach(() => {...}); it('test description', async(() => { expect(...).toBe(...); })); ... });
In Angular unit testing, dependencies are imported first. These are always elements of the @angular/core/testing library and entities for which the test is written.
The describe() function brings together a group of related tests. The first parameter accepts a brief description/name of the group, and the second parameter should be a function that contains the structure and a set of tests.
It is endorsed to group tests linked to one component, service, etc., and call the group as the name of the service, component, etc.
describe('AppComponent', () => { // })
In describe(), the test itself is described by the it() function. It also takes as parameters a textual description and a function that describes all the logic as parameters.
Checking the result of execution is carried out using the expect() function, which takes a final value in conjunction with one of the matching functions.
it('expect example', () => { let a = 5 a = a + 7 expect(a).toBe(12) })
The beforeEach() function sets the default state and is called before each it() function. For example, before running each test, it is needed to create an instance of the class of the component below the test, and to avoid doing this in every it() function, you can use beforeEach().
beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [AppComponent], }).compileComponents() }))
2. Spy Objects
The Jasmine Spy objects are very important in testing Angular applications. They allow you to emulate function calls and use objects without calling the original function and without instantiating the object class.
Spy objects are not directly instantiated. There are functions for this:
- spyOn()- keeps track of the states and usage of the object's methods.
- spyOnProperty() - used to track the state and the properties of an object.
- jasmine.createSpy - creates a function that has no definition.
- jasmine.createSpyObj - creates a stub object
The Jasmine spyOn() function is used for real class methods. It takes two parameters: an instance of the class and the name of the method being tracked.
const appService = new AppService() const appServiceSpy = spyOn(appService, 'getData')
Now you can control the getData() method and track all calls to it using the methods of the Spy class.
appServiceSpy.getData.and.returnValue(8) expect(appServiceSpy.getData()).toBe(8) expect(appServiceSpy.getData).toHaveBeenCalled() expect(appServiceSpy.getData.calls.count()).toBe(1) expect(appServiceSpy.getData.calls.mostRecent().returnValue).toBe(8)
The returnValue() method accepts a value that will be returned by the method the next time it is called. In this case, the original method will not be called.
Jasmine has a toHaveBeenCalled() function to check if a method call is being made. More distinct information about method calls is contained in the call's property of the Spy object itself.As an example, you can find out exactly how many times a method was called using the count() function, or you can find out everything about its last call using mostRecent().
To reset the call statistics and use the method, use the reset() function of the calls object.
appServiceSpy.getData.calls.reset()
Now let's look at an example with a call to the original function.
appServiceSpy.getData(3).and.callThrough() expect(appServiceSpy.getData).toHaveBeenCalledWith(3)
The callThrough() function calls the class method directly. You can check the passed arguments using the toHaveBeenCalledWith() function.
To call a function other than the original definition when calling a method, the callFake() is used.
appServiceSpy.getData.and.callFake((number) => 3 * number) appServiceSpy.getData(3) expect(appServiceSpy.getData.calls.mostRecent().returnValue).toBe(9)
Here is an example of how to check if an exception will be thrown. It is important to note that when we expect it to throw an exception, it is recommended to use lambda because of the context changes.
appServiceSpygetData('two').and.throwError('Argument must be a number') expect(() => appServiceSpy.getData()).toThrow()
The stub() function is available to reset all previously set values using returnValue(), callThrough(), callFake(), throwError()
appServiceSpy.getData.and.stub()
spyOnProperty() is similar to spyOn(), only it is used to track the usage of object properties and takes three parameters:
- class object
- object property name
- access modifier - can be 'get' or 'set' (default is 'get')
The practical application is identical to spyOn().
const modelAutoSpy = spyOnProperty(auto, 'model') modelAutoSpy.model.and.returnValue('Mercedes') expect(modelAutoSpy.model).toBe('Mercedes') expect(modelAutoSpy.model).toHaveBeenCalled() expect(modelAutoSpy.model.calls.count()).toBe(1) modelAutoSpy.model.and.callThrough() expect(modelAutoSpy.model).toBe('BMW')
Unlike spyOn(), jasmine.createSpy is for undefined functions. In other words, the function creates a Spy object to track calls to a method that doesn't actually exist.
jasmine.createSpy()takes a single argument: the name of a non-existent function.
const getValueSpy = jasmine.createSpy('getValue') getValueSpy.getValue.and.returnValue(2) expect(getValueSpy.getValue).toBe(2) expect(getValueSpy.getValue).toHaveBeenCalled()
createSpy and createSpyObject are useful when the class you are testing has external dependencies, so you can inject fake dependencies from the outside and see how your class / service interacts with these external dependencies.
jasmine.createSpyObj() allows you to create a new class object with a set of already tracked methods.
The first parameter is the name of the class, but the second is an array of methods or an object whose keys are the names of the methods, and their values are the data returned by the analogous methods by default.
An example of creating a Spy object using jasmine.createSpyObj():
const exampleSpy = jasmine.createSpyObj('ExampleClass', [ 'getData', 'getValue', ])
or
const exampleSpy = jasmine.createSpyObj('ExampleClass', { getData: 'Hello', getValue: 1, })
Now you can track the getData() and getValue() methods at once. Let us assume that exampleSpy was created in the first way.
exampleSpy.getValue.and.returnValue(7) expect(exampleSpy.getValue).toBe(7) expect(exampleSpy.getValue).toHaveBeenCalled() exampleSpy.getData.and.returnValue('Bye') expect(exampleSpy.getData).toBe('Bye') expect(exampleSpy.getData).toHaveBeenCalled()
If exampleSpy was defined with default method values, then you can immediately check the return value.
expect(exampleSpy.getValue).toBe(1) expect(exampleSpy.getValue).toHaveBeenCalled() expect(exampleSpy.getData).toBe('Hello') expect(exampleSpy.getData).toHaveBeenCalled()
3. Service testing
Let us look over testing Angular services since tests most easily cover them.
The TestBed utility from the @angular/core/testing library plays is important in testing Angular applications. It allows you to imitate the Testing Module, like the module created with the @NgModule() decorator. A test module is required to define the modules, services, components, etc., on which the test depends.
TestBed has a configureTestingModule() method that accepts a configuration object similar to the one passed to @NgModule().
beforeEach(() => { TestBed.configureTestingModule({ providers: [AppService], }) })
In the code above, the AppService defined in the providers of the test module becomes available for use by each of the tests being executed. The service instance is retrieved by the get() method of the TestBed utility.
get() can only provide services that are specified in the providers property of the Testing Module.
describe('AppService', () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [AppService], }) appService = TestBed.get(AppService) }) it('getData() should multiply passed number by 2', () => { spyOn(appService, 'getData').and.callThrough() let a = appService.getData(2) let b = appService.getData(3) expect(a).toBe(4, 'should be 4') expect(b).toBe(6, 'should be 6') expect(appService.getData).toHaveBeenCalled() expect(appService.getData.calls.count()).toBe(2) expect(appService.getData.calls.mostRecent()).toBe(6) }) })
Here is one test that verifies whether the getData() method of the AppService is working correctly. The getData() method takes a number and returns its doubled value. SpyOn() is used to collect information about calls to the getData() method.
Usually, the service is not limited to its own methods. In a complex application, services interact with each other using each other's functionalities.
In such cases, it is common to use Spy objects in order not to create a full-fledged instance of another service for the sake of one or more methods.
describe('AppService', () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [ AppService, ], }) appService = TestBed.inject(AppService) }) it('emulate getData usage', () => { const spy = spyOn(appService, ‘getData’); expect(spy).toHaveBeenCalled(); expect(appService.getData().length).not.toBe(null); }) })
In the example, createSpyObj() emulates the AppService with its only getData() method.
If all tests work with one set of data that the getData() method should return, then in beforeEach(), you can set this data like this:
const appServiceSpy = jasmine.createSpyObj('AppService', { getData: [1, 2, 3], })
The tools also provide the ability to test Angular services that access data from a remote server. The HttpTestingModule and the HttpTestingController are key to it.
Testing HTTP services does not involve accessing a remote API. Instead, all outgoing requests are redirected to the HttpTestingController.
app.service.ts
import { Injectable } from '@angular/core' import { HttpClient } from '@angular/common/http' @Injectable({ providedIn: 'root' }) export class AppService { constructor(private http: HttpClient) {} getData() { return this.http.get(`/api/data`) } }
app.service.spec.ts
import { HttpClientTestingModule, HttpTestingController, } from '@angular/common/http/testing' import { TestBed } from '@angular/core/testing' describe('AppService - testing HTTP request method getData()', () => { let httpTestingController: HttpTestingController beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [AppService], }) appService = TestBed.get(AppService) httpTestingController = TestBed.get( HttpTestingController ) }) it('can test HttpClient.get', () => { const data = [1, 2, 3] appService .getData() .subscribe((response) => expect(response).toBe(data)) const req = httpTestingController.expectOne('/api/data') expect(req.request.method).toBe('GET') req.flush(data) }) afterEach(() => httpTestingController.verify()) })
As you can see from the example, the request object is accessed using the expectOne() method of an instance of the HttpTestingController class that identifies the request depending on the condition passed to it. The method takes as a parameter the URL to which the request is made, or the request object itself. For example, you can snap a request with a specific HTTP header or with a specific value.
Only one request must satisfy the condition. If there is more than one such request or none at all, an exception will be thrown. To work with a group of requests, you must use the match() method, which returns an array of HTTP requests that match the specified criterion.
const req = httpTestingController.match('/api/data')
In the above code, the req variable will contain an array of all requests made to the URL /api/data.
The data returned in response to the request is passed as an argument to the flush() method. At the end of each such test, the verify() method must be called on an instance of the HttpTestingController class, which confirms that all requests for the current test have been completed. The code fits perfectly into the afterEach() function.
To emulate a server response with an error code, an object is passed to the flush() method as the second argument, which indicates the status and error text.
it('can test HttpClient.get', () => { const message = 'Session expired' appService.getData().subscribe( (response) => fail('should fail with the 401 error'), (err: HttpErrorResponse) => { expect(err.status).toBe(401, 'status') expect(err.error).toBe(message, 'message') } ) const req = httpTestingController.expectOne('/api/data') expect(req.request.method).toBe('GET') req.flush(message, { status: 401, statusText: 'Unauthorized', }) })
For a network layer error, you can use the error() method of the request object. The passed parameter is an object of type ErrorEvent.
const error = new ErrorEvent('Network error', { message: 'Something wrong with network', }) req.error(error)
In the examples provided, fail() is used to force the test execution to fail in places where Angular cannot independently determine if the script was executed correctly.
4. Directive Testing
Directives are used to change the behavior or appearance of an element. The code for a directive that changes an element's background color if its text value has an occurrence of the specified sequence of characters is listed below. The default background color is gray (#a9a9a9), but it can also be user defined.
match-string.directive.ts
@Directive({ selector: '[matchString]', }) export class HintHotKeyDirective implements OnChanges { @Input('matchString') matchString: string @Input() bgColor: string = '#a9a9a9' constructor(private el: ElementRef) {} ngOnChanges(changes: SimpleChanges) { if ( (changes.matchString && changes.matchString.currentValue) || (changes.bgColor && changes.bgColor.currentValue) ) { this._search(changes.matchString.currentValue) } } private _search(match: string) { if ( this.el.nativeElement.textContent.indexOf(match) != -1 ) this.el.nativeElement.style.backgroundColor = this.bgColor else this.el.nativeElement.style.backgroundColor = '' } }
It is good practice for testing directives to create a test component that describes all of its use cases. This approach is explained by the fact that the real application component can use only part of the directive's functionality and it will simply be impossible to test the full functionality without its help.
match-string-test.component.ts
@Component({ selector: 'match-string-test', template: ` <div> <h3 [matchString]="match"> Match string test component </h3> <p [matchString]="match" [bgColor]="color"> This is a component for testing all use cases of [matchString] directive. </p> </div> `, }) export class MatchStringTestComponent { match: string color: string constructor() {} }
match-string-test.component.spec.ts
describe('[matchString] directive', () => { let fixture: ComponentFixture<MatchStringTestComponent> let comp: MatchStringTestComponent beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ MatchStringDirective, MatchStringTestComponent, ], schemas: [NO_ERRORS_SCHEMA], }) .compileComponents() .then(() => { fixture = TestBed.createComponent( MatchStringTestComponent ) comp = fixture.componentInstance }) })) it('should color only p background', () => { comp.match = 'directive' fixture.detectChanges() const el = fixture.debugElement.queryAll( By.directive(MatchStringDirective) ) const h3 = el[0].nativeElement const p = el[1].nativeElement expect(h3.style.backgroundColor).toBe('') expect(p.style.backgroundColor).toBe('#a9a9a9') }) it('should color only p background with color value', () => { comp.match = 'directive' comp.color = '#fafad2' fixture.detectChanges() const el = fixture.debugElement.queryAll( By.directive(MatchStringDirective) ) const p = el[1].nativeElement expect(p.style.backgroundColor).toBe('#fafad2') }) })
It is worth noting that the test module's configuration includes a schemas property with a value of [NO_ERRORS_SCHEMA], which disables Angular throwing exceptions when undeclared components or directives are encountered.
Elements are accessed when testing directives using the directive() method of the By class. It is also acceptable to use querySelector() and By.css().
Check the styles applied to the element through the styles property of the DebugElement object. All custom properties of the element set by the directive are available in the properties property.
5. Pipe Testing
A pipe is the easiest portion of an application to test because its class usually only contains one method called transform() and does not need services. The tests themselves do not even require the TestBed utility.
Let us consider an example of testing the cutTxt filter, which cuts a string if its length exceeds the past value and adds an ellipsis to its end.
cut-txt.pipe.ts
@Pipe({ name: 'cutTxt' }) export class CutTxtPipe implements PipeTransform { transform(text: string, length: number): string { if (text.length <= length) return text else return `${text.substr(0, length)}...` } }
cut-txt.pipe.spec.ts
describe('CutTxtPipe', () => { let cutTxt = new CutTxtPipe() it('doesn\'t transform "Hello, World!"', () => { expect(cutTxt.transform('Hello, World!', 50)).toBe( 'Hello, World!' ) }) it('transforms "Hello, World!" to "Hello..."', () => { expect(cutTxt.transform('Hello, World!', 5)).toBe( 'Hello...' ) }) })
You should also examine the validity of the pipe's work in the component template to thoroughly test it.
cut-txt-pipe-test.component.ts
@Component({ selector: 'cut-txt-pipe-test', template: ` <p id="case-1">{{ 'Hello, World!' | cutTxt: 50 }}</p> <p id="case-2">{{ 'Hello, World!' | cutTxt: 5 }}</p> `, }) export class CutTxtPipeTestComponent { constructor() {} }
cut-txt-pipe-test.component.spec.ts
describe('cutTxt in component template', () => { let fixture: ComponentFixture<CutTxtPipeTestComponent> beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [CutTxtPipeTestComponent], }) .compileComponents() .then(() => { fixture = TestBed.createComponent( CutTxtPipeTestComponent ) }) })) it('#case-1 should contain "Hello, World"', () => { const el = fixture.debugElement.nativeElement.query( '#case-1' ) expect(el.textContent).toBe('Hello, World!') }) it('#case-2 should contain "Hello..."', () => { const el = fixture.debugElement.nativeElement.query( '#case-2' ) expect(el.textContent).toBe('Hello...') }) })
6. NgRx Store Testing
Tests for the NgRx store in an Angular application are also necessary for increased test coverage. Many people are confused by reactive testing since it appears difficult to create tests that both challenge and expect the proper behavior.
NgRx requires testing various application pieces (such as reducers, actions, selectors, and effects), each of which needs its own logic to test successfully.
6.1 Testing reducers
Reducers are the easiest to test since they are simple functions. All you have to do is make sure you're getting the right condition for the input.
Let's pretend we've got the following reducer:
export const employeeReducers = ( lastState: IEmployeeState = new EmployeeState(), action: GenericAction<EmployeeActionTypes, any> ): EmployeeState => { switch (action.type) { case employeeActionTypes.RequestEmployees: return requestEmployees(lastState, action); case employeeActionTypes.ReceiveEmployees: return receiveEmployees(lastState, action); case employeeActionTypes.ErrorReceiveEmployees: return errorReceiveEmployees(lastState, action); default: return lastState; } };
It would be best if you started by testing the default case. We are to pass in action to fall back to the default case, it can be any string, basically
describe('default case', () => { it('should return init state', () => { const noopAction = new GenericAction('noop' as employeeActionTypes); const newState = employeeReducers(undefined, noopAction); const initState = new EmployeeState(); expect(newState).toEqual(initState); }); });
The same state is returned when you call the reducer with a non-matching action.
If we want to test a specific instance, such as ReceiveEmployees, we must first ensure that the change related to that case happened, such as a loading flag being set true.
describe('requestEmployees', () => { it('should return isLoading true', () => { const initState = new EmployeeState(); const requestEmployeesAction = new GenericAction(employeeActionTypes. RequestEmployees); const newState = employeeReducers(initState, requestEmployeesAction); expect(newState.isLoading).toBe(true); }); });
Note that I am using GenericAction, which is a generic actiongenerator that can help you decrease the amount of boilerplate in your NgRx apps.
There must be a failure case as well. This is how the test appears:
describe(errorReceiveEmployees, () => { it('should return isLoading false and error', () => { const initState = new EmployeeState (); const error = new Error('http error'); const requestEmployeesAction = new GenericAction(employeeActionTypes. ErrorReceiveEmployees, error); const newState = employeeReducers (initState, requestEmployeesAction); expect(newState.isLoading).toBe(false); expect(newState.errors).toBe(error); }); });
ReceiveEmployeesError is triggered, with the error as a payload. The mistake is not likely to be contained in the new state.
All of these are fairly simple to complete. Therefore, this is a good use case for practicing the TDD approach due to its easy testability and the fact that it ensures that your business logic meets the requirements. TDD gets harder as you work closer to the UI since you may wish to utilize a more experimental development technique, such as mocking a new prototype proof of concept.
6.2 Testing Actions
Because actions do not express logic, Typescript's type-safety protects them. They merely trigger reducers and effects. You may want to write tests for your action dispatchers anyhow to ensure a certain degree of coverage and to "double-check" that the proper action is being sent out.
Actions may be wrapped in a service to minimize boilerplate and the requirement to inject the store in every container component, making it easier to use and test:
@Injectable({ providedIn: 'root' }) export class EmployeeActions { constructor(private store: Store<EmployeeState>) {} public requestEmployees(): void { this.store.dispatch(new RequestEmployees()); } }
To test this, just assert that the correct action is dispatched when the associated method is invoked. As a result, we observe the store's dispatch, call the tested method, and assert that the anticipated action was passed to dispatch as a parameter:
describe('requestEmployees', () => { it('should dispatch load employees action', () => { const expectedAction = new RequestEmployees(); const store = jasmine.createSpyObj<Store<EmployeeState>>('store', ['dispatch']); const employeeActions = new EmployeeActions(store); employeeActions.requestEmployees(); expect(store.dispatch).toHaveBeenCalledWith(expectedAction); }); });
6.3 Testing Effects
Effects are a lot more fun to experiment with. They necessitate both the triggering of an effect and the assertion of the reactive result. Rx Marbles may be used to test that an effect returns the correct observable stream. Rx Marbles may look scary at first, but it is a standard for expressing observable streams, and it is there to assist you. Jasmine-marbles will be used. The Angular app's default testing framework is a customized version of Rx Marbles for Jasmine.
The Rx Marbles standard is used to define observable streams. It's a quick and straightforward approach to making an observable stream. You'll need to know the following symbols to make them.
You can create either a cold or a hot observable with Marbles. These are created with the functions: cold and hot
- “-” is 10 frames, indicating that time has passed. Each of the symbols below will also take up 10 frames, which is a type of virtual time to separate occurrences of events. They are not bound to any real-time measure.
- “|” means completed observable.
- “#” means error. - You can specify the error by setting it as the third argument to cold or hot.
- “()” can wrap a couple of events that should happen in the same timeframe.
- [a-z 0-9] Everything else is variables, which can be set with the second argument in cold or hot.
That's all there is to know about creating NgRx tests. If you want to read more about Rx Marbles you can find good info here: https://rxmarbles.com
An Rx Marbles test
Take at this effect:
@Injectable() export class EmployeeEffects { constructor(private actions$: Actions, private employeeService: EmployeeService) {} @Effect() requestEmployees$ = this.actions$.pipe( ofType(employeeActionTypes.RequestEmployees), exhaustMap(() => this.employeeService.getEmployees()), map((employees) => new ReceiveEmployees(employees)), catchError((error: Error) => of(new ErrorReceiveEmployees(error))) ); }
Let’s write tests for the two obvious scenarios: success and failure. So, success goes first:
describe('EmployeeEffects', () => { let actions: Observable<any>; let effects: EmployeeEffects; beforeEach(() => { TestBed.configureTestingModule({ providers: [ EmployeeEffects, provideMockActions(() => actions), mockProvider(EmployeeResourcesService), ], }); effects = TestBed.inject(EmployeeEffects); }); it('should return a stream with employees loaded action', () => { const employees: IEmployee[] = [{ name: '', id: '1', address: '' }]; const action = new RequestEmployees(); const outcome = new ReceiveEmployees(employees); actions = hot('-a', { a: action }); const response = cold('-a|', { a: employees }); employeeService.getEmployees.and.returnValue(response); const expected = cold('--b', { b: outcome }); expect(effects.requestEmployees$).toBeObservable(expected); }); });
The action is configured to be transmitted by making it a hot observable and then emitting it after waiting for 10 frames. It's utilized to start the effect that's being evaluated.
Because the getEmployees response should only execute when the test calls it, we describe it as a cold observable. It takes ten frames to complete, then returns employees. Finally, we anticipate waiting for 10 + 10 = 20 frames before returning a stream containing the ReceiveEmployees action.
Experiment with the state of failure:
it('should fail and return an action with the error', () => { const action = new RequestEmployees(); const error = new Error('some error') as any; const outcome = new ErrorReceiveEmployees(error); actions = hot('-a', { a: action }); const response = cold('-#|', {}, error); employeeService.getEmployees.and.returnValue(response); const expected = cold('--(b|)', { b: outcome }); expect(effects.requestEmployees$).toBeObservable(expected); });
To test unsuccessful cases, we first trigger the effect by setting the actions as hot observables. Then we do the answer from getEmployees: wait 10 frames, throw an error and then come back. Again, from 20 frames until the stream is complete, the expected stream waits 20 frames and then returns ErrorReceiveEmployees and will be completed on the same frame (because the action is wrapped in the operator, which terminates instantly).
6.4 Selector testing
You should not use the shop directly in your feature services to ensure the type of store security is selected, but refer to the store selector file, which works as a storefront. This also makes testing more convenient because you will not have to deal with a phony store.
Selector testing follows the same ideas as action testing. It is already Typescript-secured for types, and it works closely with the NgRx framework, which is covered in the tests. However, if your app requires 100% coverage and you want to "double-check," you will need to specify that the store is using the correct selection feature.
Consider the following class of selector:
export const getEmployeesState = createFeatureSelector<EmployeeState>('employees'); export const employeesSelectorFn = createSelector( getEmployeesState, (employeeState) => employeeState.employees ); @Injectable({ providedIn: 'root' }) export class EmployeesSelector { constructor(private store: Store<EmployeeState>) {} public getEmployees() { return this.store.select(employeesSelectorFn); } }
You would test it like this:
it('should return call the employeesSelectorFn', () => { const store = jasmine.createSpyObj<Store<EmployeeState>>('store', ['select']); const employeesSelector = new EmployeesSelector(store); employeesSelector.getEmployees(); expect(store.select).toHaveBeenCalledWith(employeesSelectorFn); });
Here we simply test that the store is called with the appropriate selection function. As I said, very similar to action testing.
Using the selector facade helps with mocking, as it’s simpler to mock the selector than directly mock the store. Reduced friction means more test coverage and usability.
To test the selector function in isolation, the ‘projector’ method can be called. It ensures a specific selector function is ran against a specified state, just out of the air:
it('should return the employees, () => { const employees = [new Employee('employee1', 'employee1')]; const employeeState = { employees: employees, isLoading: true } as EmployeeState; expect(employeesSelectorFn.projector(employeeState)).toEqual(employees); });
6.5 Testing NgRx facade using override/MockStore
As we aim to decouple the UI from business logic - the same must be done in relation to the NgRx architecture (or other state management framework you’re using). Otherwise, maintainability and scalability would get questionable. So, to ensure loose coupling you should establish a facade in between, to reside there all the NgRx delegation (dispatch actions and setup selectors).
MockStore
Still, you might decide to avoid services wrapping it all. Maybe you’ll consider them too much boilerplate, or some might say that they only suit well the tests, not the app architecture itself. In this case, you could use those functions directly in your facade. But as you still need to set the state somehow and control things for mocking, you shall use the MockStore. This way, you can test the selectors in integration.
This is the complete example:
describe('Service: Employees, () => { let employeeService: EmployeeService; let store: MockStore<EmployeeState>; const initialState = {}; beforeEach(() => { TestBed.configureTestingModule({ providers: [ employeeService, { provide: }, provideMockStore({ initialState }), ], }); employeeService = TestBed.get(employeeService); store = TestBed.get(Store); }); it('should return the employees', (done) => { const employees = [{ id: '1' } as Employee]; store.setState({ employees: employees, isLoading: false }) employeesService.employees$.pipe(first()).subscribe((employees) => { expect(employees).toBe(employees); done(); }); }); });
OverrideSelector
If you don’t need to include the selectors in the tests, you can enjoy faster and easier tests by using overrideSelector. With overrideSelector, you can simply specify a return value for a specific selector, so you don’t need to set the store state using MockStore.
7. Conclusion
We looked at using NgRx to test Angular applications in this post. Reducers, effects, actions, and selectors were all tested. Because reducers are pure functions, they were the easiest to test of all the tests. We looked at how to test effects with Jasmine Marbles, which enabled us to describe the expected observable stream the effect should produce and activate the effect under test. I also demonstrated how to use selectors and actions to reduce boilerplate and make working with NgRx easier and more seamless.
I hope you enjoyed it and you’ll find them a good start in covering all your projects with useful tests! Happy coding!
We have another topic that might interest you! Continue reading on how to use Redux in an Angular application.