kdocs
GitHub
Lang - Web
Lang - Web
  • Base
    • Css
    • Javascript
    • Typescript
      • New Project
  • Frameworks
    • Angular
      • Directives
      • Components
      • Templates
        • Bindings
        • Control Flow
        • Variables
      • Signals
      • Pipes
      • Services
        • Dependency Injection
      • Forms
        • Reactive Form
        • Template-Driven Form
      • Router
      • HTTP Client
      • Observables RxJS
      • Testing
        • Components
        • Directives
        • Pipes
        • Services
      • Optimization & Performance
      • Security
Powered by GitBook
On this page
  • Isolated Services
  • Services with Dependencies
  • Injecting with new
  • Injecting with spies
  • Services with TestBed
  • Simple Service test
  • Injecting other services as dependencies
  • Testing HTTP Services
  • Test with spies
  • Http Testing Library
  1. Frameworks
  2. Angular
  3. Testing

Services

PreviousPipesNextOptimization & Performance

Last updated 5 months ago

Isolated Services

You may test isolated Services that don't depend on any other Services like this.

describe("", () => {
    let service: MasterService;

    beforeEach(() => {
        service = new MasterService();
    });

    it("", () => {
        expect(service.getValue()).toBe("");
    });

    it("", () => {
        service.getPromiseValue().then((value) => {
            expect(value).toBe("");
            done();
        });
    });
});

Services with Dependencies

Services often depend on other services that Angular inject into the constructor.

Create and inject these dependencies by hand.

These standard testing techniques are great for unit testing services in isolation.

However, you almost always inject services into application classes using Angular dependency injection and you should have tests that reflect that usage pattern.

Angular testing utilities, like TestBed make it straightforward to investigate how injected services behave.

Prefer using Services with TestBed.

Injecting with new

Only use this way if testing real simple Services.

Prefer injecting with spies as they are usually a better way to mock services.

describe("", () => {
    let masterService: MasterService;

    it('#getValue should return real value from the real service', () => {
        masterService = new MasterService(new ValueService());
        expect(masterService.getValue()).toBe('real value');
    });
    
    it('#getValue should return faked value from a fakeService', () => {
        masterService = new MasterService(new FakeValueService());
        expect(masterService.getValue()).toBe('faked service value');
    });
    
    it('#getValue should return faked value from a fake object', () => {
        const fake = {getValue: () => 'fake value'};
        masterService = new MasterService(fake as ValueService);
        expect(masterService.getValue()).toBe('fake value');
    });
});

Injecting with spies

describe("", () => {
    let masterService: MasterService;

    it('#getValue should return stubbed value from a spy', () => {
        // Create `getValue` spy on an object (in this case method) representing the ValueService
        const valueServiceSpy = jasmine.createSpyObj('ValueService', ['getValue']);
        
        // Set the value to return when the `getValue` spy is called.
        const stubValue = 'stub value';
        valueServiceSpy.getValue.and.returnValue(stubValue);
        
        masterService = new MasterService(valueServiceSpy);
        
        expect(masterService.getValue()).withContext('service returned stub value').toBe(stubValue);
        expect(valueServiceSpy.getValue.calls.count())
            .withContext('spy method was called once')
            .toBe(1);
        expect(valueServiceSpy.getValue.calls.mostRecent().returnValue).toBe(stubValue);
    });
});

Services with TestBed

Angular DI handle the discovery and creating of dependant services.

As a service consumer, you don't worry about the order of constructor arguments or how they are created. For this you use TestBed testing utility to provide and create services.

Simple Service test

  1. You set the providers property with an array of the services that you'll test or mock.

  2. Then inject it inside a test by calling TestBed.inject() with the service class as the argument.

// This is how the `ValueService` testing file should be
describe("", () => {
    let service: ValueService;

    beforeEach(() => {
        TestBed.configureTestingModule({ providers: [ValueService] });
    });

    it("", () => {
        service = TestBed.inject(ValueService);
        expect(service.getValue()).toBe("");
    });
});

Injecting other services as dependencies

When testing a service with a dependency, provide the mock in the providers array.

