In-depth Guides
Testing

Testing Attribute Directives

An attribute directive modifies the behavior of an element, component or another directive. Its name reflects the way the directive is applied: as an attribute on a host element.

Testing the HighlightDirective

The sample application's HighlightDirective sets the background color of an element based on either a data bound color or a default color (lightgray). It also sets a custom property of the element (customProperty) to true for no reason other than to show that it can.

app/shared/highlight.directive.ts

      
import {Directive, ElementRef, Input, OnChanges} from '@angular/core';
@Directive({standalone: true, selector: '[highlight]'})
/**
* Set backgroundColor for the attached element to highlight color
* and set the element's customProperty to true
*/
export class HighlightDirective implements OnChanges {
defaultColor = 'rgb(211, 211, 211)'; // lightgray
@Input('highlight') bgColor = '';
constructor(private el: ElementRef) {
el.nativeElement.style.customProperty = true;
}
ngOnChanges() {
this.el.nativeElement.style.backgroundColor = this.bgColor || this.defaultColor;
}
}

It's used throughout the application, perhaps most simply in the AboutComponent:

app/about/about.component.ts

      
import {Component} from '@angular/core';
import {HighlightDirective} from '../shared/highlight.directive';
import {TwainComponent} from '../twain/twain.component';
@Component({
standalone: true,
template: `
<h2 highlight="skyblue">About</h2>
<h3>Quote of the day:</h3>
<twain-quote></twain-quote>
`,
imports: [TwainComponent, HighlightDirective],
})
export class AboutComponent {}

Testing the specific use of the HighlightDirective within the AboutComponent requires only the techniques explored in the "Nested component tests" section of Component testing scenarios.

app/about/about.component.spec.ts

      
import {provideHttpClient} from '@angular/common/http';
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {UserService} from '../model';
import {TwainService} from '../twain/twain.service';
import {AboutComponent} from './about.component';
let fixture: ComponentFixture<AboutComponent>;
describe('AboutComponent (highlightDirective)', () => {
beforeEach(() => {
fixture = TestBed.configureTestingModule({
imports: [AboutComponent],
providers: [provideHttpClient(), TwainService, UserService],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
}).createComponent(AboutComponent);
fixture.detectChanges(); // initial binding
});
it('should have skyblue <h2>', () => {
const h2: HTMLElement = fixture.nativeElement.querySelector('h2');
const bgColor = h2.style.backgroundColor;
expect(bgColor).toBe('skyblue');
});
});

However, testing a single use case is unlikely to explore the full range of a directive's capabilities. Finding and testing all components that use the directive is tedious, brittle, and almost as unlikely to afford full coverage.

Class-only tests might be helpful, but attribute directives like this one tend to manipulate the DOM. Isolated unit tests don't touch the DOM and, therefore, do not inspire confidence in the directive's efficacy.

A better solution is to create an artificial test component that demonstrates all ways to apply the directive.

app/shared/highlight.directive.spec.ts (TestComponent)

      
import {Component, DebugElement} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {HighlightDirective} from './highlight.directive';
@Component({
standalone: true,
template: ` <h2 highlight="yellow">Something Yellow</h2>
<h2 highlight>The Default (Gray)</h2>
<h2>No Highlight</h2>
<input #box [highlight]="box.value" value="cyan" />`,
imports: [HighlightDirective],
})
class TestComponent {}
describe('HighlightDirective', () => {
let fixture: ComponentFixture<TestComponent>;
let des: DebugElement[]; // the three elements w/ the directive
let bareH2: DebugElement; // the <h2> w/o the directive
beforeEach(() => {
fixture = TestBed.configureTestingModule({
imports: [HighlightDirective, TestComponent],
}).createComponent(TestComponent);
fixture.detectChanges(); // initial binding
// all elements with an attached HighlightDirective
des = fixture.debugElement.queryAll(By.directive(HighlightDirective));
// the h2 without the HighlightDirective
bareH2 = fixture.debugElement.query(By.css('h2:not([highlight])'));
});
// color tests
it('should have three highlighted elements', () => {
expect(des.length).toBe(3);
});
it('should color 1st <h2> background "yellow"', () => {
const bgColor = des[0].nativeElement.style.backgroundColor;
expect(bgColor).toBe('yellow');
});
it('should color 2nd <h2> background w/ default color', () => {
const dir = des[1].injector.get(HighlightDirective) as HighlightDirective;
const bgColor = des[1].nativeElement.style.backgroundColor;
expect(bgColor).toBe(dir.defaultColor);
});
it('should bind <input> background to value color', () => {
// easier to work with nativeElement
const input = des[2].nativeElement as HTMLInputElement;
expect(input.style.backgroundColor).withContext('initial backgroundColor').toBe('cyan');
input.value = 'green';
// Dispatch a DOM event so that Angular responds to the input value change.
input.dispatchEvent(new Event('input'));
fixture.detectChanges();
expect(input.style.backgroundColor).withContext('changed backgroundColor').toBe('green');
});
it('bare <h2> should not have a customProperty', () => {
expect(bareH2.properties['customProperty']).toBeUndefined();
});
// Removed on 12/02/2016 when ceased public discussion of the `Renderer`. Revive in future?
// // customProperty tests
// it('all highlighted elements should have a true customProperty', () => {
// const allTrue = des.map(de => !!de.properties['customProperty']).every(v => v === true);
// expect(allTrue).toBe(true);
// });
// injected directive
// attached HighlightDirective can be injected
it('can inject `HighlightDirective` in 1st <h2>', () => {
const dir = des[0].injector.get(HighlightDirective);
expect(dir).toBeTruthy();
});
it('cannot inject `HighlightDirective` in 3rd <h2>', () => {
const dir = bareH2.injector.get(HighlightDirective, null);
expect(dir).toBe(null);
});
// DebugElement.providerTokens
// attached HighlightDirective should be listed in the providerTokens
it('should have `HighlightDirective` in 1st <h2> providerTokens', () => {
expect(des[0].providerTokens).toContain(HighlightDirective);
});
it('should not have `HighlightDirective` in 3rd <h2> providerTokens', () => {
expect(bareH2.providerTokens).not.toContain(HighlightDirective);
});
});
HighlightDirective spec in action

