9

I am trying to write a unit test for a component in Angular 1.5. I want to unit test that component and its dom nodes. This component contains a child component that is quite complex.

My goal is to unit test the outer component without compiling the child component.

Since I want to test the DOM as well, it is not sufficient to use $componentController for this test.

Here is a short example of what I would like to achieve:

Component code:

angular.module('app').component('myComponent', {
  bindings: {
    username: '<',
  },
  template: `
    <span>{{ $ctrl.username }}</span>
    <my-complex-component />
  `
  controller: function () {}
});

Unit test for my-component:

it('my-component should render username', function () {
  var template = '<my-component username="username"></my-component>',
    element,
    scope,
    date;

  scope = $rootScope.$new();
  scope.username = 'John';

  element = $compile(template)(scope);
  scope.$digest();

  username = element.find('span');
  expect(username.text()).to.be.equal('John');
});

my-complex-component should not be instantiated. It should resist in the template as it is. Creating the element in the unit test should result in

<span>John</span>
<my-complex-component />

Is there any way to do this?

Pascal Chorus
  • 360
  • 4
  • 12
  • As I know it works exactly how you expected. When you compile your component it doesn't compile inner components. Just look at `console.log(element)`. – Jake Blues Dec 20 '16 at 16:04
  • Unfortunately, my-component and my-complex-component are both located in module app. Since I have to call angular.mock.module('app') to mock the module, it loads both components and it tries to compile my-complex-component as well. – Pascal Chorus Dec 20 '16 at 20:13
  • you can try and mock `my-complex-component`. See [this answer](http://stackoverflow.com/questions/17533052/how-do-you-mock-directives-to-enable-unit-testing-of-higher-level-directive#answer-21923462) on mocking a directive but use [$compileProvider.component](https://docs.angularjs.org/api/ng/provider/$compileProvider#component) instead – logee Mar 03 '17 at 00:55

4 Answers4

4

Here is a very good explanation of how to test a component without rendering the whole tree under the current component.

What you want is called

shallow rendering (avoid rendering the whole template of the child component)

and Angular 1.5 doesn’t offer it out-of-the-box.

If you take a look at the link above, Wojciech Zawistowski shows how to use the helper below in order to achieve shallow rendering.

export function componentSpyOn(name) {
  function componentSpy($provide) {
    componentSpy.bindings = [];

    $provide.decorator(name + 'Directive', ($delegate) => {
      let component = $delegate[0];

      component.template = '';
      component.controller = class {
        constructor() {
          componentSpy.bindings.push(this);
        }
      };

      return $delegate;
    });
  }

  return componentSpy;
}
tottomotto
  • 2,193
  • 2
  • 22
  • 29
3

You can do this by overriding your child component with $compileProvider.directive even though it is a component, because a component is a special kind of directive.

In your unit tests register a new component with the compiler with the same name as your child component but do not provide a controller and set the template to an empty string. This will result in a component without any logic/complexity:

beforeEach(module('app', function($compileProvider){
    $compileProvider.directive('myComplexComponent', function (){
        return {
            priority: 9999,
            terminal: true,
            template: ''
        };
    });
}));

So, the resulting html will be the most ideal for your outer component unit tests:

<span>John</span>    
Rui Lopes
  • 106
  • 1
  • 12
0

There is a way to test it but the final result will be:

<span>John</span>
<ng-transclude></ng-transclude>

Component code:

  angular.module('app').component('myComponent', {
  enter code herebindings: {
    username: '<',
  },
  template: `
    <span>{{ $ctrl.username }}</span>
    <ng-transclude></ng-transclude>
  `,
  transclude: true, // Added property
  controller: function () {}
});

As you can see I deleted <my-complex-component>, and added <ng-transclude> instead.

That means that you can add your information from the outside and it will be injected at the location of ng-transclude.

For example this in your main html:

<my-component>
    <my-complex-component></my-complex-component>
</my-component>

Will be in the DOM in the way that you wanted from the start:

<span>John</span>
<my-complex-component></my-complex-component>

After that your test should work.

I hope this answer is what you wanted and that it will help you.

Alon
  • 112
  • 1
  • 7
  • 3
    I'm not sure changing the component's markup is the right approach here. It makes the markup less intuitive. Also, what if there's more than one child component? – isherwood Feb 28 '17 at 15:26
  • I think it's a better approach if you want your components to be more testable, and with this way you can put as many components inside your main one, without breaking your tests. This way you can see that your main component still works (or not) when things start to break. – Alon Feb 28 '17 at 21:34
0

You could just overwrite the child components module definition in the unit test with an empty module.

// Unit Test Parent component
describe('Unit Testing: myComponent Component', () => { 

  beforeEach(() => {
    // overwrite module
    angular.module('myComplexComponent', []);
    module('myComponent');
  });
});

Now the parent component would still try to inject and render the child component, but as this child component is completely empty nothing will happen.