Preface
A few weeks ago, Pascal Precht wrote a blog article on Testing Services with HTTP with Angular. In this article, we want to discuss more advanced topics on DRY Angular testing techniques using Custom Matchers and Special Helpers.
TABLE OF CONTENTS
These are techniques to make your unit-tests incredibly easy to read and to maintain. And to achieve our learning goals, there are three (3) important testing topics that we want to cover:
- (1) Testing custom Angular Directives
- (2) Building reusable, DRY TestBed Helper methods
- (3) Using Typescript Jasmine Custom Matchers
These techniques are practically undocumented… yet they dramatically improve the quality of our tests.
To whet your appetite, here are some sample DRY tests that we will be showing you how to write:
Background
There a several excellent resources already available that developers can read to learn about Angular testing:
- Angular 2 - Testing Guide (by Gerard Sans)
- Angular 2 - Unit Testing Recipes (by Gerard Sans)
- Testing with the Angular CLI (by Todd Motto, Jurgen Van de Moere)
- Testing Angular 2 Components (by Ken Rimple)
The biggest take-aways from these articles is the singular concept that instead of manually
instantiating and testing classes, Angular developers should consider using
the TestBed.configureTestingModule()
to prepare an entire test Angular DI environment for each
test module (*.spec.ts).
We would not use
TestBed.configureTestingModule()
when we are testing a service that doesn’t have any dependencies. It’d be easier and less verbose to just instantiate usingnew
. The TestBed is for useful for dependencies and injections.
Traditional Testing
Consider the traditional approach of manually instantiating and testing a Service or component:
It’s fine to do it this way, because ServiceA obviously does not need anything else to be instantiated; it is a self-contained service without external dependencies.
So assuming that this service won’t get any dependencies in the future, this test is the one we want to write.
Introducing Angular TestBed
The Angular TestBed allows developers to configure ngModules that provide instances/values and use Dependency Injection. This is the same approach developers use in their regular Angular applications.
With the TestBed and its support for for component-lifecycle features, Angular components can be easily instantiated and tested. Here is an example - shown below - that we will continue to use in this article:
Before each test, we want configure a new, fresh testing module with only the providers, components, and directives we need for the current test module.
And notice that we just created a reusable Helper function:
createTestComponent()
. This cool utility function will construct an instance of MyComponent [using the configured TestBed] using any custom HTML template you specify.
At first, this complexity may seem like overkill. But let’s consider two critical requirements shown in the sample above:
- MatchMedia instantiation requires an injected BreakPointRegistry instance
- MyComponent instantiation requires an injected MatchMedia instance
Even with these requirements, testing developers should NOT have to worry about all those internals just to test MyComponent. Using ngModule, DI, and Angular… we now don’t have to worry about those details.
This is just like those real-world scenarios where our components, directives, and services will have complex dependencies upon providers and non-trivial construction processes.
And this is where TestBed demonstrates its real value!
We are not using external templates nor any other resources or services that are asynchronous. So we do not discuss the
async()
nor the theTestBed::compileComponents()
functions.
1) Testing Directives
With relative ease, developers can find literature on testing Angular Services and Components. Yet the How-to's for testing Directives is oddly not well documented.
Unlike Components with their associated templates, Directives do not have templates. This means that we cannot simply import a Directive and manually instantiate it.
The solution is rather easy! We only need to:
- configure a TestBed that imports and declares the directive(s),
- prepare a shell test Component, and
- use a custom template which uses the Directive selector(s) as attribute(s).
Since Directives usually affect the host element, our tests will check if those changes to the host element are present and valid.
So let’s use the Angular Flex-Layout library as the basis for the following discussions on Directives, Matchers, and TestBed Helpers.
Real-world solutions often provide great examples for reusable techniques.
We will be using both the fxLayout directive and excerpts from the unit test for that directive to explore testing ideas, techniques, and solutions that we can also use in our own tests.
First, let’s import the FlexLayout library into our own tests and prepare to test the fxLayout directive.
You can see the actual testing code in layout.spec.ts.
But don’t jump there yet! Wait until you have finished reading this article.
Configuring the TestBed
Very similar to the TestBed sample shown above, we will configure a testing module but we will
not import an external test component. My test component TestLayoutComponent
is itself defined
within our test (*.spec.ts) module.
Using an internal test component enables each test module
e.g.<directive>.spec.ts
to define and use its own custom test component with custom properties.
Here is the initial configuration:
We now have everything we need to write a Directive test quickly.
Wow! That is pretty easy.
The component has been constructed and prepared with the same processes and DI that your real world application will use.
Let’s first write our test using the traditional long-form… one without custom matchers and the more advanced helper methods.
Since the fxLayout directive will add custom flexbox CSS to the host element, our test logic here will confirm that the initial CSS is correctly assigned.
Non-DRY Testing
The traditional approach would probably implement something like this:
In the code above, we
- defined a custom template with bindings to the component property
direction
, - use deeply nested references to get access to the native element,
- test each style individually.
All this in one (1) single test. And truly it is not easily read.
Now imagine that our test module has more than 20 individual it(...)
tests!
That would be a lot of duplicate code.
And there is certainly nothing DRY (“do not repeat yourself”) about such code!
2) Testing with Helpers
Above we explored the standard approach to implementing unit tests… an approach which resulted in verbose, non-reusable code.
Let’s contrast that with the DRY version that we want:
Now this test is much more readable, maintainable, and DRY. We hide all the complexities of:
- forcing change detection,
- accessing the native element, and
- confirming 1…n DOM CSS styles
Those complexities are now encapsulated in a Helper function and a Custom Matcher (respectively).
The custom Helper function expectNativeEl( ) is similar to the standard expect( ) function.
In fact, it is wrapper function that internalizes the expect( )
call.
It is important to note that these helper methods always return the value of an
expect(...)
call!
And the resulting code change uses a similar notation to our standard training. So
becomes
Testing Nested DOM
For more complex DOM access, we can use the DebugElement’s query feature to select nested DOM nodes.
Angular’s DebugElement has several query features:
query(predicate: Predicate<DebugElement>): DebugElement;
queryAll(predicate: Predicate<DebugElement>): DebugElement[];
queryAllNodes(predicate: Predicate<DebugNode>): DebugNode[];
Please note that all debugging apis are currently experimental.
Consider the following helper function expectDomForQuery( ):
In this example, we actually want to test the nested DOM node that hosts the attribute fxFlex. Using another helper method expectDomForQuery( ) makes that easy.
And the resulting code change is again a similar notation to our standard training:
becomes
More Special Helpers
Earlier, we showed a code snapshot that had a special helperactivateMediaQuery( )
:
The Flex-Layout library has a responsive engine that supports change detection when a mediaQuery activates (aka when the viewport size changes). The API uses selectors with dot-notation to indicate which values should be used for which mediaQuery:
Testing these features presents several additional requirements:
- mock the window API
window.matchMedia(...)
- simulate a mediaQuery activation
- trigger fixture.detectChange() after a simulated activation
- hide all these details from individual tests (DRY)
Thankfully the Flex-Layout library actually publishes a MockMatchMedia class. And we use this class in our TestBed configuration below:
Let’s explore three (3) very interesting things are happening in this code:
- configure Dependency Injection providers to override a class with a mock,
- dynamic injection using
injector.get(MatchMedia)
, and - use special helper activateMediaQuery( ) function that hides all these details
(1) Overriding DI Providers
tells the TestBed DI system to provide an instance of the MockMatchMedia
whenever any code
asks for an instance of the MatchMedia
token to be injected via DI.
You can read more about the Angular DI systems here:
Dependency Injection in Angular
(2) Dynamic Injection
Our special helper activateMediaQuery()
needs a dynamic injected instance of the MatchMedia token.
Using the fixture
instance, we can access the injector service for our components. With the injector,
we can dynamically get a MockMatchMedia instance using the provider token MatchMedia.
In this case, MatchMedia is both a class and a provider token used for D-injection.
Notice that all this complexity [and construction details] on preparing a MockMatchMedia instance is encapsulated in our TestBed… and the use of the injector is hidden within our special helper.
Now our individual tests simply use the easy special helper function activateMediaQuery( ):
That is very, very cool!
3) Custom Matchers
We have only one more tool [in our testing toolkit] to discuss: Custom Jasmine Matchers.
For those developers not familiar with the concepts of Jasmine matchers, we recommend the online Jasmine documentation:
Remember that matchers are used after the expect()
call and should encapsulate complex logic
and reduce code-clutter in our test code.
This allows our test(s) to remain terse, concise, readable, maintainable, and DRY.
And we should always give our custom matcher functions clear, readable names. E.g. toHaveCssStyles().
Building a TypeScript Matcher
Similar to expect(...).toBeTruthy()
, we want a custom matcher toHaveCSSStyle( )
:
Creating a matcher in JavaScript is documented on the Jasmine site. Our challenge is the harder goal of creating a custom matcher implemented in TypeScript using well-defined types.
- we need to enhance the
expect()
API, and - we need to implement custom matchers.
The global Jasmine expect()
method normally returns <any>
value. To use custom matchers
(with types), we want the expect( ) function to support returning either a standard matcher or
a custom matcher.
Here is a teaser that shows how that is done:
We assigned the global expect function reference to a new export that states that expect()
calls
now return references to a NgMatchers interface.
Here are the full implementation details of our custom-matchers.ts
module:
With the above definitions, we can now use expect(...).toHaveCssStyles(...)
without any
TypeScript complaints.
Wait!
We need one to discuss one more addition to our custom-matcher code. Did you notice the call to
getDOM().hasStyle() ? But where does getDOM
come from?
After some inspection of the @angular/core code, we were able to get a reference to the special Angular DOM Adapter:
Please note that the getDOM() is a private Angular API and may change in the future.
Using a Custom Matcher
Now we are golden with features. Let’s import and use our custom Jasmine matcher.
Notice that we must register the custom matchers in a beforeEach()
call to configure the
matchers for each subsequent test. And now everything is ready for the individual tests:
Protractor + e2e
It should be noted that the above sample tests confirm whether CSS styles have been applied correctly to the DOM element. Unit tests perform tests logic and state… but those same tests cannot easily test how those values affect renderings in the UI.
Jasmine unit tests do not test whether the CSS styles or states render the elements in the browser as expected. Nor do they test renderings across different browsers. Those types of visual tests are best performed in e2e testing with Protractor and visual differencing tools.
Summary
Perhaps you will say: “Wow, this is cool… but totally overkill!” If you are tempted to say that, then look at all the DRY tests here: layout.spec.ts. You will quickly see the value of using helpers and custom matchers.
You now have the techniques and tools to create your own custom matchers and deliver quality, terse unit tests.
Enjoy!
Resources
- Custom Matchers: a full set of custom Jasmine Matchers
- Special Helpers: a reusable, importable set of Helpers
- Usage Samples: a full DRY set of usages.
The Flex-Layout Helper functions are actually partial applications (function currying). Here is How to use them.