How to test a service that uses Signals + Observables with a declarative approach?
Heey Heey Folks! How are you doing? Recently I have been studying Signals and how to use them with Observables. There is a great video from Deborah Kurata addressing this topic, where you can watch it here.
I have a project called RxF1 which I developed quite some time ago to study some reactive patterns with RxJS. These patterns use a declarative approach instead of the imperative approach, which is the most common one. I explained these patterns in details in my other article, which you can check here.
Some time ago, I refactored my services on the RxF1 project to use Signals and I struggled a lit bit to test these guys. It was difficult to find any answers on StackOverFlow, so I decided to write this article as documentation and I hope it may help someone in the future. Let’s start?
Before we move on, just a side note: For the sake of simplicity, I will just add the skeleton of my files to make them smaller and easier to read. I will provide the link to the project’s repository at the end of the article :D
The first example that I want to talk about is from a class called DriversService
. This service is responsible for getting the data used on the drivers page where I show the list of drivers driving for each constructor per season.
Let’s have a look on the code of the DriversService
.
On line 1, I declared a property called seasonSelected
which is a Signal with an initial value of ‘2021'. On line 3, I have a method responsible for setting a new value to the seasonSelected
Signal and everytime that its value change, I want to trigger an HTTP request to update my list of drivers based on this new season information. This is accomplished on line 7, where I declared a private observable called driversList$
that will hold the value return from the HTTP call.
I’m also using the function toObservable() provided by the @angular/core/rxjs-interop package to convert my Signal to an observable. Then I’m using the pipe function to apply a switchMap operator which automatically subscribes to the outer seasonSelected
observable to get the value of the season and call the getDrivers() method responsible for the HTTP call.
Finally, on line 12 I expose a Signal property called driversList
using the function toSignal() that will convert the driversList$
observable to a Signal. The function toSignal() is also available through the @angular/core/rxjs-interop package.
Now, how can we test that?
The test itself is quite simple to write. You can find the code bellow:
First I’m going to mock a response object from the API. Then on line 8, I’m declaring the object that I expect to receive from my service.
On line 12, I’m creating a Signal referencing the Signal from my service. On line 13, I’m mocking the function get() from the HttpClient to return the mocked object of line 8.
On line 15 I’m calling the service method onSeasonSelected() that will start the chain of changes until the HTTP request.
On line 16, I’m using the flushEffects() function to manually trigger the signal to be read.
Finally, on line 18 I compare the content of the Signal with my mocked driversResponse
, and that’s it!
The most important part of this test is the function flushEffects() that will make the signal be read.
Now, I want to talk about a second example that is a little bit more complicated. For this example, I’m going to use a class called RacesService
. In this service, I’m combining two HTTP requests to show a modal with information about a specific race including the final result and the championship standings until that date, as you can see bellow.
Let’s see the code:
Again, I have a similar structure to the driversService
class. The only difference now is that I have one more information that I will need in order to make the request, which is the round. For that, I have created a subject called roundSelectedSubject
and expose it as an observable on line 4.
Now, to trigger the HTTP requests we need that the roundSelected() and selectedSeasonChanged() functions to be called. The roundSelected() function will start the HTTP request chain, using the withLatestFrom
operator that emits the value of the season signal. Then, on line 50 I’m exposing another observable calledfinalResults$
which combine the response from both HTTP requests using the RxJS operator called zip that emits values as an array after all observables emit.
So, how to test it?
As I mentioned before, this test is a bit more challenging because I have two parallel requests being made, both using the withLatestFrom
operator. Firsts, let’s have a look on the code:
Again, I started the test mocking my data. I mocked the responses from both HTTP requests. On line 14, I’m subscribing to the finalResults$
observable in order to check its data emitted. I’m also using a callback from jest called done
which will wait until it is called before finishing the test.
On line 19, I created a variable mockSignal
to reference the season signal from the service. On line 20, I call the selectedSeasonChanged() function with a new season value and right after I called the flushEffects() functions to make this signal change to be read.
This sequence is important, because the
withLatestFrom
operator needs that its source had emitted some data before theroundSelected$
emits.
Then, I called the roundSelected() function that will trigger the emission from the roundSelected$
observable which starts the HTTP requests.
Finally, I mocked the return from both HTTP requests with the mocks that I created in the beginning of the test and I’m also using a timeout of 10s since this code is asynchronous.
Here is the link to my repository on github. You can check the complete files there.
That’s it guys! I hope you’ll find this article useful.
Nice coding!