380 lines
14 KiB
Markdown
380 lines
14 KiB
Markdown
# Basics of testing components
|
|
|
|
A component, unlike all other parts of an Angular application,
|
|
combines an HTML template and a TypeScript class.
|
|
The component truly is the template and the class _working together_. To adequately test a component, you should test that they work together
|
|
as intended.
|
|
|
|
Such tests require creating the component's host element in the browser DOM,
|
|
as Angular does, and investigating the component class's interaction with
|
|
the DOM as described by its template.
|
|
|
|
The Angular `TestBed` facilitates this kind of testing as you'll see in the sections below.
|
|
But in many cases, _testing the component class alone_, without DOM involvement,
|
|
can validate much of the component's behavior in an easier, more obvious way.
|
|
|
|
<div class="alert is-helpful">
|
|
|
|
For the sample app that the testing guides describe, see the <live-example name="testing" embedded-style noDownload>sample app</live-example>.
|
|
|
|
For the tests features in the testing guides, see <live-example name="testing" stackblitz="specs" noDownload>tests</live-example>.
|
|
|
|
</div>
|
|
|
|
|
|
{@a component-class-testing}
|
|
|
|
## Component class testing
|
|
|
|
Test a component class on its own as you would test a service class.
|
|
|
|
Component class testing should be kept very clean and simple.
|
|
It should test only a single unit.
|
|
At first glance, you should be able to understand
|
|
what the test is testing.
|
|
|
|
Consider this `LightswitchComponent` which toggles a light on and off
|
|
(represented by an on-screen message) when the user clicks the button.
|
|
|
|
<code-example
|
|
path="testing/src/app/demo/demo.ts"
|
|
region="LightswitchComp"
|
|
header="app/demo/demo.ts (LightswitchComp)"></code-example>
|
|
|
|
You might decide only to test that the `clicked()` method
|
|
toggles the light's _on/off_ state and sets the message appropriately.
|
|
|
|
This component class has no dependencies. To test these types of classes, follow the same steps as you would for a service that has no dependencies:
|
|
|
|
1. Create a component using the new keyword.
|
|
2. Poke at its API.
|
|
3. Assert expectations on its public state.
|
|
|
|
<code-example
|
|
path="testing/src/app/demo/demo.spec.ts"
|
|
region="Lightswitch"
|
|
header="app/demo/demo.spec.ts (Lightswitch tests)"></code-example>
|
|
|
|
Here is the `DashboardHeroComponent` from the _Tour of Heroes_ tutorial.
|
|
|
|
<code-example
|
|
path="testing/src/app/dashboard/dashboard-hero.component.ts"
|
|
region="class"
|
|
header="app/dashboard/dashboard-hero.component.ts (component)"></code-example>
|
|
|
|
It appears within the template of a parent component,
|
|
which binds a _hero_ to the `@Input` property and
|
|
listens for an event raised through the _selected_ `@Output` property.
|
|
|
|
You can test that the class code works without creating the `DashboardHeroComponent`
|
|
or its parent component.
|
|
|
|
<code-example
|
|
path="testing/src/app/dashboard/dashboard-hero.component.spec.ts"
|
|
region="class-only"
|
|
header="app/dashboard/dashboard-hero.component.spec.ts (class tests)"></code-example>
|
|
|
|
When a component has dependencies, you may wish to use the `TestBed` to both
|
|
create the component and its dependencies.
|
|
|
|
The following `WelcomeComponent` depends on the `UserService` to know the name of the user to greet.
|
|
|
|
<code-example
|
|
path="testing/src/app/welcome/welcome.component.ts"
|
|
region="class"
|
|
header="app/welcome/welcome.component.ts"></code-example>
|
|
|
|
You might start by creating a mock of the `UserService` that meets the minimum needs of this component.
|
|
|
|
<code-example
|
|
path="testing/src/app/welcome/welcome.component.spec.ts"
|
|
region="mock-user-service"
|
|
header="app/welcome/welcome.component.spec.ts (MockUserService)"></code-example>
|
|
|
|
Then provide and inject _both the_ **component** _and the service_ in the `TestBed` configuration.
|
|
|
|
<code-example
|
|
path="testing/src/app/welcome/welcome.component.spec.ts"
|
|
region="class-only-before-each"
|
|
header="app/welcome/welcome.component.spec.ts (class-only setup)"></code-example>
|
|
|
|
Then exercise the component class, remembering to call the [lifecycle hook methods](guide/lifecycle-hooks) as Angular does when running the app.
|
|
|
|
<code-example
|
|
path="testing/src/app/welcome/welcome.component.spec.ts"
|
|
region="class-only-tests"
|
|
header="app/welcome/welcome.component.spec.ts (class-only tests)"></code-example>
|
|
|
|
## Component DOM testing
|
|
|
|
Testing the component _class_ is as easy as [testing a service](guide/testing-services).
|
|
|
|
But a component is more than just its class.
|
|
A component interacts with the DOM and with other components.
|
|
The _class-only_ tests can tell you about class behavior.
|
|
They cannot tell you if the component is going to render properly,
|
|
respond to user input and gestures, or integrate with its parent and child components.
|
|
|
|
None of the _class-only_ tests above can answer key questions about how the
|
|
components actually behave on screen.
|
|
|
|
- Is `Lightswitch.clicked()` bound to anything such that the user can invoke it?
|
|
- Is the `Lightswitch.message` displayed?
|
|
- Can the user actually select the hero displayed by `DashboardHeroComponent`?
|
|
- Is the hero name displayed as expected (i.e, in uppercase)?
|
|
- Is the welcome message displayed by the template of `WelcomeComponent`?
|
|
|
|
These may not be troubling questions for the simple components illustrated above.
|
|
But many components have complex interactions with the DOM elements
|
|
described in their templates, causing HTML to appear and disappear as
|
|
the component state changes.
|
|
|
|
To answer these kinds of questions, you have to create the DOM elements associated
|
|
with the components, you must examine the DOM to confirm that component state
|
|
displays properly at the appropriate times, and you must simulate user interaction
|
|
with the screen to determine whether those interactions cause the component to
|
|
behave as expected.
|
|
|
|
To write these kinds of test, you'll use additional features of the `TestBed`
|
|
as well as other testing helpers.
|
|
|
|
### CLI-generated tests
|
|
|
|
The CLI creates an initial test file for you by default when you ask it to
|
|
generate a new component.
|
|
|
|
For example, the following CLI command generates a `BannerComponent` in the `app/banner` folder (with inline template and styles):
|
|
|
|
<code-example language="sh">
|
|
ng generate component banner --inline-template --inline-style --module app
|
|
</code-example>
|
|
|
|
It also generates an initial test file for the component, `banner-external.component.spec.ts`, that looks like this:
|
|
|
|
<code-example
|
|
path="testing/src/app/banner/banner-initial.component.spec.ts"
|
|
region="v1"
|
|
header="app/banner/banner-external.component.spec.ts (initial)"></code-example>
|
|
|
|
<div class="alert is-helpful">
|
|
|
|
Because `compileComponents` is asynchronous, it uses
|
|
the [`waitForAsync`](api/core/testing/waitForAsync) utility
|
|
function imported from `@angular/core/testing`.
|
|
|
|
Please refer to the [waitForAsync](guide/testing-components-scenarios#waitForAsync) section for more details.
|
|
|
|
</div>
|
|
|
|
### Reduce the setup
|
|
|
|
Only the last three lines of this file actually test the component
|
|
and all they do is assert that Angular can create the component.
|
|
|
|
The rest of the file is boilerplate setup code anticipating more advanced tests that _might_ become necessary if the component evolves into something substantial.
|
|
|
|
You'll learn about these advanced test features below.
|
|
For now, you can radically reduce this test file to a more manageable size:
|
|
|
|
<code-example
|
|
path="testing/src/app/banner/banner-initial.component.spec.ts"
|
|
region="v2"
|
|
header="app/banner/banner-initial.component.spec.ts (minimal)"></code-example>
|
|
|
|
In this example, the metadata object passed to `TestBed.configureTestingModule`
|
|
simply declares `BannerComponent`, the component to test.
|
|
|
|
<code-example
|
|
path="testing/src/app/banner/banner-initial.component.spec.ts"
|
|
region="configureTestingModule">
|
|
</code-example>
|
|
|
|
<div class="alert is-helpful">
|
|
|
|
There's no need to declare or import anything else.
|
|
The default test module is pre-configured with
|
|
something like the `BrowserModule` from `@angular/platform-browser`.
|
|
|
|
Later you'll call `TestBed.configureTestingModule()` with
|
|
imports, providers, and more declarations to suit your testing needs.
|
|
Optional `override` methods can further fine-tune aspects of the configuration.
|
|
|
|
</div>
|
|
|
|
{@a create-component}
|
|
|
|
### _createComponent()_
|
|
|
|
After configuring `TestBed`, you call its `createComponent()` method.
|
|
|
|
<code-example
|
|
path="testing/src/app/banner/banner-initial.component.spec.ts"
|
|
region="createComponent">
|
|
</code-example>
|
|
|
|
`TestBed.createComponent()` creates an instance of the `BannerComponent`,
|
|
adds a corresponding element to the test-runner DOM,
|
|
and returns a [`ComponentFixture`](#component-fixture).
|
|
|
|
<div class="alert is-important">
|
|
|
|
Do not re-configure `TestBed` after calling `createComponent`.
|
|
|
|
The `createComponent` method freezes the current `TestBed` definition,
|
|
closing it to further configuration.
|
|
|
|
You cannot call any more `TestBed` configuration methods, not `configureTestingModule()`,
|
|
nor `get()`, nor any of the `override...` methods.
|
|
If you try, `TestBed` throws an error.
|
|
|
|
</div>
|
|
|
|
{@a component-fixture}
|
|
|
|
### _ComponentFixture_
|
|
|
|
The [ComponentFixture](api/core/testing/ComponentFixture) is a test harness for interacting with the created component and its corresponding element.
|
|
|
|
Access the component instance through the fixture and confirm it exists with a Jasmine expectation:
|
|
|
|
<code-example
|
|
path="testing/src/app/banner/banner-initial.component.spec.ts"
|
|
region="componentInstance">
|
|
</code-example>
|
|
|
|
### _beforeEach()_
|
|
|
|
You will add more tests as this component evolves.
|
|
Rather than duplicate the `TestBed` configuration for each test,
|
|
you refactor to pull the setup into a Jasmine `beforeEach()` and some supporting variables:
|
|
|
|
<code-example
|
|
path="testing/src/app/banner/banner-initial.component.spec.ts"
|
|
region="v3"
|
|
></code-example>
|
|
|
|
Now add a test that gets the component's element from `fixture.nativeElement` and
|
|
looks for the expected text.
|
|
|
|
<code-example
|
|
path="testing/src/app/banner/banner-initial.component.spec.ts"
|
|
region="v4-test-2">
|
|
</code-example>
|
|
|
|
{@a native-element}
|
|
|
|
### _nativeElement_
|
|
|
|
The value of `ComponentFixture.nativeElement` has the `any` type.
|
|
Later you'll encounter the `DebugElement.nativeElement` and it too has the `any` type.
|
|
|
|
Angular can't know at compile time what kind of HTML element the `nativeElement` is or
|
|
if it even is an HTML element.
|
|
The app might be running on a _non-browser platform_, such as the server or a
|
|
[Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API),
|
|
where the element may have a diminished API or not exist at all.
|
|
|
|
The tests in this guide are designed to run in a browser so a
|
|
`nativeElement` value will always be an `HTMLElement` or
|
|
one of its derived classes.
|
|
|
|
Knowing that it is an `HTMLElement` of some sort, you can use
|
|
the standard HTML `querySelector` to dive deeper into the element tree.
|
|
|
|
Here's another test that calls `HTMLElement.querySelector` to get the paragraph element and look for the banner text:
|
|
|
|
<code-example
|
|
path="testing/src/app/banner/banner-initial.component.spec.ts"
|
|
region="v4-test-3">
|
|
</code-example>
|
|
|
|
{@a debug-element}
|
|
|
|
### _DebugElement_
|
|
|
|
The Angular _fixture_ provides the component's element directly through the `fixture.nativeElement`.
|
|
|
|
<code-example
|
|
path="testing/src/app/banner/banner-initial.component.spec.ts"
|
|
region="nativeElement">
|
|
</code-example>
|
|
|
|
This is actually a convenience method, implemented as `fixture.debugElement.nativeElement`.
|
|
|
|
<code-example
|
|
path="testing/src/app/banner/banner-initial.component.spec.ts"
|
|
region="debugElement-nativeElement">
|
|
</code-example>
|
|
|
|
There's a good reason for this circuitous path to the element.
|
|
|
|
The properties of the `nativeElement` depend upon the runtime environment.
|
|
You could be running these tests on a _non-browser_ platform that doesn't have a DOM or
|
|
whose DOM-emulation doesn't support the full `HTMLElement` API.
|
|
|
|
Angular relies on the `DebugElement` abstraction to work safely across _all supported platforms_.
|
|
Instead of creating an HTML element tree, Angular creates a `DebugElement` tree that wraps the _native elements_ for the runtime platform.
|
|
The `nativeElement` property unwraps the `DebugElement` and returns the platform-specific element object.
|
|
|
|
Because the sample tests for this guide are designed to run only in a browser,
|
|
a `nativeElement` in these tests is always an `HTMLElement`
|
|
whose familiar methods and properties you can explore within a test.
|
|
|
|
Here's the previous test, re-implemented with `fixture.debugElement.nativeElement`:
|
|
|
|
<code-example
|
|
path="testing/src/app/banner/banner-initial.component.spec.ts"
|
|
region="v4-test-4">
|
|
</code-example>
|
|
|
|
The `DebugElement` has other methods and properties that
|
|
are useful in tests, as you'll see elsewhere in this guide.
|
|
|
|
You import the `DebugElement` symbol from the Angular core library.
|
|
|
|
<code-example
|
|
path="testing/src/app/banner/banner-initial.component.spec.ts"
|
|
region="import-debug-element">
|
|
</code-example>
|
|
|
|
{@a by-css}
|
|
### _By.css()_
|
|
|
|
Although the tests in this guide all run in the browser,
|
|
some apps might run on a different platform at least some of the time.
|
|
|
|
For example, the component might render first on the server as part of a strategy to make the application launch faster on poorly connected devices. The server-side renderer might not support the full HTML element API.
|
|
If it doesn't support `querySelector`, the previous test could fail.
|
|
|
|
The `DebugElement` offers query methods that work for all supported platforms.
|
|
These query methods take a _predicate_ function that returns `true` when a node in the `DebugElement` tree matches the selection criteria.
|
|
|
|
You create a _predicate_ with the help of a `By` class imported from a
|
|
library for the runtime platform. Here's the `By` import for the browser platform:
|
|
|
|
<code-example
|
|
path="testing/src/app/banner/banner-initial.component.spec.ts"
|
|
region="import-by">
|
|
</code-example>
|
|
|
|
The following example re-implements the previous test with
|
|
`DebugElement.query()` and the browser's `By.css` method.
|
|
|
|
<code-example
|
|
path="testing/src/app/banner/banner-initial.component.spec.ts"
|
|
region="v4-test-5">
|
|
</code-example>
|
|
|
|
Some noteworthy observations:
|
|
|
|
- The `By.css()` static method selects `DebugElement` nodes
|
|
with a [standard CSS selector](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_started/Selectors 'CSS selectors').
|
|
- The query returns a `DebugElement` for the paragraph.
|
|
- You must unwrap that result to get the paragraph element.
|
|
|
|
When you're filtering by CSS selector and only testing properties of a browser's _native element_, the `By.css` approach may be overkill.
|
|
|
|
It's often easier and more clear to filter with a standard `HTMLElement` method
|
|
such as `querySelector()` or `querySelectorAll()`.
|
|
|