Test pipe with dependencies on services
Asked Answered
M

3

45

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?

Motheaten answered 27/11, 2017 at 15:27 Comment(4)
check github.com/angular/angular/blob/master/packages/… you should inject an instance of DomSanitizerImplNewlywed
It struggles to find it: Module not found: Error: Can't resolve '@angular/platform-browser/src/security/dom_sanitization_service'Motheaten
looks like its not part of the public apiNewlywed
#39019497Newlywed
N
75

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();
  })); 
});
Newlywed answered 27/11, 2017 at 17:26 Comment(9)
I actually got the test running by commenting out the line: TestBed.resetTestEnvironment(); Remove this and I'll mark correctMotheaten
I also needed to add import { BrowserModule, DomSanitizer } from '@angular/platform-browser';Perjury
@Newlywed 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.Perennate
@Perennate "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 abstractNewlywed
Depending on the kind of dependencies that the SUT has, one can choose between real or mock objects. 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...Newlywed
@Newlywed - 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.Perennate
@Perennate 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 itNewlywed
@Newlywed 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.Perennate
Very good answer. This was the only one, which worked with my pipe which uses DOCUMENT as an injection token.Followthrough
S
20

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();
  });
Stolzer answered 5/11, 2019 at 10:45 Comment(4)
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.Multiply
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 : <pre><code>TestBed.configureTestingModule({ imports: [BrowserModule], providers: [ {provide: DomSanitizer, useValue: { sanitize: () => 'safeString', bypassSecurityTrustHtml: () => 'safeString' } } ] });</code></pre>Stolzer
sorry, abstract methods, not staticStolzer
Is there a way to inject the DomSanitizerImpl that is actually used in non-test code ?Multiply
A
6

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;
  }
}
Accordance answered 5/11, 2020 at 9:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.