See how to test a service in Angular and make sure it interacts with APIs. Check the API call and return the data.
When we build applications with Angular, the components have the responsibility to display data and allow user interaction. In some cases, the data comes from services as static data, or in the real world, it comes from an API.
In the previous article, we learned how to test our components, mocking with fake data. However, the app is more than just the components—what about the service?
Today we’re going to learn how to test our services so they interact with APIs. We’ll learn how to make sure they call the API and return the data.
Let’s dive in!
First, clone the project by running the following command in the terminal:
git clone https://github.com/danywalls/testing-kendo-store.git
Cloning into 'testing-kendo-store'...
remote: Enumerating objects: 149, done.
remote: Counting objects: 100% (149/149), done.
remote: Compressing objects: 100% (90/90), done.
remote: Total 149 (delta 86), reused 115 (delta 54), pack-reused 0
Receiving objects: 93% (139/149
Receiving objects: 95% (142/149)
Receiving objects: 100% (149/149), 158.84 KiB | 2.24 MiB/s, done.
Resolving deltas: 100% (86/86), done.
Next, install all dependencies for the project by running npm install
in the testing-kendo-store
directory.
cd testing-kendostore
npm i
added 1120 packages, and audited 1121 packages in 6s
133 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
Run the command npm run test
to confirm everything works.
Output location: C:\Users\DPAREDES\Desktop\articles\testing-kendo-store\dist\test-out
Application bundle generation complete. [1.294 seconds]
dist\test-out\browser\app.component.spec.js:
π§ Browser logs:
Tests passed!
Chrome: |ββββββββββββββββββββββββββββββ| 1/1 test files | 1 passed, 0 failed
Finished running tests in 0.8s, all tests passed! π
Services are easy to test. They are classes with or without dependencies. Similar to the components, we can rely on TestBed
to handle the service and its dependencies.
Testbed allows us to provide the dependencies. Angular also provides other utilities to simplify, like HttpTestingController
and provideHttpClientTesting
, to make testing and mocking requests easier.
Read more about HttpTestingController.
Before we start, let’s analyze our code. Open the product.service.ts
. We have a property to make the request to the API and expose the products$
observable.
@Injectable({
providedIn: 'root'
})
export class ProductsService {
private API = 'https://fakestoreapi.com/products'
private http = inject(HttpClient)
public products$ = this.http.get<Product[]>(this.API);
}
The product.service.ts
needs the httpClient
. Also, it has a private property API
pointing to the API, so we need to make sure it calls the API with a GET request.
We must configure the TestBed
with the following points:
HttpClient
.HttpTestingController
to mock the response.MOCK_PRODUCTS
example data.Let’s get started!
Remember, we explained Jasmine basics in the previous article.
Open the products.service.spec.ts
, it contains the following code that does not looks like a real test:
describe('ProductsService', () => {
it('should be create in next article π€£', () => {
expect(true).toBeTruthy();
});
});
It’s time to write the test for our ProductService
, in the products.service.spec.ts
file so we’re going to do the same approach, declare two variables for the productService
and httpTestingController
.
import {ProductsService} from "./products.service";
import {HttpTestingController} from "@angular/common/http/testing";
describe('ProductsService', () => {
let productService: ProductsService;
let httpTestingController: HttpTestingController;
});
Next, using the beforeEach
lifecycle hook, and using Testbed
, we provide the provideHttpClient
, provideHttpClientTesting
and ProductService
.
Because the Testbed
takes care of the DI, we use TestBed.inject()
to set the httpTestingController
and ProductService
.
import {ProductsService} from "./products.service";
import {HttpTestingController, provideHttpClientTesting} from "@angular/common/http/testing";
import {TestBed} from "@angular/core/testing";
import {provideHttpClient} from "@angular/common/http";
describe('ProductsService ', () => {
let productService: ProductsService;
let httpTestingController: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule(
{
providers: [
provideHttpClient(),
provideHttpClientTesting(),
ProductsService,
],
}
)
productService = TestBed.inject(ProductsService);
httpTestingController = TestBed.inject(HttpTestingController);
});
});
Next, write a test to validate we have an instance of ProductService
.
it('should create an instance', () => {
expect(productService).toBeTruthy();
})
Run the test again and it’s perfect! π
Application bundle generation complete. [1.258 seconds]
dist\test-out\browser\products.service.spec.js:
π§ Browser logs:
Tests passed!
dist\test-out\browser\app.component.spec.js:
π§ Browser logs:
Tests passed!
Chrome: |ββββββββββββββββββββββββββββββ| 2/2 test files | 2 passed, 0 failed
Finished running tests in 0.9s, all tests passed! π
OK, we have our basic test to create an instance of ProductService
, but it’s not really adding much value. I want to check that $products
returns a list of observables from the Http without requesting the real API.
Let’s write a real test where we verify the request data using a GET
request and return a mock example.
Let’s test our service. First, add a new test like should make an HTTP request
. Our test uses the products$
observable property and subscribes to the observable.
Inside, we use the Jasmine matcher expect
and toEqual
to confirm the response to be equal to MOCK_PRODUCTS
.
it('should make an HTTP request', fakeAsync (() => {
productService.products$.subscribe((response) => {
expect(response).toEqual(MOCK_PRODUCTS)
})
}
Because the httpTestingController
is listening to the request, we expect it to call the https://fakestoreapi.com/products
URL. So, we call the expectOne
method and store the request in the variable req
, then call the httpTestingController.verify()
method to confirm the request was to the expected URL.
const req = httpTestingController.expectOne('https://fakestoreapi.com/products');
httpTestingController.verify();
Finally, we confirm the request is a GET
and flush the mock example data.
expect(req.request.method).toBe('GET');
req.flush(MOCK_PRODUCTS)
flush()
The final code looks like:
import { fakeAsync, flush, TestBed } from '@angular/core/testing';
import { MOCK_PRODUCTS } from '../tests/mock';
...
it('should make an HTTP request', fakeAsync(() => {
productService.products$.subscribe((response) => {
expect(response).toEqual(MOCK_PRODUCTS);
});
const req = httpTestingController.expectOne(
'https://fakestoreapi.com/products',
);
httpTestingController.verify();
expect(req.request.method).toBe('GET');
req.flush(MOCK_PRODUCTS);
flush();
}));
Save changes and run your tests again and tada!! π
Output location: C:\Users\DPAREDES\Desktop\articles\testing-kendo-store\dist\test-out
Application bundle generation complete. [1.220 seconds]
dist\test-out\browser\products.service.spec.js:
π§ Browser logs:
Tests passed!
dist\test-out\browser\app.component.spec.js:
π§ Browser logs:
Tests passed!
Chrome: |ββββββββββββββββββββββββββββββ| 2/2 test files | 4 passed, 0 failed
Finished running tests in 0.9s, all tests passed! π
Yes, we have our application with tests for both our components and services!! I can rest easy or make new changes in the app.
Yes, that’s a good question. What if I want to test an interaction? For example, when the user clicks on the article, the purchase button appears?
Well, that’s the moment when we need to delve deeper into learning about Jasmine and using CSS selectors to query elements, or we can utilize libraries like Angular Testing Library. Alternatively, an easy yet powerful alternative is to use Progress Telerik Test Studio.
Telerik Test Studio makes it easy to check that applications running in a web browser behave as expected across different browsers and simplifies observing consistent web UI on multiple browsers or browser versions, for clean user interactions, form validation, API calls and more.
Visit the Progress Telerik DevCraft page to learn about bundle options that include Test Studio and Kendo UI for Angular.
We’ve learned how easy it is to test our service and how TestBed helps us to configure the dependencies, we register the dependencies on TestBed and it handles DI to provide instances to us.
We played with the HttpTestingController
, which helps us to mock the response and avoid making calls to the real server. We can effectively test service logic without relying on external dependencies.
I hope this article helps you get started with testing in Angular. If you want more articles like this, leave a comment ππΌ!
Dany Paredes is a Google Developer Expert on Angular and Progress Champion. He loves sharing content and writing articles about Angular, TypeScript and testing on his blog and on Twitter (@danywalls).