41

I have a pipe that sanatises HTML as below:

import { Pipe, PipeTransform } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';

@Pipe({
    name: 'sanitiseHtml'
})

export class SanitiseHtmlPipe implements PipeTransform {

constructor(private _sanitizer: DomSanitizer) {}

    transform(value: any): any {
      return this._sanitizer.bypassSecurityTrustHtml(value);
    }

}

I want to test it as below:

describe('Pipe: Sanatiser', () => {
    let pipe: SanitiseHtmlPipe;

    beforeEach(() => {
        pipe = new SanitiseHtmlPipe(new DomSanitizer());
    });

    it('create an instance', () => {
        expect(pipe).toBeTruthy();
    }); 
});

The DomSanatizer is an abstract class which is autowired by typescript by passing it into a constructor:

constructor(private _sanitizer: DomSanitizer) {}

Currently I get the typescript errror:

Cannot create an instance of the abstract class 'DomSanitizer'.

Does anyone know what typescript does when instantiating dependencies passed into a constructor in Angular? Or what the way to test something like this is?

Jota.Toledo
  • 27,293
  • 11
  • 59
  • 73
Ben Taliadoros
  • 7,003
  • 15
  • 60
  • 97

3 Answers3

71

Because of the DI in your pipe, you need to configure a test environment (test bed) to resolve the dependency:

import { BrowserModule, DomSanitizer } from '@angular/platform-browser';
import { inject, TestBed } from '@angular/core/testing';

describe('SanitiseHtmlPipe', () => {
  beforeEach(() => {
    TestBed
      .configureTestingModule({
        imports: [
          BrowserModule
        ]
      });
  });

  it('create an instance', inject([DomSanitizer], (domSanitizer: DomSanitizer) => {
    let pipe = new SanitiseHtmlPipe(domSanitizer);
    expect(pipe).toBeTruthy();
  })); 
});
CodeMonkey
  • 3,271
  • 25
  • 50
Jota.Toledo
  • 27,293
  • 11
  • 59
  • 73
  • I actually got the test running by commenting out the line: TestBed.resetTestEnvironment(); Remove this and I'll mark correct – Ben Taliadoros Nov 28 '17 at 15:27
  • I also needed to add `import { BrowserModule, DomSanitizer } from '@angular/platform-browser';` – Csaba Toth Sep 29 '18 at 06:02
  • @Jota.Toledo What's the difference between `new SanitiseHtmlPipe(new DomSanitizer())` and injecting it like you did above. I am testing the pipe that has a custom service injected into it and I mocked my service and injected it into pipe like this: `const pipe = new DataHealthPipe(new DateServiceMockBefore17thApril());` It is working for me, but I wonder whether this is the best approach. I had to mock the service 3 times, so each time its `getFullDate()` method returns different date, for each test case. – codeepic Dec 05 '18 at 13:08
  • 1
    @codeepic _"What's the difference between new SanitiseHtmlPipe(new DomSanitizer()) and injecting it like you did above"_ its not possible to use new as the class is abstract – Jota.Toledo Dec 05 '18 at 13:42
  • Depending on the kind of dependencies that the SUT has, one can choose between [real or mock objects](https://martinfowler.com/articles/mocksArentStubs.html). In the original OP case, mocking the `DomSanitizer` feels like an overkill, as the implementation is really straightforward. In your case, I cant tell if your service is complex enough to be worth mocking or not... – Jota.Toledo Dec 05 '18 at 13:45
  • @Jota.Toledo - ok, you can't new up abstract classes, makes sense to use inject. In my case the `getFullDate()` method in mocked service relies on member variables of the class, some of them Observables that are set by user. I don't want to pull the whole gorilla and a tree if I want a banana - I only need the return object from the method, so mocking the `DateService` class and returning different values from `DateService.getFullDate()` 3 times for each test case seemed like lesser evil, even when I have to mock the class and its method 3 times. – codeepic Dec 05 '18 at 16:12
  • 1
    @codeepic doesnt sound that complex. I dont know exactly what you mean by _mock the class and its method 3 times_, but my approach would be to _provide_ a mock object and then _spy_ with jasmine on the _getFullDate()_ method and return what you need for your tests. Feel free to open a new question and tag me on it – Jota.Toledo Dec 05 '18 at 19:53
  • 1
    @Jota.Toledo Thanks for the tip with the `spy` and `returnValue` - I wasn't aware of that - I need to check Jasmine docs to see on what other goodies I am missing. I refactored my unit tests. – codeepic Dec 06 '18 at 10:01
  • Very good answer. This was the only one, which worked with my pipe which uses DOCUMENT as an injection token. – PeterB Apr 13 '23 at 12:03
19

just in case anyone would like to reuse the constructor of the Pipe, you can use the TestBed to acheive the same result :

  let pipe: SafeHtmlPipe;
  let sanitized: DomSanitizer

  beforeEach(async() => {
    TestBed.configureTestingModule({
      providers: [DomSanitizer]
    });
    sanitized = TestBed.get(DomSanitizer);
    pipe = new SafeHtmlPipe(sanitized);
  });

  it('create an instance', () => {
    expect(pipe).toBeTruthy();
  });
Alain Boudard
  • 768
  • 6
  • 16
  • 3
    but it seems `sanitized` is not usable: `this.sanitizer.bypassSecurityTrustHtml is not a function`. This is the error when I try to use the pipe. – martin-g Nov 06 '19 at 11:03
  • 2
    Well, as far as I can tell, it's not a matter of method, this is because for whatever reason, this function and others are static, and therefore, you can't call them like that in your test. If I'm not wrong, you need to mock them like this if you want to test the methods calls :
    TestBed.configureTestingModule({
          imports: [BrowserModule],
          providers: [
            {provide: DomSanitizer, useValue: {
                sanitize: () => 'safeString',
                bypassSecurityTrustHtml: () => 'safeString'
              }
            }
          ]
        });
    – Alain Boudard Nov 06 '19 at 13:50
  • sorry, abstract methods, not static – Alain Boudard Nov 06 '19 at 14:11
  • 1
    Is there a way to inject the DomSanitizerImpl that is actually used in non-test code ? – martin-g Nov 06 '19 at 14:43
4

In case you want to mock the whole providers and don't wanna use the constructor, this is how I do it (with Jest but replace the spy with your regular jasmine.createSpyObj)

spec

describe("MyPipe", () => {
  let pipe: MyPipe;
  const myServiceSpy = { myFunction: jest.fn() };

  beforeEach(() => {
    jest.clearAllMocks();
    TestBed.configureTestingModule({
      providers: [
        MyPipe,
        {
          provide: MyService,
          useValue: myServiceSpy
        }
      ]
    });

    pipe = TestBed.inject(myPipe);
  });

  it("create an instance", () => {
    expect(pipe).toBeTruthy();
  });
});

pipe

@Pipe({
  name: "myPipe"
})
export class MyPipe implements PipeTransform {
  constructor(private readonly myService: MyService) {}

  transform(value: Item): boolean {
    // stuff with your service
    return true;
  }
}
Yohan Dahmani
  • 1,670
  • 21
  • 33