How to Unit Test a Component that extends a base Class
See original GitHub issueI’m unable to get my Jasmine tests to work on my Component that extends a base class. I have tried using a mock for the base class, but it seems to be ignored.
Here’s the SupplierComponent
(simplified) that I need to test. See how it extends the BaseEditComponent
import { Component } from '@angular/core';
import { BaseEditComponent } from '../base-edit.component';
@Component({
selector: 'app-supplier',
templateUrl: './supplier.component.html',
styleUrls: ['./supplier.component.scss'],
})
export class SupplierComponent extends BaseEditComponent {
constructor() {
super();
}
}
BaseEditComponent
is defined as just an abstract class (not a Component). It has a bunch of extra dependencies that are out of scope for testing the SupplierComponent
. A similar inheritance setup is discussed in this blog entry.
Here’s how I have configured my TestBed. See how I have told it to useClass MockBaseEditComponent
instead of BaseEditComponent
in the providers.
import { } from 'jasmine';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SupplierComponent } from './supplier.component';
import { BaseEditComponent } from '../base-edit.component';
export class MockBaseEditComponent {
}
describe('component: SupplierComponent', () => {
let component: SupplierComponent;
let fixture: ComponentFixture<SupplierComponent>;
let editService: ProgramEditService;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [SupplierComponent],
providers: [
{ provide: BaseEditComponent, useClass: MockBaseEditComponent },
],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SupplierComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeDefined();
});
});
When I run, I get an exception because Jasmine has loaded the real BaseEditComponent
which has an unfulfilled dependency, instead of loading my empty MockBaseEditComponent
.
Issue Analytics
- State:
- Created 6 years ago
- Reactions:12
- Comments:8
Top GitHub Comments
Angular does not provide a way to replace the base class with a stub. However, I have discovered that isn’t really necessary.
We can create a Jasmine Spy for each of the methods in the base class, and verify they have been called. That relieves us from testing all of the base class implementation when we just want to focus on the sub class.
spyOn(component, methodInBase).and.callThrough()
If there are injected dependencies to the sub class, or the super class, we do have the chance to replace these with Mocks using the
TestBed.configureTestingModule
. If our sub class has its own constructor, the Mocks will be injected there and passed to the super class with the call tosuper()
found in the subclass’s constructor. If our sub class doesn’t have its own constructor, Angular will call the base class’s constructor directly, with the injected Mock services we have set up.Therefore, no special setup or config is needed to unit test this situation. We just need to make sure to mock all the dependencies and they will be injected as expected, whether it’s in the base or the sub.
For a large project with lots of classes that extend a base class, I have found that it is useful to create Mocks for each of my injected services. For a service named
supplier.service.ts
beside that in the same folder I am now in the habit of making asupplier.service.mock.ts
which has the same interface as the real service but has only Jasmine Spys for all of the method implementations. Then, each time I need to test a component that extends a base, I just inject all the mocks that are needed by the base with theTestBed.configureTestingModule
. There is some boiler plate setup that appears in all of my tests, but at least the mock itself is just implemented in one place.Finally, if you do decide to create
*.mock.ts
files for your services, they need to be registered somewhere to get your AOT build to work. I created a separate module just for unit testing, where all of my mock services are registered. (this isn’t necessary in Angular 6 CLI-generated projects which have separate tsconfig files for testing. in that case just add *.mock.ts to all the same places you see *.spec.ts configured in both tsconfig files).One other thing I noticed, by carefully analyzing my base components that I use, is that for the most part these could be replaced with a base service instead. There isn’t really much benefit that my component gets from being composed with a base component. All my base is really doing is calling things on services. So, if I was going to write my app all over again, I would try to avoid component inheritance altogether, and if I think I need any common functionality I would place that into a service or interface instead.
Another important technique we have is TypeScript Interfaces. If we notice a group of components is similar, we can create an Interface that describes the common functionality. Then when we go to make a new component we easily conform to the contract by implementing the Interface.
Closing this issue, since this does not need attention from Angular team.
The bottom line is, Angular doesn’t have a way to replace a base class with a mock. But, we shouldn’t need to. Instead, we can use spies for methods on the base component, and/or we can use mocks for the dependencies of the sub class and the base class.