Check out a free preview of the full Enterprise UI Development: Testing & Code Quality course

The "Async & Asymmetric Tests" Lesson is part of the full, Enterprise UI Development: Testing & Code Quality course featured in this preview video. Here's what you'd learn in this lesson:

Steve demonstrates techniques for handling async behavior in unit tests. Asymmetric tests are also shown and are helpful when a value is generated each time, and the test only needs to know if it exists or is a proper type.


Transcript from the "Async & Asymmetric Tests" Lesson

>> Just to kinda show, I will use this half done asynchronous function to just talk about some of the edge cases are on async. We'll kind of live code for a second instead of going into that whole exercise cuz we're here. So in this case, right, it does modify.

Even though it's technically an asynchronous function, this line does run synchronously, it's mostly the return value. Splice, what does splice give me these days? It will give me, let's see. Let's actually just have it return, The number four just to make my point. Cool, and so if I say const, Return value, And in this case, we'll say, cool, expect that.

And then also expect, That its return value, To be 4. This one will break cuz we've got a promise which hasn't resolved yet because we run this code, that returns a promise that hasn't resolved. Next tick of the event loop, it will finally resolve and we'll have it cuz there's no network call, it's just a promise, it's a job execute technically.

And so this is now the promise which is, the promise is not 4. I have a few different ways that I can deal with this and we'll talk about some of the differences between them. One, the most common one, Is, I can just await it and then we'll pause right there, we'll do other stuff.

We'll pause, we'll wait for it. Now, return value is in fact 4 and my test passes, cool. The other thing that we can do is, I can say that this promise resolves to be 4, right? Really, the time that I would choose to use this, I probably just await it most of the time, where I would choose to use this is not necessarily resolves to, but cuz resolved await works just fine, its cousin, reject, is the more interesting one, right?

Because in an async await function, this will throw an error. And there are ways to check to see if something throws an error, but then trying to catch it and make the expectation, but if you didn't make the expectation cuz you didn't catch the error, then the test passes, right?

That's when I would choose to purposely say, hey, this promise should reject to be this error, this value. Usually with the unhappy path is where I'll use resolves and rejects. For resolves, I'll usually just use async await, but trying to do a try-catch in your test, that's a lot of logic.

All of a sudden you never wanna get your test to get to the point where we have so much abstraction, you're so clever that you need tests for your tests. All right, like cue an Xzibit meme, right now I heard you like tests so I put tests in your tests, right, you don't want that in your life.

So a lot of times being overly simple and repeating yourself versus being clever, cuz if you get too abstract, who tests the tests, right? So yeah, the only time I would use that one really is if it does in fact throw. And there's an asynchronous folder in here that we might visit, which will kind of show you some of this as well.

You can expect it to be, you can reject it in this case. This one will throw. Or I'll just reject it, but you can actually see that it does the thing you want. Also if you throw inside a promise that technically rejects it, so on and so forth.

But most of the time, I would just await it. And the nice part is you saw when I didn't await it, there was expected 4, received promise. Pretty clear what the issue was, right? Not particularly versus callbacks where you were just kinda bewildered. [LAUGH] I don't know why it's undefined.

Cuz I didn't put done somewhere in there or something along those lines. So the modern era is great. JavaScript is wonderful, everything is good.
>> Earlier, you mentioned comparing objects where we only cared about a few of the fields. I think we kinda went over this, but how would you go about testing to identify an object based on incomplete information or just a few fields?

>> So, yeah, the question was basically, if I only care about a few properties of an object, let's say I've got some objects that I work with which are gigantic, and maybe I only care about the type field on it or something, or does it have these two or three properties, and I don't care about the rest of the ceremony.

And then I find myself bringing in these giant JSON representations of what I'm expecting only to see two fields that change. You have two options, and it really depends on what you're going for. One of them is super simple, it's called toMatchObject and might as well also talk about its sibling to match, which works with strings and regexs, which will make sure that it's.

So if you don't actually care about the casing, you could do a case insensitive regex. That's usually really helpful for a lot of things, particularly when we're doing component testing. But to answer your question, we've got toMatchObject, which basically takes a subset of that and checks to see if that subset.

So here if you look at the example in here, we have the John invoice where the customer in there has got all of the details, and then John details is also that roughly the same object, right? So you can see does the invoice have the customer field and does that kinda matching just on that and we don't care about the rest.

There can be more nuanced ways where we're somewhere in the middle, where maybe we don't care about everything, but we care about some things, right? And for instance, to kind of that half joke that I made earlier, if you find yourself stubbing and mocking your own code, you probably should take a moment go for a walk and think about it.

