Before reading any further, I recommend reading part 1 for some context.
When you're done and want to know how to test a Smart Component, maybe part 3 will help.
Why the Http Service?
So, last time we talked about the Angular Component and how to test it. Now, that was a "dumb" component but hey, we have to start somewhere. I promise I'll show you how to test a "smart" component in the nearby future.
So, back to our WHY question. I think next to components, services make for a big part of our Angular applications. Then, out of all services, I noticed that people have issues testing the ones that do HTTP calls. So, without further ado, here is my take on it.
Don't worry, this will still be done in a Test Driven Development way.
Where we left off
If you remember from last time, we relied on the existence of a FavoriteMoviesService. Since that was beyond the purpose at the time, it was just a shell for the actual functionality. This is how it ended up looking like:
Create a service spec file
So, first things first: we need a spec file for our service. This is where we will write our tests. So, let’s create it:
What do we have here? This is the boilerplate for testing any service in Angular. It contains the initialization and a test that checks that the service is created.
Then, we add a variable for our serviceUnderTest and we initialize it in the beforeEach so we don’t have to do it in every single test.
Also, we added a list of movies that we will use as mocked data for the tests. More on that, later.
That’s about it.
So, we run our test now, what does it do?
Great! It passes.
How to start?
Now that that is out of the way, what do we do? Well, we want to make sure that our service returns what we expect it to. How can we achieve that? We will write a test for it:
So, here we have a new test that subscribes to our method and checks that what it got back is the actual list of movies that we expect.
Let’s run it:
Well, would you look at that; it failed!
Now, following the red-green approach, let’s fix it. How do we fix it? Doing the minimum amount of work necessary. Remember, we’re lazy.
So, we just return the list we know it’s expected, wrapped in an Observable. You might think it’s a hack, which it kinda is, but it’s fine. Don’t worry about it. It’s a work in progress.
Now, when we run our tests:
Check!
What about the HTTP part?
Why are we here? Yes, testing an HTTP service. So, far, there is no HTTP. Let’s change that.
The first step is to make sure our environment has a variable with the URL that we expect to be called. This is just good practice.
This makes our service configurable so we can have different URLs for DEV, TEST and PROD.
Now, back to our test. We want to make sure that our service does an HTTP call to our endpoint and it returns whatever that call returns. This will require some changes in the test:
First, we import the HttpClientTestingModule. This will help us test/mock/intercept/analyze HTTP traffic. Then, we create a new variable for the http helper because we will need this in more tests and it’s cleaner than to do it in each and every test.
Now, to the actual test. First, we changed the name to reflect that we expect the data to come from the backend. After that, there are a couple of things happening:
- We use the new http variable to check that one and only one call was made to our endpoint. This will return the request that was made by our service, if any.
- We check the HTTP method of this request to make sure it’s a GET request
- We use this request object to flush data. This is our way of mocking the response that would otherwise come from the backend
- Last but not least, we call the verify() method to make sure that there are no pending HTTP calls. This is to make sure that we don’t do more requests than we expect to or that we don’t have any unhandled requests.
Now, let’s run it!
Of course, it fails. Red-green, remember? So, it says that it expected one request for our URL, but found none. Let’s fix that.
So, in order to make our test pass, we inject the HttpClient in our service and we use it to make a call to our favoriteUrl endpoint. We return the response of that. Let’s try to run our tests again.
Check!
What other things should we test?
Another thing I like to test is error handling. We want to make sure that our errors are handled properly and are not swallowed.
What is that I hear? Time for another test!
So, here we use the second parameter of the subscribe method, the error callback. That method is called in case of an error. We want to check that if the backend returns an error, that error is not swallowed by our service but is bubbled up, so the consumer can do the handling.
Then we just tell our http to return a 500 ERROR.
Now, this is highly subjective. You can also have a logger injected in your service, and here instead of checking the bubblesUpTheError variable, you would check if the logger was asked to log an error(remember spies from lesson 1?). Whatever works for you. For simplicity, I went with this approach. I just wanted to show HOW to test this scenario.
Now, let’s run the test:
Check!
Let’s get fancy. RETRY
Now that the basic functionality is tested, let’s make things interesting.
We have a new feature request. Since our backend is flaky, let’s make sure that our service tries to get the data 3 times before actually failing. Interesting right?
Now, let’s change our previous test to check for that functionality:
Here we created 3 more request-checks. Each check comes with its own error message so is easier for us to know what failed. So, we expect 3 HTTP GET calls to go to our endpoint and if ALL of them fail, we expect an error.
Let’s run our test:
You see? Our helper error message came in handy. It failed but we know why. It did not make a second request. Now, let’s make this test pass:
The way we make this pass is by using the retry operator. We tell it: if the request fails, retry 2 more times before actually passing the error.
And, running our test now:
Check!
One last test for the road
Now, for a sanity check, we might want to write a test that checks the situation when the backend fails twice but works the third time. In that case, we want to make sure that the data is returned and there is no error. How would that test look like?
This looks very similar to our previous test. The difference is with the last request that we intercept. We tell that one to return data instead of failing. Then, we check that data was actually returned. Easy peasy.
Let’s run it:
It’s ALIVE!!
Summary
What did we learn?
- Testing an HTTP Service is important
- Test that an actual HTTP call was made. Test the HTTP method and check the data that was returned
- Test that errors are handled properly
- Test whatever extra logic you have handling the response from the server
And remember: RED => GREEN => SUCCESS
Be kind to each other! 🧡
Over and out
Test Driven Development in an Angular World - Part 2