Friday, March 16, 2018

Testing Angular Components That Use Pipes

Pipes are a common feature in Angular that are often used to do things like format data (read more about Pipes in the Angular documentation). Unfortunately, when we use Pipes in our Components they can cause some issues with testing. In order to test a Component that uses a Pipe you need to either bring the Pipe in to the TestBed or (my preference) create a Fake Pipe and instruct your test to use that instead.

I prefer this method because it means I can control the functionality of the Pipe, which usually means replacing whatever the Pipe actually does with nothing. I can also more easily manipulate my tests to provide expected outputs, which is always helpful.

Let's say you have a Component whose template uses a Pipe to translate something. The real TranslatePipe accepts a parameter called "query", looks up the query value in a file, and returns the value found in the file. It's basically a key/value lookup. When we're unit testing we want to make sure we're testing the smallest possible unit, which means we should have already tested the real TranslatePipe. Since we can trust that the real TranslatePipe works as expected, we don't want to actually use the TranslatePipe in our Component tests. Instead we'll create a fake TranslatePipe.

When we use the Angular CLI to generate our components we also get a spec file for that component, which has some boilerplate code in it that probably looks something like this:
/* Import statements up here */

describe('CoolComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [CoolComponent]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(CoolComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});
Let's say the very first thing we're going to do on our CoolComponent is use our TranslatePipe to display a title on the page. We know the title of the page will need to be translated and we know we have the TranslatePipe. What we'll want to test is whether the TranslatePipe is invoked during rendering.
it('should display translated title', () => {});
Before we can actually write our test we'll need to create our FakeTranslatePipe and instruct the TestBed to use it instead of the real TranslatePipe. In the same file*, before the describe block, but after the imports, add the following code:
@Pipe({name: 'translate'})
export class FakeTranslatePipe implements PipeTransform {
  transform(query: string): any {
    return 'Translated!';
  }
}
Now that we've created the FakeTranslatePipe, we'll instruct the TestBed to use it by supplying it to the declarations array. The updated beforeEach looks like this:
beforeEach(async(() => {
  TestBed.configureTestingModule({
    declarations: [
      CoolComponent,
      FakeTranslatePipe
    ]
  })
  .compileComponents();
}));
When our component markup uses the translate pipe, our FakeTranslatePipe will be used instead of the real TranslatePipe. That's all we have to do to avoid an error, but if we want to test whether the Pipe was actually invoked during rendering we'll need to create a spy on it. Create a variable for the spy inside the describe block, but outside the first beforeEach.
let pipeSpy: jasmine.Spy;
After we compile the components, but while we're still inside the first beforeEach, we'll spy on FakeTranslatePipe's translate function. Because we're spying on a function that exists on a class instead of an instance we'll have to spy on the prototype of FakeTranslatePipe. It's easy enough to do once you know to do it. The spy creation looks like this:
pipeSpy = spyOn(FakeTranslatePipe.prototype, 'transform').and.callThrough();
Now we can treat pipeSpy like other spy (that is, we can check whether it was called, how many times, with what parameters, etc.) in our tests. Here's what our test might look like now:
it('should display translated title', () => {
  expect(pipeSpy).toHaveBeenCalledTimes(1);
  expect(pipeSpy).toHaveBeenCalledWith('page-title');
});
We're really close to being finished, but all we know is that our FakeTranslatePipe was invoked. We don't know whether we put the result in an h1 tag. We can add that to our test so the end result looks like this:
it('should display translated title', () => {
  const debugElements = fixture.debugElement.queryAll(By.css('h1'));

  expect(debugElements.length).toBe(1);
  expect(debugElements[0].nativeElement.textContent).toBe('Translated!');
  expect(pipeSpy).toHaveBeenCalledTimes(1);
  expect(pipeSpy).toHaveBeenCalledWith('page-title');
});
The last thing I'll leave you with is the final version of the whole spec file.
import { Pipe, PipeTransform } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';

import { CoolComponent } from './cool-component';

@Pipe({name: 'translate'})
export class FakeTranslatePipe implements PipeTransform {
  transform(query: string): any {
    return 'Translated!';
  }
}

describe('CoolComponent', () => {
  let pipeSpy: jasmine.Spy;
  let component: CoolComponent;
  let fixture: ComponentFixture<CoolComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [
        CoolComponent,
        FakeTranslatePipe
      ]
    })
    .compileComponents();
    
    pipeSpy = spyOn(FakeTranslatePipe.prototype, 'transform').and.callThrough();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(CoolComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should display translated title', () => {
    const debugElements = fixture.debugElement.queryAll(By.css('h1'));
    
    expect(debugElements.length).toBe(1);
    expect(debugElements[0].nativeElement.textContent).toBe('Translated!');
    expect(pipeSpy).toHaveBeenCalledTimes(1);
    expect(pipeSpy).toHaveBeenCalledWith('page-title');
  });
});



*If we know we'll be using the same FakeTranslatePipe in lots of different spec files throughout the project, we could create it in a separate file and import it, but for the purposes of this post we're going to create it in the same file.

2 comments:

  1. Answers I Couldn'T Find Anywhere Else: Testing Angular Components That Use Pipes >>>>> Download Now

    >>>>> Download Full

    Answers I Couldn'T Find Anywhere Else: Testing Angular Components That Use Pipes >>>>> Download LINK

    >>>>> Download Now

    Answers I Couldn'T Find Anywhere Else: Testing Angular Components That Use Pipes >>>>> Download Full

    >>>>> Download LINK 9K

    ReplyDelete