Testing
When an @ionic/angular
application is generated using the Ionic CLI, it is automatically set up for unit testing and end-to-end testing of the application. This is the same setup that is used by the Angular CLI. Refer to the Angular Testing Guide for detailed information on testing Angular applications.
Testing Principles
When testing an application, it is best to keep in mind that testing can show if defects are present in a system. However, it is impossible to prove that any non-trivial system is completely free of defects. For this reason, the goal of testing is not to verify that the code is correct but to find problems within the code. This is a subtle but important distinction.
If we set out to prove that the code is correct, we are more likely to stick to the happy path through the code. If we set out to find problems, we are more likely to more fully exercise the code and find the bugs that are lurking there.
It is also best to begin testing an application from the very start. This allows defects to be found early in the process when they are easier to fix. This also allows code to be refactored with confidence as new features are added to the system.
Unit Testing
Unit tests exercise a single unit of code (component, page, service, pipe, etc) in isolation from the rest of the system. Isolation is achieved through the injection of mock objects in place of the code's dependencies. The mock objects allow the test to have fine-grained control of the outputs of the dependencies. The mocks also allow the test to determine which dependencies have been called and what has been passed to them.
Well-written unit tests are structured such that the unit of code and the features it contains are described via describe()
callbacks. The requirements for the unit of code and its features are tested via it()
callbacks. When the descriptions for the describe()
and it()
callbacks are read, they make sense as a phrase. When the descriptions for nested describe()
s and a final it()
are concatenated together, they form a sentence that fully describes the test case.
Since unit tests exercise the code in isolation, they are fast, robust, and allow for a high degree of code coverage.
Using Mocks
Unit tests exercise a code module in isolation. To facilitate this, we recommend using Jasmine (https://jasmine.github.io/). Jasmine creates mock objects (which Jasmine calls "spies") to take the place of dependencies while testing. When a mock object is used, the test can control the values returned by calls to that dependency, making the current test independent of changes made to the dependency. This also makes the test setup easier, allowing the test to only be concerned with the code within the module under test.
Using mocks also allows the test to query the mock to determine if it was called and how it was called via the toHaveBeenCalled*
set of functions. Tests should be as specific as possible with these functions, favoring calls to toHaveBeenCalledTimes
over calls to toHaveBeenCalled
when testing that a method has been called. That is expect(mock.foo).toHaveBeenCalledTimes(1)
is better than expect(mock.foo).toHaveBeenCalled()
. The opposite advice should be followed when testing that something has not been called (expect(mock.foo).not.toHaveBeenCalled()
).
There are two common ways to create mock objects in Jasmine. Mock objects can be constructed from scratch using jasmine.createSpy
and jasmine.createSpyObj
or spies can be installed onto existing objects using spyOn()
and spyOnProperty()
.
Using jasmine.createSpy
and jasmine.createSpyObj
jasmine.createSpyObj
creates a full mock object from scratch with a set of mock methods defined on creation. This is useful in that it is very simple. Nothing needs to be constructed or injected into the test. The disadvantage of using this function is that it allows the creation of objects that may not match the real objects.
jasmine.createSpy
is similar but it creates a stand-alone mock function.
Using spyOn()
and spyOnProperty()
spyOn()
installs the spy on an existing object. The advantage of using this technique is that if an attempt is made to spy on a method that does not exist on the object, an exception is raised. This prevents the test from mocking methods that do not exist. The disadvantage is that the test needs a fully formed object to begin with, which may increase the amount of test setup required.
spyOnProperty()
is similar with the difference being that it spies on a property and not a method.