In our last article, we would have tested basic HTTP requests and dictated how the response would look. However, many unexpected things can happen when sending a request. We need to handle these situations so that our application doesn't crash and the User Experience remains flawless.
Code for this article can be found here
Understanding the HttpErrorResponse
Before we test situations where our requests can fail, we need to understand the HttpErrorResponse. This is a class that Angular wraps around any network errors thrown by the browser before it reaches into our application.
Failing HTTP requests are caught by the error
callback of the subscribe
function and the error parameter is of type HttpErrorResponse
. This is useful for UI where we can grab the error message and display it to the user. It is also useful for testing where we expected our request to fail and have a certain status code.
this.http.get('/some/failing/request').subscribe(
(data) => { console.log(data); },
(error: HttpErrorResponse) => { /*Access error object here*/ }
);
All responses carrying 400 and 500 status codes are immediately treated as an error and will cause the subscription to fail.
Handling Failed requests
There are a number of ways to handle failed requests and the choice may depend on the application domain and business rules. Generally speaking, we can:
- Tell the user something went wrong
- Retry the request in the background x number of times
- Redirect to another page
- Return a default value
It's always a good idea to let the user know what happened so they won't be left waiting and confused. This can take the form of a simple pop up message on the screen. It is easy to replace this code in the future if error handling methods change.
Generally, when subscribing to observables fail, code inside the next
and the complete
callbacks never run. Therefore, whenever we expect our requests to fail, we need to run our assertions inside the error
callback. This is useful for testing if certain error messages are being displayed for different types of errors.
Ideally, we want to simulate a failing request and test that our application recovers. In other words, even though the request may have failed, our application code won't throw an error and freeze up. Let's get started.
Writing our tests
We'll use the same To-Do list service from our previous article.
Let's test our getAllTodos
function but if the server fails, we return an empty array.
Remember that our service looks like this:
I made a separate test suite for this function since I wanted to test more than what I described above.
Since our function is supposed to recover from the error and continue normally, our assertions are in the next
function of the subscribe
. We would expect that the response data is defined, it is an array and it has a length of 0.
We can simulate different statuses, status texts, headers and more by passing in a second parameter into testRequest.flush
. In this case, a status of 500 was simulated which means an internal error has occurred in the server.
When we run our test, it fails since we haven't modified our code to take care of this situation.
Notice how the error
callback is triggered and fail
function that Jasmine provides is executed. However, our tests will pass if we modify our code to the following:
getAllTodos() {
return this.http.get(this.url).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 500) {
return of([]);
}
})
)
}
The above code says to execute the HTTP request but if an error occurs and the response status is 500, then return an Observable containing an empty array. We return an Observable as opposed to the raw value because this is what catchError
expects.
Testing unauthorized requests
Usually, when dealing with authorization, we include an access token in our request headers so that the server knows who we are. Absence of this token means that the server should reject the request and return a 401 response.
Let's say that we needed to be authorized to update a to-do item.
We can test that a certain error message is displayed if the request is unauthorized.
Our test would look something like this:
and the corresponding code to make the test pass will be:
updateTodo(updatedItem: Todo) {
return this.http.put(`${this.url}/${updatedItem.id}`, updatedItem).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
this.displayError(error.statusText);
return of(null);
}
})
)
}
Testing retries
There are times where the user, through no fault of their own, may have an unstable connection. Though we may display an error message on the screen when a request fails, we should first retry the request in hopes that the response comes through.
Let's say we want to retry getting a single to-do item 3 more times after it fails the first time. If it fails, after 3 retries, then it should throw an error.
Our test:
And the corresponding code:
getSingleTodo(id: number) {
return this.http.get(`${this.url}/${id}`).pipe(
retry(3),
catchError(error => {
return throwError(`Failed to fetch item with id ${id}`)
})
)
}
In our test, we would have simulated a 404 error but our function actually catches all errors and then retries the request. Additionally, notice that the for loop in our test runs 4 times. This is for the original request and then the following 3 retries.
We also expected this function to throw an error. Therefore, our assertion was in the error
callback of the Observable.
Conclusion
In this article, we gained a deeper understanding of the HttpErrorResponse and how it appears in Observables. We also tested Http Requests further by manipulating the response data and status code.
This forms merely the foundation of testing more complex Http Requests that chain main RxJs operators together. Hope you begin writing your requests with more confidence and for a better User Experience. Thanks for reading 😄
Top comments (0)