Testing Service in Angular
Overview
The large number of Angular services that are included in an Angular application greatly facilitates communication and contributes to a cleaner code base.
To utilize them as well, a service can inject other services. It's relatively simple to test a standalone service: after obtaining an instance from the injector, we begin to explore its public methods and properties.
Introduction to Testing Services
We only care about evaluating a service's public API, which is the interface that components and other artifacts used to communicate. Testing private symbols is useless unless they have any visible negative impacts. For instance, a public method may call a private one that, as a byproduct, may set a public property.
Three different sorts of tests are available for a service:
- Checking synchronous operations, such as, a method that produces a straightforward array.
- Testing a method that returns an observable or an asynchronous operation.
- Testing dependencies in services, such as an HTTP request method.
Services with Dependencies
Services that Angular injects into the constructor frequently rely on other services. In many instances, you can manually generate and inject these dependencies when running the constructor for the service.
The MasterService is a simple example:
File: app/services/master.service.ts
MasterService delegates its only method, getValue, to the injected ValueService.
Here are several ways to test it.
File: app/services/master.service.spec.ts
In the first test, a ValueService is created using new and given to the constructor of the MasterService.
Since the majority of dependent services are challenging to produce and manage, injecting the real service rarely succeeds.
Replace it with a mock dependence, a dummy value, or a spy on the relevant service method.
These common testing methods work well for unit testing isolated services.
However, you nearly always use Angular dependency injection to inject services into application classes, and you should have tests that represent this usage pattern. Investigating how injected services behave is simple with Angular testing tools.
Testing services with the TestBed
Dependency injection (DI) in Angular is used by your application to build services. A dependent service is found or created by DI when a service has one. Additionally, DI detects or builds those dependencies if the dependent service has them.
When using the TestBed testing tool to offer and create services, you can let Angular DI handle service creation and constructor argument order. However, as a service tester, you must, at the very least, consider the first level of service dependencies.
Angular TestBed
The TestBed is the most significant Angular testing tool. To simulate an Angular @NgModule, the TestBed builds an Angular test module dynamically.
A metadata object with the majority of the properties of a @NgModule is passed to the TestBed.configureTestingModule() method.
An array of the services you'll test or simulate is set in the provider's metadata property when testing a service.
Then, call TestBed.inject() with the service class as the argument to inject it within a test.
Alternatively, if you prefer to inject the service as part of your setup, you can inject it inside the beforeEach().
Make the mock available in the provider's array when testing a service with a dependency.
The mock is a spy object in the example that follows.
That spy is consumed by the test in the same manner as before.
Testing without beforeEach()
The majority of test suites in this article use the TestBed to generate classes and inject services while calling beforeEach() to specify the prerequisites for each it() test.
Another testing method never uses beforeEach() and favors explicitly creating classes rather than using the TestBed.
Here is an example of how you might rewrite one of the MasterService tests in that manner.
Instead of using beforeEach, start by placing reusable, preparatory code in a setup method().
File: app/services/master.service.spec.ts (setup)
A test could reference variables like masterService in an object literal that the setup() function returns. Semi-global variables (such as let masterService: MasterService) are not defined in the describefunction's body ().
Each test starts by calling setup(), and then proceeds to assert expectations and change the test subject.
The test extracts the setup variables it needs, by using destructuring assignment.
Many developers believe that this strategy is clearer and more straightforward than the conventional beforeEach() method.
Feel free to use this alternate method in your projects even if this testing guide follows the conventional way and the default CLI schematics produce test files with beforeEach() and TestBed.
Testing HTTP services
For XHR calls, data services that use HTTP to communicate with distant servers commonly inject and delegate to the Angular HttpClient service.
With an injected HttpClient spy, you can test a data service just as you would any other service with a dependence.
File: app/services/hero.service.spec.ts
The methods of the HeroService yield Observables. To execute an observable and to assert if a method succeeds or fails, you must subscribe to it.
The success (next) and fail (error) callbacks are passed to the subscribe() method. To ensure that you catch failures, make sure to give both callbacks. An asynchronous uncaught observable error results from failing to perform this, which the test runner will probably blame on a separate test.
HttpClientTestingModule
Long-term exchanges between a data service and an HTTP client can be challenging for spies to mimic.
These testing cases may be easier to handle with the HttpClientTestingModule.
Import the HttpClientTestingModule, the mimicking controller HttpTestingController, and the necessary symbols needed for your tests before starting to test calls to HttpClient.
File: app/services/http-client.service.spec.ts
Then proceed with setting up the service-under-test after adding the HttpClientTestingModule to the TestBed.
File: app/services/http-client.service.spec.ts (setup)
Requests made during your tests now go to the testing backend rather than the main backend.
In order for the HttpClient service and the mocking controller to be used during the tests, this setup is additionally called TestBed.inject().
Now you can create a test that generates a mock answer and anticipates the occurrence of a GET Request.
File: app/services/http-client.service.spec.ts (setup) (HttpClient.get)
The final step, which checks to see if any requests are still pending, is sufficiently frequent to be moved into an afterEach() step:
Custom request expectations
You can implement your own matching function if matching by URL is insufficient. You may, for instance, search for an outgoing request with an authorization header:
The test fails if 0 or 2+ requests fulfill this criterion, similar to the preceding expectOne().
Handling more than one request
Use the match() API as opposed to the expectOne method if you need to reply to duplicate requests in your test(). While returning an array of requests that match, it accepts the same inputs. You are in charge of flushing and verifying these requests because once they are returned, they are no longer eligible for matching in the future.
Conclusion
- Angular application has numerous services that each need to be tested separately.
- Three different sorts of tests are available for a service:
- Testing synchronous operations
- Testing a method that returns an observable
- Testing dependencies in services