6

I'm new to unit testing in the context of an Angular 5 application. And right now, I'm trying to unit test a basic component.

The component is called CardComponent, and within the HTML of this component, I call the CheckboxComponent.
So here's the HTML of the CardComponent:

<div>
    <p>Test</p>
    <jg-checkbox [label]="'Test label'"></jg-checkbox>
</div>

As you can see, there's nothing complicated going on.

However, the CheckboxComponent does inject a service. For the sake of this question, I'll just call it TestService.

So when I unit test my CardComponent, here's my testbed:

TestBed.configureTestingModule({
    declarations: [
        CheckboxComponent
    ]
}).compileComponents();

Then I run this test:

it('should create', () => {
    expect(component).toBeTruthy();
});

This is just the default test that gets created through the CLI.

But now, it complains that there's no provider for the TestService. Am I really supposed to inject (and mock/spy) that as well?

That seems a bit backwards because I only care about the CardComponent, I shouldn't have to care about the CheckboxComponent, right? That's the whole point of unit testing.

Otherwise, since Angular has hierarchical components, I might have to go down many levels deep as my app grows.

This can't be right.

Can someone please help with this issue? I appreciate the help!

gjvatsalya
  • 1,129
  • 13
  • 29

3 Answers3

3

If there's no need to reference the CheckboxComponent in the CardComponent, there are two approaches:

  • the CheckboxComponent can be stubbed
  • NO_ERRORS_SCHEMA can be used in TestBed.configureTestingModule({})

The documentation has a section about Nested component tests

There is also an answer concerning Shallow component tests

Stubbing

Create a stub component in card.component.spec.ts.

@Component({selector: 'jg-checkbox', template: ''})
class CheckboxStubComponent {}

Then declare this in TestBed.configureTestingModule({}).

TestBed.configureTestingModule({
  declarations: [
    CardComponent,
    CheckboxStubComponent
  ]
})

NO_ERRORS_SCHEMA

NO_ERRORS_SCHEMA can be used instead of stubs.

TestBed.configureTestingModule({
  declarations: [
    CardComponent
  ],
  schemas: [ NO_ERRORS_SCHEMA ]
})

The NO_ERRORS_SCHEMA tells the Angular compiler to ignore unrecognized elements and attributes.

Either approach is acceptable. However, the documentation has a warning about overusing NO_ERRORS_SCHEMA.

The NO_ERRORS_SCHEMA also prevents the compiler from telling you about the missing components and attributes that you omitted inadvertently or misspelled. You could waste hours chasing phantom bugs that the compiler would have caught in an instant.

It also mentions that stubs have an additional advantage.

The stub component approach has another advantage. While the stubs in this example were empty, you could give them stripped-down templates and classes if your tests need to interact with them in some way.

It goes on further to show how to use both approaches together depending on the needs of the test.

josavish
  • 461
  • 1
  • 4
  • 9
0

Yes, you need to create a mock service that covers every service function call your component makes in the ngOnInit() function..

export class MockTestService {
    public myFunction() {

   }
}

...
describe('TheComponent', () => {
   const svc = new MockTestService();

...
TestBed.configureTestingModule({
declarations: [],
providers: [{provide: TestService, useValue: svc}]
}

}).compileComponents();

Kevin
  • 827
  • 1
  • 7
  • 18
  • Unless I'm doing something wrong, it looks like it's not just restrict to ngOnInit(). As long as it gets injected into a component, I'll need to mock. That could huge number of mocks if I have big hierarchy of components. – gjvatsalya Apr 13 '18 at 03:33
  • yes, your app component will have dang near everything your app uses. So your components that have other components inside of them are a little tougher to test. Sibling components don't require having to do this, just components inside other components – Kevin Apr 13 '18 at 17:52
  • Dang, that sucks. Thanks for letting me know! – gjvatsalya Apr 13 '18 at 21:18
0

While @josavish's answer is correct if you are just using TestBed, NO-ERRORS-SCHEMA masks errors in your template and manually created stub-components can get out-of-sync quickly causing your tests to pass when they should fail (again masking real errors)!

This is exactly the reason I wrote shallow-render. With Shallow, mocking the checkbox component would be automatic.

Setup your tests with:

const shallow: Shallow<CardComponent>;
beforeEach(() => {
  shallow = new Shallow(CardComponent, TheModuleCardComponentIsDeclaredIn);
});

Note that by providing the module, you'll automatically be able to use components that are available from your module (and it's dependencies). Basically, you'll get a mock of jg-checkbox with identical input/output properties.

Then, you may verify the rendering of your component with spec like:

it('renders a checkbox with the correct label', async () => {
  const {findComponent} = await shallow.render();
  const checkbox = findComponent(CheckboxComponent);

  expect(checkbox.label).toBe('Test Label');
});

There are lots of example specs here and a full readme.

Hope this helps!

getsaf
  • 1,761
  • 1
  • 10
  • 8