Test your RxJS code in an efficient and sustainable way || Angular Virtual Conference 2021
Nov 9, 2023
Conference Website: https://globaltechconferences.com/event/angular-virtual-conference-2021/
C# Corner - Community of Software and Data Developers: https://www.c-sharpcorner.com
C# Live - Dev Streaming Destination: https://csharp.live
#angular #webdevelopment #RxJS
Show More Show Less View Video Transcript
0:00
Thanks a lot for having me
0:03
And I'm super excited to do this talk. And I particularly like talking about testing
0:08
not because it's such a great topic or it's so much fun to do that
0:12
but it's such a big pain point. And therefore, with this talk, I want to provide you a very practical guide
0:18
how you can test observables in RxJS code. And I already teased it, like testing in general is already pretty hard
0:26
And adding RxJS to it doesn't make it a lot easier. it actually makes it even harder
0:31
as we are now dealing with complex asynchronous problems and stuff like that. So testing on XJS code might often feel very intimidating
0:40
So what I want to show you is a good way how you can write in an efficient manner maintainable tests
0:45
because we don't want tests that no one likes to touch afterwards. And we don't like tests
0:49
that we spend days and days and weeks just on writing those
0:54
And all of that, I want to show you without marble diagrams
0:59
Marble diagrams are a way for testing provided by RxJS. And I'm, in the very beginning, not a big fan of
1:06
and I don't think most business applications just don't need it. But we will talk about that later again
1:12
And without further ado, let me quickly introduce myself, even though there was already this super fancy video in the front
1:18
So my name is Sir Nicholas Waltman. I'm a consultant for Evora IT and member of the RxJS team since
1:24
for years, four years, something like that. Additionally, I'm also in Angular GDE and doing most of the time as my consultant activities, Angular work, and therefore testing RHS code
1:36
So the stuff that I show you today is my experience and the patterns that I like to apply
1:42
So let's take a look at some code, right? So in your code, you sometimes have observables, right
1:50
So if you have observables, you want to test them somehow. And you would start for looking at a marble diagram
1:58
like how could your observable look like? And you would notice like, okay
2:03
of true, for example, is emitting synchronously and then immediately completes afterwards
2:08
Doesn't sound too complicated. So let's take a look how a test for that could look like
2:13
Recalling a function, subscribing to the observable, and in the subscribe callback
2:17
we are expecting the emission value to be true. seems fair, seems pretty straightforward, right
2:24
And the test pass. I mean, what else can we want more from a test? Like three lines of code, five lines of code with the test itself
2:31
and a running test looks great. And what I like to do when I test
2:37
particularly when I test observables, I like to do the negative test as well to figure out
2:42
like, is my test really proving the thing that I wanted to test? Is the expectation really executed
2:47
And when we run this piece of code, we will notice the test still pass
2:54
That's weird. That's kind of weird. And it kind of feels like you've thought about everything
3:02
like it's synchronously executed. You haven't forgot anything. The only thing you might have forgot is that RxJS
3:10
handles unhandled exception by throwing it at a separate call stack, meaning asynchronously
3:17
And as a failed expectation throws an exception, we are in the situation where we are right now
3:24
We executed the test. We had our expectation. It failed. But in the next loop, it would be evaluated and mark the test as false
3:36
But at that point of time, the test is already complete. How can we handle that
3:42
Most testing libraries provide a so-called done callback. And I like to use that pattern just because it's very explicit when I expect my test to be done
3:51
So what I do is I add the done callback function in the test function, however you want to call it
3:58
And then after my expectation, I execute it manually. And with this piece of code, the testing framework then knows like, okay, I need to wait for this expectation to be executed
4:09
And we are good to go. And now we even see that the test is working and failing
4:18
as shown on the slide before, in both scenarios. So pretty good
4:25
So the first thing that I wanted to show you is when you testing RHS code use that done callback to be sure that your test is properly executed And on top of that this done callback also provides a done call
4:39
where you sometimes are able to explicitly mark a test as failed
4:43
It's sometimes easier or better for readability than waiting for the timeout
4:47
This is just one of the recommendations I give. And I like this done callback particularly as it's widely supported
4:54
Jest uses it. Jesmin uses it. Also Monkha supports that. So it's very universally that you can apply that
5:03
Okay, that was easy. I agree on that, but this was like the appetizer
5:07
So let's look at something a little bit more complex, right? Because this is just executing synchronously
5:14
There's not much fancy to it. And also on top of that, it's just one value that is emitted that we are testing here
5:21
observables have the power or one of the biggest powers of observables is actually that they emit
5:27
multiple values. So how would we test something like that interval that emits every second
5:33
a value, for example, asynchronously and multiple values? So how can we test that
5:40
So code looks like that. And I now use from just to make it a little bit easier
5:45
And this is now also executed synchronously. But what I like to apply here, and this is a pretty
5:51
straightforward pattern is just storing the values in an array and then do a manual expectation on
5:56
that. This just works with the, because they are asynchronous. So the other thing that you could do
6:06
is waiting for a complete signal. This would also work with the, with an asynchronous observable
6:13
like interval, for example. So you're just subscribing to the observable in the next
6:18
callback, storing all the values in an array, and then perform your expectation
6:21
in the complete callback when you know, okay, my observable is complete. Problem with that, though, is that you need to have a
6:29
complete signal. Technically, not every observable needs to complete. Like interval does. Interval by default does not complete
6:38
It just emits every second. You can apply completion operator like take
6:42
and stuff like that. But in general, it doesn't complete. So, and my actual recommendation at this point of time is just add a completion operator
6:53
Usually for your test, you have certain expectations. And one expectation can be like, okay, I expect n values to arrive
7:02
Sorry, I expect n values to arrive. This way your test is deterministic, at least
7:09
And you're good to go with that. It is not ideal as you're adding code and logic in your test
7:17
I'm aware of that from a testing best practice that there are some gotchas to that
7:21
but it at least makes your test more deterministic and therefore more reliable. And on top of that, you're adding RxJS code to your test
7:30
and RxJS has a test coverage of 98% or something like that
7:35
So this is some of the trade-offs that I'm willing to take in this regard. one thing that i want to give a shout out at this place is the library observer spy
7:43
it's working under the hood with this array pattern as well but it's a little bit more
7:48
feature provided whatever it provides more features uh than just that so you can also
7:55
listen for error and complete signals and stuff like that it's pretty cool check it out
8:00
okay i i'm well aware that those examples were not at all representing real world problems or even
8:11
code and i agree with that i totally agree with that so what i did is i copied some of my code
8:19
actually more or less after obfuscating a little bit of stuff but more or less this piece of code
8:25
is running in production, and it's for an authentication mechanism. So in production right now, it's opening a pop-up
8:32
performs authentication, and returns whether the authentication was successful or not. And it also covers cases like, okay, what happens if the users
8:41
subscribe multiple times to the observable, therefore, would open multiple authentication process at the same time
8:46
this is handled with the share replay. There's also this weird case where occasionally the authentication
8:52
service just says like, you look suspicious. I returned you a 403 forbidden and please retry that
9:03
So we handled that in a way that we said in the retry when okay if we get this weird 403 for whatever reason try it again If you get any other error then bypass it or pass through this error to the subscriber
9:18
So the only thing that you have to do to run this test properly is kind of initiate it
9:27
So usually when you're executing tests, you should follow AAA criterias, right
9:32
So like arrange, act, assert. And for observables, the order of those A's does not apply very well
9:42
Because this is act already. So we're saying like we're triggering our observable emitting events
9:51
And in the case here, we are saying like, okay, when we get a log in event
10:01
Then we map this to true. This is really just for demonstration purposes now
10:07
And this is the public method that is exposed for the user. So when the users try to log in, they call this method, subscribe to it, and are good to go
10:17
The cool thing that I wanted to show you now is that the test will look exactly like the others before
10:23
So let's have a look. We are instantiating our class because this is kind of stateful
10:28
We are having the login with pop-up method that I just showed you
10:32
We are subscribing to that, and then we are dispatching the event
10:37
So technically, what I mentioned before, we have arrange in the first line
10:42
We have assert in line with a subscribe, and then we have act
10:47
We need this because observables are lazy, which would happen is if we would dispatch the event without being subscribed to that, nothing would be executed
10:55
As we are not subscribed to it. So, what I want to show here is that I use the same pattern. I subscribe to an observable, call the done method, and I'm good to go. As easy as that. And the same even goes for error scenarios, right? So, my observable can emit an error and I can react to an error
11:16
What I did here is I subscribed to the error channel, saying, like, in case of an error
11:21
I expect the error to be a certain error, and then say, okay, I'm done
11:28
When talking about testing, we now have a good feeling for, like, how I can write tests
11:34
But when we're talking about testing, we also have to talk about mocking, because we might not directly test our observable
11:41
but we have a piece of functionality that is affected by an observable
11:45
So how can I mock observables? And I want to introduce you to the six step mocking strategy
11:54
This is not a patent or anything. This is a name that I came up. So feel free to reuse it
11:59
But when I start mocking something, doesn't matter if it's in an Angular application, React, whatever
12:05
I usually always say when I have an observable, I start with mocking it by returning never
12:10
So I have the observable that I use never instead. What never does, it's returning an observable
12:18
So therefore, like all the methods are available. I don't have to deal with problems where like subscribe is not defined, stuff like that
12:25
So I have all the methods available, but it's not emitting anything nor completing
12:29
So it won't execute any code. This is my first step just to make like tests run that are not at all affected by an observable
12:38
just to make the code executable. The next step would be using off with a single object
12:46
I use this pattern when I really want to mark one single value in a synchronous manner
12:52
So, for example, where I like to use that is if I'm marking HTTP calls
12:56
In a unit test, you wouldn't execute a real HTTP call. With this pattern, you can say, like, instead of executing this HTTP call, return off with a static value, which is a synchronous submission
13:07
If you want that for your test. next step if you don't want it to be synchronous would be use off with delay if you want an
13:16
asynchronous test for a static value use off with delay and technically i now put here a delay of
13:23
100 second you can also use delay of zero it will also be asynchronous so this was just for
13:28
demonstration purposes next thing or next stage that i would apply is using from when i want to
13:36
emit multiple synchronous values We have seen that already in the slides before the way I use that So when I use from with an array it will have three emissions First one then two then three
13:48
Therefore, I can test scenarios where I depend on multiple value emissions. For example, I want to
13:54
mark out a polling scenario, an observable that polls values and therefore emits values
14:00
like in an interval. I can do this with from. If I want multiple emissions with asynchronous value
14:09
then I usually go for interval. You could also technically go with from and delay
14:14
would also work. I personally usually use interval for that purpose. And now that was step five
14:22
And the last thing, the very last thing, if I need full flexibility of the way
14:26
I want my observable to work is I use the subject. With the subject, I can mock everything. Here's a quick summary of that. As I'm running short on time, I will move on. So we now have covered testing and mocking. So let me add my two cents about marble testing
14:43
and in all honesty marble testing is a great way because they're providing a visual way
14:50
that represent the behavior of an observable and usually when we're talking about marble diagrams we're talking about ascii marble
14:59
diagrams not like that kind of ascii art but like that kind of ascii art so they come with
15:05
their own syntax that you have to learn and they're describing the behavior of the observable
15:10
So of true, for example, would look like this in a marble diagram
15:16
So we have the parentheses to mark a synchronous emission of two of having two emissions at the same time
15:23
First, the value true represented by the character A and then the pipe for the complete signal
15:30
Now it's already getting a lot more complicated with interval. Interval would look like that. So we first because we're starting at zero milliseconds in a marble diagram
15:40
in an ASCII marble diagram, we first wait 1,000 virtual milliseconds before the first emission
15:46
So we have 0 to 999. Then we have the first emission, and then we wait another 999 milliseconds
15:54
So things like that are kind of confusing when you're getting introduced to marble diagrams
16:00
And the code is not that easy, from my perspective at least
16:05
The good thing about marble diagrams is, and, okay, I will skip this for now
16:13
If you have questions about the code, feel free to approach me on Twitter or whatever
16:19
But I want to share my opinion on marble testing because what I often hear is this opinion, like, okay, if I have to test RxJS, I have to use marble testing
16:27
And this is just not true. Marble testing are extremely powerful for libraries like RxJS
16:33
because you can test extremely difficult things, like very complex scenarios. And another thing, it also helps in understanding
16:42
how observables work. That's a great benefit. I have to give that
16:46
And it's visualizing the way your observable works. But you have to learn an entirely new syntax
16:51
that is not really intuitive. You have to know a lot about RxJS
16:57
and how things work under the hood, which is usually a level of abstraction that you don't want to deal with for test
17:03
And honestly, it's super hard to maintain. I personally had so far in my career
17:10
three scenarios by now where I said, for this piece of code, marble testing makes sense
17:17
And I actually forgot that you're testing implementation details of RxJS because you can also test like stuff
17:22
like when doesn't inner observable subscribe and stuff like that. this is not relevant for a business application
17:31
And long story short, if you're not writing a library or the most, or like very complex RxJS code
17:42
you probably don't need model testing. I will skip the summary out of time reasons
17:49
but it's this nice fancy diagram. So I will keep it for a second and then move on
17:54
Thank you very much. I really appreciate being here. And if you have any questions, feel free to approach me at any time
18:01
I rushed the last couple of slides a little bit. So totally open to that
18:05
Thank you very much