HELPFUL: The <input> case binds the HighlightDirective to the name of a color value in the input box. The initial value is the word "cyan" which should be the background color of the input box.

Here are some tests of this component:

app/shared/highlight.directive.spec.ts (selected tests)

      
import {Component, DebugElement} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {HighlightDirective} from './highlight.directive';
@Component({
standalone: true,
template: ` <h2 highlight="yellow">Something Yellow</h2>
<h2 highlight>The Default (Gray)</h2>
<h2>No Highlight</h2>
<input #box [highlight]="box.value" value="cyan" />`,
imports: [HighlightDirective],
})
class TestComponent {}
describe('HighlightDirective', () => {
let fixture: ComponentFixture<TestComponent>;
let des: DebugElement[]; // the three elements w/ the directive
let bareH2: DebugElement; // the <h2> w/o the directive
beforeEach(() => {
fixture = TestBed.configureTestingModule({
imports: [HighlightDirective, TestComponent],
}).createComponent(TestComponent);
fixture.detectChanges(); // initial binding
// all elements with an attached HighlightDirective
des = fixture.debugElement.queryAll(By.directive(HighlightDirective));
// the h2 without the HighlightDirective
bareH2 = fixture.debugElement.query(By.css('h2:not([highlight])'));
});
// color tests
it('should have three highlighted elements', () => {
expect(des.length).toBe(3);
});
it('should color 1st <h2> background "yellow"', () => {
const bgColor = des[0].nativeElement.style.backgroundColor;
expect(bgColor).toBe('yellow');
});
it('should color 2nd <h2> background w/ default color', () => {
const dir = des[1].injector.get(HighlightDirective) as HighlightDirective;
const bgColor = des[1].nativeElement.style.backgroundColor;
expect(bgColor).toBe(dir.defaultColor);
});
it('should bind <input> background to value color', () => {
// easier to work with nativeElement
const input = des[2].nativeElement as HTMLInputElement;
expect(input.style.backgroundColor).withContext('initial backgroundColor').toBe('cyan');
input.value = 'green';
// Dispatch a DOM event so that Angular responds to the input value change.
input.dispatchEvent(new Event('input'));
fixture.detectChanges();
expect(input.style.backgroundColor).withContext('changed backgroundColor').toBe('green');
});
it('bare <h2> should not have a customProperty', () => {
expect(bareH2.properties['customProperty']).toBeUndefined();
});
// Removed on 12/02/2016 when ceased public discussion of the `Renderer`. Revive in future?
// // customProperty tests
// it('all highlighted elements should have a true customProperty', () => {
// const allTrue = des.map(de => !!de.properties['customProperty']).every(v => v === true);
// expect(allTrue).toBe(true);
// });
// injected directive
// attached HighlightDirective can be injected
it('can inject `HighlightDirective` in 1st <h2>', () => {
const dir = des[0].injector.get(HighlightDirective);
expect(dir).toBeTruthy();
});
it('cannot inject `HighlightDirective` in 3rd <h2>', () => {
const dir = bareH2.injector.get(HighlightDirective, null);
expect(dir).toBe(null);
});
// DebugElement.providerTokens
// attached HighlightDirective should be listed in the providerTokens
it('should have `HighlightDirective` in 1st <h2> providerTokens', () => {
expect(des[0].providerTokens).toContain(HighlightDirective);
});
it('should not have `HighlightDirective` in 3rd <h2> providerTokens', () => {
expect(bareH2.providerTokens).not.toContain(HighlightDirective);
});
});

A few techniques are noteworthy:

  • The By.directive predicate is a great way to get the elements that have this directive when their element types are unknown

  • The :not pseudo-class in By.css('h2:not([highlight])') helps find <h2> elements that do not have the directive. By.css('*:not([highlight])') finds any element that does not have the directive.

  • DebugElement.styles affords access to element styles even in the absence of a real browser, thanks to the DebugElement abstraction. But feel free to exploit the nativeElement when that seems easier or more clear than the abstraction.

  • Angular adds a directive to the injector of the element to which it is applied. The test for the default color uses the injector of the second <h2> to get its HighlightDirective instance and its defaultColor.

  • DebugElement.properties affords access to the artificial custom property that is set by the directive