Testing document
In this document we will give a brief overview of how to test the features
related with Angular and NGRX.
Any test should follow the
Testing guidelines basic concepts.
Angular testing
This section is about how to define and what to keep in mind about testing
components, directives, services and anything directly related with angular.
Testing dumb components
This section explains how to test our angular dumb components, before continuing
it's recommended to read the following articles first:
For this case, we'll test a component in charge of displaying an error
information:
TypeScript |
---|
| @Component({
selector: 'its-error-details',
template: './error-details.component.html',
styleUrls: ['./error-details.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ErrorDetailsComponent {
@Input() error: SerializedError;
}
|
And whose template is the following:
HTML |
---|
| <section class="error-details" *ngIf="error">
<!-- Error action -->
<hr />
<p>Action</p>
<p id="actionType">{{error.actionType}}</p>
<!-- Error stacktrace -->
<ng-container *ngIf="error.error.stack">
<hr />
<p>Stack</p>
<div class="error-details__error-stack">
<pre id="stack"> {{error.error.stack}} </pre>
</div>
</ng-container>
<!-- Raw error -->
<hr />
<p>Raw error</p>
<div class="error-details__error-raw">
<pre id="raw"> {{error.error | json}} </pre>
</div>
</section>
|
For this scenario, we want to test if the component displays correctly the data
of the error it receives, so our test cases will be the following:
TypeScript |
---|
| describe('ErrorDetailsComponent', () => {
describe('@Input() error', () => {
// We want to check if the component is created.
it('should create the component', () => {});
// We want to check if the error stack trace is displayed.
it('stack should be displayed correctly', () => {});
// We want to check if the error is displayed.
it('raw error should be displayed correctly', () => {});
});
});
|
And the way to perform the tests is the following:
TypeScript |
---|
| describe('ErrorDetailsComponent', () => {
// Will hold the fixture of the component.
let fixture: ComponentFixture<ErrorDetailsComponent>;
// Will hold the component instance being tested, taken from the fixture.
let component: ErrorDetailsComponent;
// This beforeEach block will be executed before each describe() block.
beforeEach(async(() => {
// We configure a module to tests, which declares the component being tested.
TestBed.configureTestingModule({
declarations: [ErrorDetailsComponent]
}).compileComponents();
// Before each test case we make a fixture of the component, assign the instance
// to our variable and provide it with an error.
fixture = TestBed.createComponent(ErrorDetailsComponent);
component = fixture.componentInstance;
component.error = {
actionType: '[Action] some action Failure',
timestamp: new Date().toUTCString(),
error: {
name: 'some error',
message: 'some error message',
stack: 'some error stack'
},
errorActionCategory: 'Fatal'
};
// We trigger the change detection cycle of the fixture.
fixture.detectChanges();
}));
describe('@Input() error', () => {
it('should create the component', () => {
// Assert.
// Check if the component was created.
expect(component).toBeTruthy();
});
it('stack should be displayed correctly', () => {
// Arrange.
// Get the related element with the [Id = stack] that holds the error's stack
const categoryElement = fixture.debugElement.query(
By.css('#stack')
);
// Assert.
// We assert that the text inside the element is the one defined in
// the error we provided to the component.
expect(
categoryElement.nativeElement.textContent.trim()
).toStrictEqual('some error stack');
});
it('raw error should be displayed correctly', () => {
// Arrange.
// Get the related element with the [Id = raw] that holds the error's stack
const rawElement = fixture.debugElement.query(By.css('#raw'));
// We parse the text content of the element.
const rawData = JSON.parse(rawElement.nativeElement.textContent);
// Assert.
// We assert the error data is the same as the one of the error provided.
expect(rawData).toStrictEqual({
name: 'some error',
message: 'some error message',
stack: 'some error stack'
});
});
});
|
Note
We use the jest's beforeEach method to set all the necessary data
and/or configuration needed to run the tests.
Testing services
This will explain how to test angular services, before continuing with this
section it's recommended to read the following articles:
For this case, we'll tests a service that provides some simple logic
abstractions:
TypeScript |
---|
| @Injectable()
export class ErrorHandlerManagerService {
constructor(private router: Router) {}
/**
* Redirect the user to the error panel page without pushing a new state
* into history.
*/
public displayErrorPanel() {
this.router.navigate(['error-panel'], { skipLocationChange: true });
}
/**
* Creates a plain object which contains contextual of the error which have
* been thrown.
*
* @param action
* @param category
*/
public serializeError(
action: ErrorAction,
category?: ErrorActionCategory
) {
return {
// Error information.
error: action.error,
// Action information.
actionType: action.type,
errorActionCategory: ErrorActionCategory[category],
// Context information.
timestamp: new Date().toUTCString()
};
}
/**
* Returns true if the given action was added as an error action.
*
* @param action the ngrx action to be evaluated.
*/
public isErrorAction(
action: Action,
errorCategory?: ErrorActionCategory
) {
// Here, the 'isErrorAction' is a type guard.
return isErrorAction(action, errorCategory);
}
}
|
For this scenario, we will only test the methods of the service to make sure
they work properly:
TypeScript |
---|
| describe('ErrorHandlerManagerService', () => {
describe('Create service', () => {
// Check if the service can be created.
it('should be created', () => {});
});
describe('Service methods', () => {
// Check if the "isErrorAction()" works correctly for a true case.
it('isErrorAction() should return true', () => {});
// Note: We omit the check of the false case in this example, but both cases
// must be tested.
// Check if the method serializes correctly an action.
it('serializeError() should serialize errors actions.', () => {});
});
});
|
And the way to perform the tests is the following:
TypeScript |
---|
| describe('ErrorHandlerManagerService', () => {
// Will hold the instance of the service being tested.
let service: ErrorHandlerManagerService;
describe('Create service', () => {
// Before each test case we configure an Angular's module that will be used
// to perform the tests.
beforeEach(() => {
TestBed.configureTestingModule({
// Since the ErrorHandlerManagerService has a dependency on
// Router, we import a module that provide us with it (RouterTestingModule
// is a module specifically for this purpose).
imports: [RouterTestingModule],
// We make the test module to provide the services we'll need in our
// tests. in this case, we only need the service being tested.
providers: [ErrorHandlerManagerService]
});
// We assign the service instance to tests, resolved from the testing
// module we just defined.
service = TestBed.inject(ErrorHandlerManagerService);
});
it('should be created', () => {
// Assert.
// We only assert that the service instance exists.
expect(service).toBeTruthy();
});
});
describe('methods', () => {
it('isErrorAction() should return true', () => {
// Arrange.
// We create a fatal error action.
const mockedAction = createFatalErrorAction(
'[external] Do something Failure'
);
// Act.
// We invoke the method being tested with the action.
const result = service.isErrorAction(
mockedAction,
ErrorActionCategory.Fatal
);
// Assert.
// We assert that the value is true.
expect(result).toBeTruthy();
});
it('serializeError() should serialize errors actions.', () => {
// Arrange.
const actionType = '[external] Some fatal action';
const error = {
name: 'some error name.',
message: 'some error message'
};
const expectedResult = {
error: error,
actionType: actionType,
errorActionCategory: 'Fatal'
};
const mockedActionData = {
type: actionType,
error: error
};
// Act.
const result = service.serializeError(
mockedActionData,
ErrorActionCategory.Fatal
);
// Assert.
// Compares whether the result is an object that contains all the
// properties of our expected result.
expect(result).toEqual(expect.objectContaining(expectedResult));
});
});
});
|
NGRX testing
This section provides some basic guidelines on how to tests anything related
with NGRX in our application.
Marbles notation
The Marbles notation is a language for RxJS to visually represent events
happening over "time", allowing us to test asynchronous operations that
happens in the observables in our tests, which are created through the helper
methods hot() and cold().
The marbles notation (also known as marble diagrams) use a character convention
to define each type of "event", the most likely to be used are the following:
- "-": Simulate the passage of time, each dash correspond to a "frame" of time (each frame is usually about 10ms long).
- "a" (a to z): Each letter represents a value being emitted.
- "()": Used to group multiple values emitted together in the same unit of time.
- "|": Indicates the successful completion of an observable (end of the stream).
- "#": Indicates an error terminating the observable (end of the stream).
Although this is just a short summary, we recommend reading the following articles to
understand in more detail:
Testing reducers
On this part, we'll give some examples on how to define test cases for
NGRX reducers. The aim of testing
reducers is to validate that their pure functions correctly handle the
transitions from one state to another. For this scenario, we will test the load
of a Domain model, considering the following cases:
TypeScript |
---|
| describe('Domains Reducer', () => {
// Tests that the reducer does not alters the state on unknown actions.
describe('unknown action', () => {
it('should return the previous state', () => {});
});
// Tests the reducer on an action dispatched to load Domains.
describe('loadDomain action', () => {
it('should return the state not loaded and without errors', () => {});
});
// Tests the reducer on an action dispatched when the Domains are loaded successfully.
describe('loadDomainSuccess action', () => {
it('should return the state loaded', () => {});
});
// Tests the reducer on an action dispatched when the Domains load fails.
describe('loadDomainFailure action', () => {
it('should return the state with error', () => {});
});
});
|
To test if the reducers are doing their job correctly, we'll need a
initial state
to compare. For this we have to import the one we usually define
in our reducers file. We will also need to import the reducer being tested:
TypeScript |
---|
| import { initialState, reducer } from './domain.reducer';
|
For the first case we want to test the reducer to be applied to certain state by
an unknown action, and expect the result to be the same state passed as param:
TypeScript |
---|
| describe('unknown action', () => {
it('should return the previous state', () => {
// Arrange.
// We create an action with no `type` field, but
// it could be any action that is not being taken by the reducer being tested.
const action = {} as any;
// Act.
// We use the initialState defined in our domain.reducer file.
const result = reducer(initialState, action);
// Assert.
// Finally, we expect the result to be the same state with no changes.
expect(result).toBe(initialState);
});
});
|
Note
Note that for this case we can use the toBe
method, which compares by reference,
to assert that the result is in fact the initial state.
Next we will test the reducer for the action dispatched when the Domains
are
being loaded, which for our case, will set the loaded
field in our state to
false, and remove any last error in it:
TypeScript |
---|
| describe('loadDomain action', () => {
it('should return the state not loaded and without errors', () => {
// Arrange.
// We define our expected state as the same state passed to the reducer (in our case
// the initialState) with the 'loaded' field set to false and no error
const expectedState = {
...initialState,
loaded: false,
error: null
};
// Act.
// We call the reducer for the initial state and the action dispatched
// on Domains loading.
const result = reducer(initialState, DomainActions.loadDomain);
// Assert.
// Finally, we assert that the result is exactly the same as the
// expected state we defined above.
expect(result).toStrictEqual(expectedState);
});
});
|
Note
Note that for this case we have to use the toStrictEqual
method, which does
a deep comparison.
For the load success reducer, we want to assert that the result state contains
the Domains
passed as argument of the action dispatched to add new ones to the
store:
TypeScript |
---|
| // We define an array of Domains to be used whenever needed in the tests
// of the reducers below, the "createDomain" is a local method that creates a Domain
// whose definition would be above this array, but whose implementation is
// not needed for this example.
const domains: Domain[] = [
createDomain(1, 'Domain A'),
createDomain(2, 'Domain B'),
createDomain(3, 'Domain C')
];
describe('loadDomainSuccess action', () => {
it('should return the state loaded', () => {
// Arrange.
// We define the expected state as the one passed as param to the reducer
// (in this case, the initialState) plus the 'ids' and 'entities' fields.
const expectedState = {
...initialState,
loaded: true,
// The ids field is an array with all the ids of the entities in the piece
// of the store for Domains. This field is added by default by the
// reducers created by the schematics.
ids: domains.map((x) => x.Id),
// The entities field is a dictionary { [id]: Domain } which is also
// added by the default reducers automatically created.
// PD: the createDictionaryFrom is also a local method that creates
// the dictionary, whose implementation is not needed for this example.
entities: createDictionaryFrom(domains)
};
// Act.
const result = reducer(
initialState,
// We pass the action with the same domains define above as the argument.
DomainActions.loadDomainSuccess({ domain: domains })
);
// Assert.
expect(result).toStrictEqual(expectedState);
});
});
|
For the last case, we want to assert that if the load of Domains fails, the
error thrown is added to the state:
TypeScript |
---|
| describe('loadDomainFailure action', () => {
it('should return the state with error', () => {
// Arrange.
// We define the error message to use.
const ERROR_MSG = 'Error';
const expectedState = {
...initialState,
error: ERROR_MSG
};
// Act.
const result = reducer(
initialState,
// We pass the action with the same error as the argument.
DomainActions.loadDomainFailure({ error: ERROR_MSG })
);
// Assert.
expect(result).toStrictEqual(expectedState);
});
});
|
Testing selectors
On this part, we'll give some examples on how to define test cases for
NGRX selectors. The aim of testing
selectors is to check that they are obtaining the correct slice of the store
state. For this scenario, consider the following cases:
TypeScript |
---|
| describe('Domain Selectors', () => {
// A selector which takes the list of Domains from the state.
it('getAllDomain should return the list of Domain', () => {
});
// A selector which takes the loaded status from the state.
it("getDomainIsLoaded should return the current 'loaded' status", () => {
});
// A selector which takes the last error from the state.
it("getDomainError should return the current 'error' state", () => {
});
|
Since to test selectors we don't need to assert differences between two states
like in the reducers' section, we only need a state with the needed data to
test each one of our selectors.
TypeScript |
---|
| describe('Domain Selectors', () => {
// The state which we'll use in each test.
let state;
// A list of domains as in the previous example.
const domains: Domain[];
// A constant for the error message we'll use in our test.
const ERROR_MSG = "Error";
// A constant for the 'loaded' field we'll use in our test.
const isLoaded = true;
beforeEach(() => {
// For each selector's test, we define a state that contains all the data
// needed for us to validate the selectors, in this case it will contain
// the domains, an error message and the 'loaded' field.
state = {
[DOMAIN_FEATURE_KEY]: domainAdapter.addMany(domains, {
...initialState,
error: ERROR_MSG,
loaded: isLoaded
})
};
});
[...]
}
|
And to test a selector, we simply have to assert that the result after calling
it passing the state we just defined above is the value of the expected slice of
the state to be selected.
TypeScript |
---|
| describe('Domain Selectors', () => {
it('getAllDomain should return the list of Domain', () => {
// Act.
const results = DomainSelectors.getAllDomain(state);
// Assert.
expect(results).toStrictEqual(domains);
});
it("getDomainIsLoaded should return the current 'loaded' status", () => {
// Act.
const result = DomainSelectors.getDomainIsLoaded(state);
// Assert.
expect(result).toBe(isLoaded);
});
it("getDomainError should return the current 'error' state", () => {
// Act.
const result = DomainSelectors.getDomainError(state);
// Assert.
expect(result).toBe(ERROR_MSG);
});
});
|
Testing effects
On this part, we'll talk about how to test
NGRX effects.
For this, we recommend to take a look on the following links before starting:
Consider the following scenario where we have the following effect:
TypeScript |
---|
| @Injectable()
export class DomainEffects {
loadDomains$ = createEffect(() =>
this.actions$.pipe(
// This effect will occur when a loadDomain action is dispatched.
ofType(DomainActions.loadDomain),
// This effect will make an API call to obtain the domains from the back-end
// We return the observable of that call.
switchMap(() =>
this.domainApiService.getAllDomains().pipe(
// Since the effects have to return an observable of an action we
// have to map both the success an error cases.
map(
// For the success case, we return a 'loadDomainSuccess' action with the domains returned.
(domains) =>
DomainActions.loadDomainSuccess({
domain: domains
}),
// For the fail case, we return a 'loadDomainFailure' action with the error returned.
catchError((error) =>
of(DomainActions.loadDomainFailure({ error }))
)
)
)
)
)
);
}
|
And the test case will check that, after dispatching the loadDomain action, the
effect is an observable of the expected result (in this case, a domains array).
TypeScript |
---|
| describe('loadDomain$', () => {
it('should return an observable action', () => {});
});
|
Since effects are all about observables, we need a way to dispatch the original
action which will trigger the effect being tested, for that purpose we use the
beforeEach block.
TypeScript |
---|
| // This will be the stream of actions where the original action
// (the one which triggers the effect being tested) is dispatched.
let actions: Observable<any>;
// The class where the effects were defined in.
let effects: DomainEffects;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [NxModule.forRoot()],
providers: [
DomainEffects,
DataPersistence,
// We provide the var defined above as the mock of the stream of actions.
provideMockActions(() => actions),
// We provide a mock for the DomainApiService
provideMockStore(),
{
provide: DomainApiService,
useValue: {
// We make that the "getAllDomains" of the DomainApiService returns
// an observable of domains, for this case, we only return an empty array.
getAllDomains: jest.fn(() => of([]))
}
}
]
});
effects = TestBed.inject(DomainEffects);
});
describe('loadDomain$', () => {
it('should return an observable action', () => {
// Arrange.
// We set a hot observable of the original action to dispatch.
actions = hot('-a-|', {
a: DomainActions.loadDomain()
});
// We expect the result to be an observable of the loadDomainSuccess action
// with an empty array of domains, since that's how we mocked the "getAllDomains"
// method which will be called in the effect.
const expected = cold('-a-|', {
a: DomainActions.loadDomainSuccess({
domain: []
})
});
// Assert.
// We assert that the effect is an observable of the same kind we expected.
expect(effects.loadDomains$).toBeObservable(expected);
});
});
|
Running the tests
This section explains how to run the tests of our angular modules, we
recommended to read the following articles in order to have a general idea:
To run the tests, we need to be inside the angular folder into the suite
project, and then use the following command to execute the test for a project:
TypeScript |
---|
| -nx test --project=[project-name]
|
Where [project-name] needs to be replaced with the name of the project which
can be found in the angular.json file inside the root /angular folder, for
example: suite-navigation-ui, sample-datasync, etc.
Coming soon
- Testing async methods.
- Testing with fakeAsync.
- Testing with tick.