javascript mock - How to Unit Test a Directive In Angular 2?




testing components (3)

Took me a while to find a good example, a good person on angular gitter channel pointed me to look at the Angular Material Design 2 repository for examples. You can find a Directive test example here. This is the test file for the tooltip directive of Material Design 2. It looks like you have to test it as part of a component.

Problem: I would like to be able to unit test a directive in Angular 2 to make sure that it properly compiles.

In Angular 1, it was possible to use$compile(angular.element(myElement) service and call $scope.$digest() after that. I specifically want to be able to do this in unit tests so I could test that when Angular ends up running across <div my-attr-directive/> in the code that my-attr-directive compiles.

Constraints:


Testing compiled directive using TestBed

Let's say you have a following directive:

@Directive({
  selector: '[my-directive]',
})
class MyDirective {
  public directiveProperty = 'hi!';
}

What you have to do, is to create a component that uses the directive (it can be just for testing purpose):

@Component({
  selector: 'my-test-component',
  template: ''
})
class TestComponent {}

Now you need to create a module that has them declared:

describe('App', () => {

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [
        TestComponent,
        MyDirective
      ]
    });
  });

  // ...

});

You can add the template (that contains directive) to the component, but it can be handled dynamically by overwriting the template in test:

it('should be able to test directive', async(() => {
  TestBed.overrideComponent(TestComponent, {
    set: {
      template: '<div my-directive></div>'
    }
  });

  // ...      

}));

Now you can try to compile the component, and query it using By.directive. At the very end, there is a possibility to get a directive instance using the injector:

TestBed.compileComponents().then(() => {
  const fixture = TestBed.createComponent(TestComponent);
  const directiveEl = fixture.debugElement.query(By.directive(MyDirective));
  expect(directiveEl).not.toBeNull();

  const directiveInstance = directiveEl.injector.get(MyDirective);
  expect(directiveInstance.directiveProperty).toBe('hi!');
}); 

# Old answer:

To test a directive you need to create a fake component with it:

@Component({
  selector: 'test-cmp',
  directives: [MyAttrDirective],
  template: ''
})
class TestComponent {}

You can add the template in the component itself but it can be handled dynamically by overwriting the template in test:

it('Should setup with conversation', inject([TestComponentBuilder], (testComponentBuilder: TestComponentBuilder) => {
    return testComponentBuilder
      .overrideTemplate(TestComponent, `<div my-attr-directive></div>`)
      .createAsync(TestComponent)
      .then((fixture: ComponentFixture<TestComponent>) => {
        fixture.detectChanges();
        const directiveEl = fixture.debugElement.query(By.css('[my-attr-directive]'));
        expect(directiveEl.nativeElement).toBeDefined();
      });
  }));

Note that you're able to test what directive renders but I couldn't find the way to test a directive in a way components are (there is no TestComponentBuilder for directives).


Based on all the answers which were mainly correct and taking into account the best practices suggested (especially not using Array.prototype directly), I came up with the below code:

function arrayWithout(arr, values) {
  var isArray = function(canBeArray) {
    if (Array.isArray) {
      return Array.isArray(canBeArray);
    }
    return Object.prototype.toString.call(canBeArray) === '[object Array]';
  };

  var excludedValues = (isArray(values)) ? values : [].slice.call(arguments, 1);
  var arrCopy = arr.slice(0);

  for (var i = arrCopy.length - 1; i >= 0; i--) {
    if (excludedValues.indexOf(arrCopy[i]) > -1) {
      arrCopy.splice(i, 1);
    }
  }

  return arrCopy;
}

Reviewing the above function, despite the fact that it works fine, I realised there could be some performance improvement. Also using ES6 instead of ES5 is a much better approach. To that end, this is the improved code:

const arrayWithoutFastest = (() => {
  const isArray = canBeArray => ('isArray' in Array) 
    ? Array.isArray(canBeArray) 
    : Object.prototype.toString.call(canBeArray) === '[object Array]';

  let mapIncludes = (map, key) => map.has(key);
  let objectIncludes = (obj, key) => key in obj;
  let includes;

  function arrayWithoutFastest(arr, ...thisArgs) {
    let withoutValues = isArray(thisArgs[0]) ? thisArgs[0] : thisArgs;

    if (typeof Map !== 'undefined') {
      withoutValues = withoutValues.reduce((map, value) => map.set(value, value), new Map());
      includes = mapIncludes;
    } else {
      withoutValues = withoutValues.reduce((map, value) => { map[value] = value; return map; } , {}); 
      includes = objectIncludes;
    }

    const arrCopy = [];
    const length = arr.length;

    for (let i = 0; i < length; i++) {
      // If value is not in exclude list
      if (!includes(withoutValues, arr[i])) {
        arrCopy.push(arr[i]);
      }
    }

    return arrCopy;
  }

  return arrayWithoutFastest;  
})();

How to use:

const arr = [1,2,3,4,5,"name", false];

arrayWithoutFastest(arr, 1); // will return array [2,3,4,5,"name", false]
arrayWithoutFastest(arr, 'name'); // will return [2,3,4,5, false]
arrayWithoutFastest(arr, false); // will return [2,3,4,5]
arrayWithoutFastest(arr,[1,2]); // will return [3,4,5,"name", false];
arrayWithoutFastest(arr, {bar: "foo"}); // will return the same array (new copy)

I am currently writing a blog post in which I have benchmarked several solutions for Array without problem and compared the time it takes to run. I will update this answer with the link once I finish that post. Just to let you know, I have compared the above against lodash's without and in case the browser supports Map, it beats lodash! Notice that I am not using Array.prototype.indexOf or Array.prototype.includes as wrapping the exlcudeValues in a Map or Object makes querying faster! (https://jsperf.com/array-without-benchmark-against-lodash)





javascript angularjs unit-testing angular angular2-directives