Quantcast
Channel: Articles by thoughtram
Viewing all articles
Browse latest Browse all 68

Testing Angular Directives with Custom Matchers

$
0
0

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.

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:

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:

Unit Test with Matchers and Helpers

Testing Nested DOM Styles

Background

There a several excellent resources already available that developers can read to learn about Angular testing:

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 using new. The TestBed is for useful for dependencies and injections.

Traditional Testing

Consider the traditional approach of manually instantiating and testing a Service or component:

import{ServiceA}from'./services/ServiceA';describe('ServiceA',()=>{it('should emit greeting event',()=>{letinst=newServiceA();inst.greeting.subscribe(g=>{expect(g).toEqual({greeting:'hello'});});inst.sayHello();});});

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:

import{MyComponent}from'./viewer/MyComponent';/**
 * Configure testbed to register components and prepare services
 */beforeEach(()=>{TestBed.configureTestingModule({imports:[FlexLayoutModule.forRoot()],declarations:[MyComponent],// 'MatchMedia' construction requires BreakPointRegistry// 'MyComponent' construction requires MatchMedia parameterproviders:[BreakPointRegistry,BreakPointsProvider,{provide:MatchMedia,useClass:MockMatchMedia}]})});/**
 * Helper function to easily build a component Fixture using the specified template
 */functioncreateTestComponent(template:string):ComponentFixture<Type<any>>{returnTestBed.overrideComponent(MyComponent,{set:{template:template}}).createComponent(MyComponent);}

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 the TestBed::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:

import{Component,OnInit}from'@angular/core';import{ComponentFixture,TestBed}from'@angular/core/testing';import{FlexLayoutModule,MockMatchMedia,MatchMedia,BreakPointsProvider,BreakPointRegistry}from'@angular/flex-layout';describe('layout directive',()=>{beforeEach(()=>{// Configure testbed to prepare servicesTestBed.configureTestingModule({imports:[FlexLayoutModule.forRoot()],declarations:[TestLayoutComponent],providers:[BreakPointRegistry,BreakPointsProvider,{provide:MatchMedia,useClass:MockMatchMedia}]})});it('should add correct styles for default `fxLayout` usage',()=>{// TEST LOGIC HERE});});/*
 * Shell component with property 'direction' that will be used
 * with our tests
 */@Component({selector:'test-layout',template:`<span>PlaceHolderHTMLtobeReplaced</span>`
})exportclassTestLayoutComponentimplementsOnInit{direction="column";constructor(){}ngOnInit(){}}/*
 * Custom Helper function to quickly create a `fixture` instance based on
 * the 'TestLayoutComponent' class
 */functioncreateTestComponent(template:string):ComponentFixture<TestLayoutComponent>{returnTestBed.overrideComponent(TestLayoutComponent,{set:{template:template}}).createComponent(TestLayoutComponent);}

We now have everything we need to write a Directive test quickly.

it('should compile with custom directives',()=>{letfixture=createTestComponent('<div fxLayout></div>');expect(fixture).toBeDefined();});

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:

it('should add correct styles for `fxLayout` with template bindings',()=>{lettemplate='<div [fxLayout]="direction"></div>';letfixture=createTestComponent(template);letel=fixture.debugElement.children[0].nativeElement;fixture.detectChanges();expect(el.style['display']).toBe('flex');expect(el.style['box-sizing']).toBe('border-box');expect(el.style['flex-direction']).toBe('column');fixture.componentInstance.direction='row';fixture.detectChanges();expect(el.style['flex-direction']).toBe('row');});

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:

Testing Directives - DRY Code

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.

/**
 * Note: this helper only accesses the 1st-level child within a component
 *       A different helper method is need to access deep-level DOM nodes
 */exportfunctionexpectNativeEl(fixture:ComponentFixture<any>):any{fixture.detectChanges();// Return response of `expect(...)`returnexpect(fixture.debugElement.children[0].nativeElement);}

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

expect(...).toBe

becomes

expectNativeEl(...).toBe

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( ):

pic6

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.

/**
 * Create component instance using custom template, the query for the native DOM node
 * based on the query selector
 */functionexpectDomForQuery(template:string,selector:string):any{letfixture=createTestComponent(template);fixture.detectChanges();// Return response of `expect(...)`returnexpect(queryFor(fixture,selector).nativeElement);};/**
 * Don't forget to import the `By` utilities
 */import{By}from'@angular/platform-browser';/**
 * Reusable Query helper function
 */functionqueryFor(fixture:ComponentFixture<any>,selector:string):any{returnfixture.debugElement.query(By.css(selector))}

And the resulting code change is again a similar notation to our standard training:

expect(...).toBe

becomes

expectDomForQuery(...).toBe

More Special Helpers

Earlier, we showed a code snapshot that had a special helperactivateMediaQuery( ):

Unit Test with Matchers and Helpers

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:

<divfxLayout="row"fxLayout.md="column"></div>

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:

letfixture:ComponentFixture<any>;/**
 * Special Helper
 */constactivateMediaQuery=(alias)=>{letinjector=fixture.debugElement.injector;letmatchMedia:MockMatchMedia=injector.get(MatchMedia);// simulate mediaQuery change and trigger fxLayout changesmatchMedia.activate(alias);};beforeEach(()=>{// Configure testbed to prepare servicesTestBed.configureTestingModule({imports:[FlexLayoutModule.forRoot()],declarations:[TestLayoutComponent],providers:[BreakPointRegistry,BreakPointsProvider,{provide:MatchMedia,useClass:MockMatchMedia}//Special mock config]})});

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

{provide:MatchMedia,useClass:MockMatchMedia}

tells the TestBed DI system to provide an instance of the MockMatchMediawhenever 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( ):

activateMediayQuery('md');

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:

pic9

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( ):

expectNativeEl(fixture).toHaveCssStyle({'display':'flex','flex-direction':'row','box-sizing':'border-box'});

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:

exportconstexpect:(actual:any)=>NgMatchers=<any>_global.expect;exportinterfaceNgMatchersextendsjasmine.Matchers{/* ... */};

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:

declarevarglobal:any;const_global=<any>(typeofwindow==='undefined'?global:window);/**
 * Extend the API to support chaining with custom matchers
 */exportconstexpect:(actual:any)=>NgMatchers=<any>_global.expect;/**
 * Jasmine matchers that support checking custom CSS conditions.
 * !! important to add your custom matcher to the interface
 */exportinterfaceNgMatchersextendsjasmine.Matchers{toHaveCssStyle(expected:{[k:string]:string}|string):boolean;not:NgMatchers;}/**
 * Implementation of 1...n custom matchers
 */exportconstcustomMatchers:jasmine.CustomMatcherFactories={// Here is our custom matcher; cloned from @angular/coretoHaveCssStyle:function(){return{compare:function(actual:any,styles:{[k:string]:string}|string){letallPassed:boolean;if(typeofstyles==='string'){allPassed=getDOM().hasStyle(actual,styles);}else{allPassed=Object.keys(styles).length!==0;Object.keys(styles).forEach(prop=>{allPassed=allPassed&&getDOM().hasStyle(actual,prop,styles[prop]);});}return{pass:allPassed,getmessage(){constexpectedValueStr=typeofstyles==='string'?styles:JSON.stringify(styles);return`Expected${actual.outerHTML}${!allPassed?' ':'not '}tocontaintheCSS${typeofstyles==='string'?'property':'styles'}"${expectedValueStr}"`;}};}};}};

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:

import{__platform_browser_private__}from'@angular/platform-browser';constgetDOM=__platform_browser_private__.getDOM;

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.

import{customMatchers}from'../utils/testing/custom-matchers';describe('layout directive',()=>{beforeEach(()=>{jasmine.addMatchers(customMatchers);// TestBed stuff here...});});

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:

expect(...).toHaveCssStyle({'display':'flex','flex-direction':'row','box-sizing':'border-box'});

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

The Flex-Layout Helper functions are actually partial applications (function currying). Here is How to use them.


Viewing all articles
Browse latest Browse all 68

Trending Articles