But places where that might be the case which is if there's any kind of random ID or UUID generated, right, either you have to choose to not care about that, right, that field, or, right, you can mock it or stub it, right? But there's a middle ground in the middle, which is completely not caring or not caring in a different way by overriding it, right?

But okay, the property should be there and it should be a string. Could you do that by hand? Does TypeScript help? Absolutely, but I think I'm always shocked and maybe you all are smarter, so maybe you've all seen this before, but I'm always shocked to see how many people don't know about this.

So it feels worthy to include is this idea of what we call asymmetric matching, right? And asymmetric matching, if we actually look in the docs here, we can kinda see it towards the bottom which are these ones here which is they're not necessarily after you have the value.

But they are like, expect anything which is there should be a value. Any is a given type here, let's click on that one, which is cool. Yeah, generate ID could be a unique value, it could be random, it could be different every time, so good luck testing that.

But if you wanna make sure that we get an object where the property has an ID and the value is, as long as it's a number, I'm good. That way you don't have to mock it out. You don't have to do a whole set of weird expectations. You're saying, hey, I'm expecting an object that has an ID and any number, right?

I don't care, it should be a number. And then maybe you'd have the rest of the property. So it's like really maybe it's a person, and you care about the first name and the last name are what you put in, but that ID is randomly generated, and you don't want that breaking your tests, and you do wanna make sure it's a number.

Could you write this with enough expectations of, expect first name to be blah, expect last name to be blah, blah, expect type of ID to be number? Could you do this, but this is a lot easier, to just define those properties and have an object the way you thought it should be?

And it's got some friends, which is object containing, which is cool, we wanna have an array, and for instance, there should be an object that contains name. This is kind of that partial matching that you were asking before with toMatchObject. There's ways to do this, it definitely should be an array.

It definitely should have an object, one object in it, and that object should just contain this property, I don't care about the rest, right? And it lets you kind of be very specific about the things you care about without one, getting to this tedious, cuz yeah, writing this test is not providing you a lot of value.

You know what you're gonna do? You're not gonna write it at all, right? Or if it's flaky, you're gonna delete it, right? And so, sometimes it's about getting very specific and very clear on what exactly you care about and what gives you the confidence. When we talk about code coverage, for instance, I don't care about the number.

It's like, do you feel weird when you have to change this? If you wanted to bump a dependency or package json, do you feel comfortable? That's what we're going for rather than 100% code coverage, you get a trophy. And so, writing your tests where it's cool, it is checking everything I care and it's not getting flaky and it's not getting weird about the stuff I don't care about, and I'm keeping the test, provides more value than either not having the test cuz you're gonna get tired, right?

There's a human nature aspect here. So how do we balance that with reality and begin to deal with it? Cool, let's just glance at the asymmetric stuff one more time and make sure I didn't miss anything cuz with the really great questions, we kind of off roaded it for a little bit.

So let's just kinda look at these real quick. This is an anti-pattern. This one passes, even though it should fail, right? It passes before it fails, you know what I mean? We run through the function. This is put on the event loop a second later, but the function's already done, we've moved on with our lives when this expectation fails, right?

So that when we could have a promise that resolves, we could fake time even later on we'll see. There's a whole bunch of ways to deal with this. But know that this is a really great way to make this test fail and then see if at least the error message roughly looks legit to you, right?

Another way to do it is to make sure that there were assertions at some point or another. In this case, this will pass cuz we got zero assertions, right, and you go, cool. And this one will purposely fail cuz it doesn't have any assertions. So in this case, if it is a callback, you can wrap it in a promise that resolves or there's various things that you can do in this case.

There's a way in if I test in Jest to basically move time forward until all scheduled timers are done, that would also do the trick. We'll see that a little bit later, the second half of this, but just to be aware of that, again, you should treat your passing tests with a certain amount of suspicion if you have a weird feeling in your stomach that it shouldn't have passed.

There's nothing worse than the like, it worked, and you don't know why. It's worse than it breaking and you don't know why. Cool, but yeah, generally speaking if you live in a promise based world like I do, you can either just await it or use resolves. I don't have to deal with that many call backs in my life anymore.

The modern era, like I said, is wonderful.

Learn Straight from the Experts Who Shape the Modern Web

  • In-depth Courses
  • Industry Leading Experts
  • Learning Paths
  • Live Interactive Workshops
Get Unlimited Access Now