TDD in Angular - Further HTTP Testing
Tags: angular, typescript, javascript, tdd
Published: 2020-10-06 03:04:34

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](https://github.com/Qarun-Qadir-Bissoondial/ng-jasmine-testing-demo/tree/testing-http)_

## Understanding the HttpErrorResponse

Before we test situations where our requests can fail, we need to understand the [HttpErrorResponse](https://angular.io/api/common/http/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. 

```typescript

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](https://dev.to/qarunqb/tdd-in-angular-basics-of-http-testing-5a52). 
Let's test our `getAllTodos` function but if the server fails, we return an empty array.

Remember that our service looks like this:
{% gist https://gist.github.com/Qarun-Qadir-Bissoondial/7634fcb13ad6441131b0485d8f8eb27e %}

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.

{% gist https://gist.github.com/Qarun-Qadir-Bissoondial/ab303fa5fbca7abe65a089bf97099065 %}


When we run our test, it fails since we haven't modified our code to take care of this situation.


![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ye6eklfoxzi5j3x7b55m.png)

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: 

```typescript
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:

{% gist https://gist.github.com/Qarun-Qadir-Bissoondial/d7e89c314e65a1b4dc939569989c7079 %} 

and the corresponding code to make the test pass will be: 

```typescript

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: 

{% gist https://gist.github.com/Qarun-Qadir-Bissoondial/9b087f5276d5a44c869a30e7b6e541af %}

And the corresponding code:

```typescript
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 :smile: