Component-level services in Angular and testing
By default, services in Angular are provided at the root module level, as configured by the @Injectable
decorator:
@Injectable({
providedIn: "root",
})
export class SampleService {
// ...
}
This way, the same instance of the service will be injected into any component depending on it:
@Component({
selector: "app-sample",
templateUrl: "./sample.component.html",
styleUrls: ["./sample.component.scss"],
})
export class SampleComponent {
constructor(private sampleService: SampleService) {}
public invokeSample() {
this.sampleService.sampleMethod();
}
}
If a component needs a separate instance of the service for itself and its children, it can change the scope by declaring a service provider in its @Component
decorator:
@Component({
selector: "app-sample",
templateUrl: "./sample.component.html",
styleUrls: ["./sample.component.scss"],
providers: [SampleService],
})
export class SampleComponent {
// ...
}
However, this change also affects dependency injection in tests. If you provide a mock service at the testbed level, it won't get used because the component-level provider has a higher priority:
beforeEach(async () => {
mockService = jasmine.createSpyObj<SampleService>("SampleService", [
"sampleMethod",
]);
await TestBed.configureTestingModule({
declarations: [SampleComponent],
providers: [{ provide: SampleService, useValue: mockService }],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(SampleComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should not use provided mock service", () => {
component.invokeSample();
expect(mockService.sampleMethod).not.toHaveBeenCalled();
});
To override the component-level provider declaration in a test, a new component must be derived from the one under test without the local provider declaration:
@Component({
selector: "app-sample",
templateUrl: "./sample.component.html",
styleUrls: ["./sample.component.scss"],
})
class TestSampleComponent extends SampleComponent {
constructor(sampleService: SampleService) {
super(sampleService);
}
}
For this component, the mock service declared at the testbed level will be injected:
beforeEach(async () => {
mockService = jasmine.createSpyObj<SampleService>("SampleService", [
"sampleMethod",
]);
await TestBed.configureTestingModule({
declarations: [TestSampleComponent],
providers: [{ provide: SampleService, useValue: mockService }],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(TestSampleComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should use provided mock service", () => {
component.invokeSample();
expect(mockService.sampleMethod).toHaveBeenCalled();
});
You can find a working sample with tests in my GitHub repository.
Dependency injection in Angular is very flexible. Services can be scoped to the component level so that each component instance gets a separate service instance. When writing unit tests for such components this behavior can be overridden in a derived component created for the test only so that a mock service can still be injected.