describe("", () => {
    let masterService: MasterService;
    let valueServiceSpy: jasmine.SpyObj<ValueService>;

    beforeEach(() => {
        const spy = jasmine.createSpyObj("ValueService", ["getValue"]);

        TestBed.configureTestingModule({
            // Provide both the service-to-test and it's spy dependency
            providers: [MasterService, { provide: ValueService, useValue: spy }],
        });

        masterService = TestBed.inject(MasterService);
        valueServiceSpy = TestBed.inject(ValueService) as jasmine.SpyObj<ValueService>;
    });

    it('#getValue should return stubbed value from a spy', () => {
        const stubValue = 'stub value';
        valueServiceSpy.getValue.and.returnValue(stubValue);
        
        expect(masterService.getValue()).withContext('service returned stub value').toBe(stubValue);
        expect(valueServiceSpy.getValue.calls.count())
            .withContext('spy method was called once')
            .toBe(1);
        expect(valueServiceSpy.getValue.calls.mostRecent().returnValue).toBe(stubValue);
    });
});

Data Services that make HTTP calls to remote servers typically inject and delegate to the Angular HttpClient service for XHR calls.

You can test a data service with an injected HttpClient spy as you would test any service with a dependency.

Test with spies

The subscribe() method takes a success next and fail error callback.

Make sure to provide both callbacks so that you capture errors.

Neglecting to do so produces an asynchronous uncaught observable error that the test runner will likely attribute to a completly different test.

describe("", () => {
    let httpClientSpy: jasmine.SpyObj<HttpClient>;
    let service: ServiceValue;

    beforeEach(() => {
        httpClientSpy = jasmine.createSpyObj("HttpClient", ["get"]);
        service = new ServiceValue(httpClientSpy);
    });

    it("should return expected values (HttpClient called once)", () => {
        const expectedHttpResponse = [{ id: 1, value: "" }];
        httpClientSpy.get.and.returnValue(expectedHttpResponse);
        
        service.getValues().subscribe({
            next: (values) => {
                expect(values).withContext("expected values").toEqual(expectedHttpResponse);
                done();
            },
            error: done.fail,
        });
        
        expect(httpsClientSpy.get.calls.count()).withContext("one call").toBe(1);
    });

    it("should return an error when the server returns a 404", (done: DoneFn) => {
        const errorResponse = new HttpErrorResponse({
            error: "test 404 error",
            status: 404,
            statusText: "Not Found",
        });
        httpClientSpy.get.and.returnValue(asyncError(errorResponse));
        
        service.getValues().subscribe({
            next: (values) => done.fail("expected an error, not values"),
            error: (error) => {
                expect(error.message).toContain("test 404 error");
                done();
            },
        });
    });
});

Extended interactions between a data service and the HttpClient can be complex and difficult to mock with spies.

Test the service

Lets suppose we have a Service that hits an endpoint url/fruits.

api.service.ts
@Injectable({
    providedIn: 'root'
})
export class ApiService {
    private httpClient = inject(HttpClient);
    
    getFruits() {
        return this.httpClient.get<string[]>('url/fruits');
    }
}
api.service.specs.ts
describe('ApiService', () => {
    let service: ApiService;
    let httpMock: HttpTestingController;

    beforeEach(() => {
        TestBed.configureTestingModule({
            // In the docs it imports different ones
            providers: [HttpClientTestingModule],
        });
        service = TestBed.inject(ApiService);
        httpMock = TestBed.inject(HttpTestingController);
    });
    
    afterEach(() => {
        // Finally, we can assert that no other requests were made.
        httpMock.verify();
    });
    
    it('should be created', () => {
        expect(service).toBeTruthy();
    });

    it('should fetch data and return list of fruits', () => {
        const expectedFruits = ['grapes', 'pineapple'];
        
        service.getFruits().subscribe((response) => expect(response).toEqual(expectedFruits));
        
        // At this point, the request is pending, and we can assert it was made via the `HttpTestingController`
        const req = httpMock.expectOne('url/fruits');
        
        // We can assert various properties of the request if desired.
        expect(req.request.method).toBe('GET');
        
        // Flushing the request causes it to complete, delivering the result.
        req.flush(expectedFruits);
    });
});

Testing error handling

To test handling of backend errors (when the server returns a non-successful status code), flush requests with an error response that emulates what your backend would return when a request fails.

const req = httpMock.expectOne('url/fruits');
req.flush('Failed!', { status: 500, statusText: 'Internal Server Error' });

Testing Interceptors

Testing HTTP Services
Http Testing Library
More on the docs.
AngularAngular
testing-services
Logo