Don’t Lose Your Marbles, We Can Test RxJS Code - RxJS Live! London - On Air - by Jay Phelps

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
[Music] [Music] [Music] [Music] [Music] so i'm going to be talking about testing your rxjs code i'm mj phelps if you want to reach out to me on twitter you can at underscore j phelps j-a-y-p-h-e-l-p-s i'm the co-founder of a company called out smartly and we deal with we're coming out of start uh stealth mode and we're we deal with performance so i'm just going to jump right in for time's sake so there are numerous ways to test rxjs code and i'm not going to cover all of them but i want to start by saying that testing async code is really really hard even with the things i'm going to teach you today it's still really hard and there's a lot of caveats depending on what environment you're testing it in like whether you're in node you're in browser you're trying to test it in some weird javascript environment there's a lot of caveats so unfortunately i can't cover everything but let's start simple let's take some very simple javascript i'm going to assume that you know how rxjs works and all that stuff so if you have trouble with some of this you might have to come back but here's some very simple javascript or arcgis where we've got an observable of zero and then one so it'll emit zero and then synchronously emit one as well so you can see that it console logs zero and one now if we were trying to test this like we wanted to like make an assertion for the value we you know the callback gets invoked for each individual next value so we need some way of accumulating all of those values so one thing you might decide to do is you might reach for the to array operator which will basically accumulate all values and then once the observable completes will emit them as a single array so you can see there that it waited to the completion and then emitted 0 1 as an array now you might do something like this so if you were using jest or something you might call expect value to equal 0 1 right and this this works this absolutely is a legitimate way of testing rxjs code today however there's lots of foot guns with this so what happens if i swap out that input with an observable of interval uh and so every one millisecond it's going to emit something and then we're going to accumulate it to an array now remember how i said that two array waits for it to complete well it never completes so that it's never going to be called like so your your your test will just lock up right so you could do something like this you could say take two like i'm just going to test two of them and and you know that that works that's that is legitimate but there are caveats again with that your test is no longer actually testing that the code never completes is one of the problems and the other problem is that what if you know we don't actually test that like the the concept of time in this test you know what i'm saying like the fact that it's one millisecond between each value we are not representing that inside the subscribe and the expectation right so it does not actually represent that each value is emitted after one second and let's see so and the the second problem is that if you are using a test runner you have to somehow signal to the test runner that you're done because this is asynchronous right so i'm not going to go into it deep because it depends on the framework but generally speaking there's some sort of like callback like see how we said done there um but the problem with actually using real timers there's several of them but the first big problem is what happens if your timer is something like you know 10 seconds and by the way just a little side note if you're using modern javascript you can now use the underscore delimiter just for to make the numbers prettier so you can see that it is actually 10 000 milliseconds or 10 seconds so if you if you were to run this you would actually have to take 10 seconds for each individual one to emit and let's say you got tons of tests that are running this over and over and over this can make your tests run for minutes or even hours depending on how many tests you have so it requires real amount of time to actually pass which is troublesome the other problem is if you make anything not non-trivial like this is a very trivial example but if you make anything non-trivial with real timers and you've got a bunch of them running and stuff like that you create a lot of non-determinism which that effectively means flaky tests that things can go in different timings different orders there's just all sorts of weird things happening and that's never a good thing right so you want your tests to always either pass or fail not sometimes pass sometimes fail right so what are our options what we really want to do is we want to test our asynchronous code synchronously and deterministically so right now it's asynchronous but we want to actually test it synchronously even though it is async and we want to be deterministic every single times the same thing and the only real way to do this is to virtualize time to create the you take the concept of time and you create a fake clock essentially and you're able to control time you could go forward backward etc um so one of the almost obvious ways is to fake out timers right you you override set timeout or in set interval and all of those sorts of things and then you can advance time trigger it as as you please lots of frameworks can do this for you automatically there's npm libraries that's a whole talk on its own there's no point in getting into that but like here's just a general example you could say just use fake timers if you're using that right and then i could advance time by one millisecond and then advance time by another millisecond and then you get the two values but we're still if you're if you're if you're a clever clever cookie you might notice we're still not actually asserting that time has progressed right we're advancing time but we're not actually saying that time act but that that was the real reason why we got zero and then one we're not actually asserting that sequence is the timing sequence so you could change this code you could write it into a buffer right you know you could push each value and then you assert it on each step that this is the one that came here and then now this is the one that came here and that works um but you know this is imperative which isn't the end of the world sometimes impaired is fine and sometimes that's what you should do just it makes things simpler but complex sequences can be very difficult to coordinate and reason if you're using this technique so is there another way and just be clear you sometimes will want to use that just because there's some times that it's just like that is the easier solution there's no one size fits all but can we declaratively represent values over time and the rx community not just the rxjs community but the rx community at large has kind of popularized this concept of marble diagrams and if you haven't ever heard of them i'm going to kind of give you a quick like primer to it so here's some very basic javascript for every time i click a button we're going to debounce it for 500 milliseconds right so if we were to break this down we would create marble diagrams like this we'd say here's an example of an input so the top is showing us the input from the clicks so let's say you know you have the green click and then the yellow and then the blue etc etc etc and then the bottom marble diagram is showing the output of the bounce time operator right it debounces it every 500 milliseconds so let's actually run through that let's throw all that away get rid of that and then actually run so we're going to start at the left time starts at the left and then we start clicking so i'll click once right then we wait 500 milliseconds and that marble is going to get emitted by debounced time because i waited enough time for it to emit it now let's say that i go and i click again and i click again and i'm just like rapidly clicking clicking clicking clicking clicking clicking keep going now 500 milliseconds have actually passed it's going to emit that that last one because that's how b bounce time actually works then again we keep going i do another click 500 milliseconds passes and then we emit again and so this is a moderate a marble diagram to demonstrate like once you learn how these marble diagrams work and some of them are very much more complicated than this but once you learn these it's very it's very simple to look at these operators and see oh okay so that's what it does right so marble diagrams are great but how do we represent marble diagrams in our code right like we can't put graphics in our code can we well we kind of can uh ascii art so to the rescue um so we take we need some way to represent the marbles in in ascii so here's an example marble right green well we could represent it instead of with colors we could represent it with letters single characters so a green marble might be a right so and then you could say that a by itself means that a emits synchronously at zero milliseconds it's the very start of the stream it just synchronously emits a whatever a is now what if we wanted to emit a and then one millisecond later emit b so asynchronous so emit asynchronous then asynchronously emit b right one one after the other so you you could put them right next to each other and that could mean that this one emits at zero milliseconds this one emits one millisecond later right so that's pretty straightforward a emits then b emits synchronously after one millisecond of virtual time okay now what if so that's one's millisecond later what if i wanted a and b to both emit at the same time on the same frame so at zero milliseconds at the very beginning it synchronously emits a then synchronously emits b well we could come up with some syntax for that we could put them in parentheses and that now means that they both emit at the exact same frame so they say a emits then b emits both synchronously right a emits then b emits now let's see so what if we wanted to say oh so so this was zero milliseconds and then one milliseconds right what if we wanted b to come at two milliseconds not one millisecond well we want to put something in between here to signal that a millisecond has passed an additional millisecond and because of a marble diagram has this lines through it we could just use a hyphen right but just hyphen that that through and then that now represents this is zero milliseconds this is one millisecond this is two milliseconds of time right so a one millisecond passes then another millisecond passes and then b and we could do that way we could do three so we could do one we could say two and then three and we could just keep adding on more and more and more right but what happens if we want to represent say 10 milliseconds it would look something like this which you know isn't that bad right so you got 10 milliseconds is all the way to there but things get really hairy because very often in in real life you very rarely have very short timers they're usually like 100 milliseconds or a thousand milliseconds or 10 000 milliseconds so you get things like this um and this was a problem with the older versions of rxjs um like version 5 and stuff but we came up with a a i used to be i forgot to mention i used to be on the core team i'm not anymore but we we came up with a different way of handling this so this would be 100 milliseconds but this is just you know not untenable so we need to come up with some way of representing a specific amount of time has passed in a very terse way and the most obvious way is taking inspiration from css right so we could just say time has passed so in this case we're saying 99 milliseconds has passed in between there so this actually emits at 100 milliseconds now and that's a very terse way of saying that now you might be looking at that and going hmm why is it 99 milliseconds why isn't it 100 milliseconds and this trips people up it trips me up too i forget um but it's kind of a quirk yet a feature it really depends on your perspective and preferences we debated this a lot but if i were to use 100 milliseconds this would actually end up being 101 milliseconds which is not what we want and the reason why is because a starts at zero but every single letter every marble you have when it's standalone like that it represents a frame one millisecond so even though the a gets emitted at zero time then passes one millisecond no matter what after a unless you group it together with the parentheses so a emits a zero but then advances the actual timer by one millisecond and that really kind of trips people up that um what you would that each marble takes up a frame itself intrinsically um and that's you know just you just got to kind of get used to it and then it kind of becomes a little bit more intuitive so 99 milliseconds is what you actually want to get 100. next up what about so that went on for 10 milliseconds but we didn't say anything about the marbles actually completing what if this stream actually completes and doesn't go on forever what about complete so we need a symbol for that we need some way of representing the end of it and when we had the graphical ascii excuse me the graphical marble diagrams we had a line at the end that represented the end of a stream and so why don't we just use that we can just use the pipe uh letter and that can now represent that the observable has completed so we're saying that that a will emit and then 100 milliseconds later b will emit and then at 101 milliseconds you'll actually complete because remember each marble takes up one millisecond frame so b actually does take up a frame right so but let's say that you want to be that actually doesn't happen very often where something emits of a emits a value waits a whole millisecond and then completes that's pretty abnormal actually so more common is you want to say emit a value and then complete synchronously and to do that remember how we do synchronous to remember how we do multiple things synchronous we use the bracket we use the parentheses so we wrap those in parentheses and now you're saying that b is going to emit and then we're going to complete synchronously on the same timeline right so that's the the very basics of marble diagrams we'll touch on a couple more but all we have to do now is put this in a javascript string and now you have just learned the basics of ascii marble diagrams for rxjs testing so let's actually write some code so we go back to that using fake timers just example we break that back down to its its basic parts how would you represent this stream as a marble diagram well it looks something like this so here we say zero this is so we're skipping a frame so we wait one millisecond because that's how that's how interval works it doesn't emit synchronously immediately it waits the time then emits the first value so it waits one it's right now away gonna wait one millisecond then it will emit a then it'll they'll wait um another millisecond and emit b and then synchronously complete now synchronously complete like we only added the take two to get around the fact that we couldn't represent time going on forever or whatever like we added that operator for that purpose so do we really actually need that and the answer is yes and no you kind of like so what we really want to do is get rid of that take taken to take uh that take value this is how we want to represent this as truthful because that is exactly what this interval is doing zero milliseconds one millisecond then two milliseconds input never ends by design because that's just what interval does oh you could add like another c or another another marble or a ton of marbles because it just goes on forever because there is no take and that was how we how we wanted to use the code but that is problematic because even though we're virtually virtualizing time we can't advance time to infinity right you know like we can't actually get an infinite number of values so we have to stop it at some point like even though we want to represent this as saying it's infinite we don't want to actually be infinite because if we tested it infinitely it would just never end so we need to write a diagram to represent the subscription that we're going to use for testing purposes so here's a diagram first for subscription we're saying that three frames will pass so three milliseconds and then infinity right and but we actually do want it to to unsubscribe so we can put the exclamation point and if you if all this syntax is kind of confusing don't worry about it you can go to the documentation and and use this as a reference until you to learn it all so another kind of cool thing about the newer versions of rxjs with the ascii diagrams is that one thing that's really helpful about them is that you can add arbitrary white space and it means nothing now so that means i could actually do this so that now the ascii diagrams i can have like i could have a bunch of ascii diagrams and they'll all line up aesthetically so that you can visually see like oh okay that's when this happens that when that's when that happens and it becomes nice and easy and intuitive to reason about it so that's marble diagrams how do i actually use this because this you know javascript has no idea what this is right you need to you need to use something called a test scheduler and that's provided by rxjs it provides that first class time virtualization so you can import that at rxjs testing you need to instantiate it and provide a callback the callback is going to accept two arguments the actual value and the expected value and then you need to integrate this with your testing library of choice like like here's an example using chai but you could just as easily use jest or some custom thing but you're saying you're expecting the actual to equal the expected and so under the hood it will call this for you when you do your test assertions now you need to keep a reference to that test scheduler and there's this run method and inside inside the callback that you provide time is automatically virtualized so you don't need to if you use marbles in the past with the new run method you no longer have to pass in your own test scheduler to all your different operators or thread that through it just automatically uh threads that through for you and automatically virtualizes time if you have no idea what i'm talking about just ignore that statement but the gist is is that it just magically works you don't have to do anything special inside that callback it also gets a argument called helpers and it has a bunch of things on it let's first start with expect observable which is a helper to be able to do assertions on your streams add that code we were working on earlier back in here and then we can just write a little code to make that assertion to actually use that marble diagram we're saying expect this the output of this observable to be this expected expectation marble diagram right pretty straightforward now one thing we haven't touched on is that we were using a and b and c as marble placeholders but our real code is not going to emit a b c d e f g right so we need some way of still using the letters to testly represent the marbles but we want to actually have real values for them and to do that you could pass a second argument to the 2b function that you that you're training on expected observable and and you just provide an object as a dictionary to look up the different marble terms so you you have a value for a and a value for b etc etc pretty straightforward if you were i would say it's like back in this example we have our our input code hard coded in our test which makes no sense no one ever is going to do that right you have an application your application code is going to be where it is then your tests are going to be somewhere else so it might look something like this this is a trivial example but still you have some example code and then you're going to somehow you know import it and use it later so let's say that we imported it i'm not going to show all the code but let's just say that we imported it into our test we could write a marble diagram for that and then we want to write our subscription logic so that we make sure to unsubscribe because it will go on forever because it's an interval um so 5 milliseconds then a then 99 milliseconds then b and then we unsubscribe because it's uh doing throttling so if we throw that in an actual test let's say using the it helper in mocha or jest or what have you so it does cool stuff we call test runner our test scheduler run we provide our boilerplate that we had there we assert that the observable is our marble diagram we expect it to be there and then we provide those two values for that that we would get back now notice that it's zero and then 20. and that's because of throttle right instead of zero and then two or zero and then one we were throttling for a bunch of time and 99 milliseconds passed and then we we actually uh emitted b and so that's um that's exactly why if that's confusing just kind of play around best thing you can do is just kind of play see what happens and you'll go oh okay that makes sense so um the other thing that i want to tell you about is is the cold helpers there's cold and hot uh it's been cold what what did was cold mean what does hot mean has been touched on a little bit before i'm not going to go too deeply in it because it is a talk on its own uh as as mentioned before ben lesch has a a wonderful medium article on on this just search hot versus cold observable ben lesh and you will find it it's wonderful explains it but the gist is is that there's a little helper for you to create your own custom observables very very trivially so you could say cold five milliseconds emit a 99 milliseconds emit b right pass in some custom values to it and now the tests will run just as it was now there's also a hot version so if you swapped out cold with hut you'd have to change the expectations because basically if you decide to subscribe that's what the bottom sub thing is and i'm not going to get too deep into this but there's this little the carrot that's basically saying that's the point i want to actually subscribe so the hot observable is pumping out values even before we subscribe to it because it's hot and once we've subscribed we've missed a bunch of values right so we need to update also our our actual asserted value so we only actually missed zero we missed all the other stuff because it got throttled then we only picked up on 20. and that's the gist of of hot versus cold there's also assertions so our search engines for heirs i mean so like let's say we do a then the 99 and then b but instead of doing b actually what happens is is it's coming back from the server or something and there's there's at 100 milliseconds there's actually an error and so we can replace that marble with this hash symbol right and the hash means error it's the equivalent of observer.air so when you provide a subscribe when you when you subscribe to observable notice that remember how there's three callbacks there's next error and complete this is saying that the air channel is going to get hit because there's a there was an exception there was an error in this stream so if we go back to some basic code we could create a cold observable saying that after one minute we just air out and that's will be our very simple expectation we say after one minute we receive an error and that's exactly what happens now if we wanted to provide a value for that error we can't use the second argument because that's a dictionary for the a b c d's right for the marbles and this is not a marble this is an air and there can only be one error because once once a stream airs it doesn't you know happen again so it's the third argument so that one and the third argument you can pass that error value if you want if you if you if otherwise if you don't have an error value you don't need that assertion so in this case we're throwing bad stuff and then we want to make sure that in fact we really are throwing bad stuff so if that all was confusing or maybe if even if it wasn't confusing you might want to learn more about this there's a couple things that i kind of skipped over um variations to this and also some more examples as well that you can find at rxjs.dev and then guide testing marble testing the other thing you can do is you can just search rxjs marble testing and it's one of the first i might not be the first but it's one of the first results one thing to keep in mind when you're trying to learn more about this is that marble testing has gone through a couple iterations and so there are outdated documentation like third-party documentation third-party videos and stuff like that that people have mentioned so the the thing to to quickly notice the difference is that the old version spaces were like if you added white space that meant something and in the old version one frame was 10 milliseconds which is weird but that's how we did it and then uh the other thing is that the sk the test scheduler didn't automatically get injected so in the old version you had to actually pass the scheduler to all your operate to all your time based operators which is a pain in the butt so if you see if you start looking at documentation like that you know you're looking at something outdated because the new version you don't need any of those things so if you have any questions i'm happy to answer them or if you just want to chat rxjs or other cool things you're welcome to reach out to me at twitter at underscore jay phelps remember the underscore and thank you all very much [Music] you
Info
Channel: TECHKNOW
Views: 983
Rating: undefined out of 5
Keywords:
Id: s9FY-MBW1rc
Channel Id: undefined
Length: 28min 7sec (1687 seconds)
Published: Wed Jan 06 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.