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

The "Asymmetric Matching Solution" 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 live codes the solution to the Asymmetric Matching exercise. The full solution can be found in the `items-slice.solution.test.ts` file.


Transcript from the "Asymmetric Matching Solution" Lesson

>> There's two things I kinda wanna point out. One, we are testing a Redux reducer. It's a real Redux reducer. It's like coming from one of those, create React, or no, Redux toolkit and slices, right? So one of the things that I feel and my hills that I'm willing to die on, is taking as much of our state management out of our view layer as possible, makes it easy to unit test, right?

Whether or not you use Redux or you use Reducer in just regular React function or use that pattern, right, somewhere else as well, all a reducer is, is a function that takes the current state of the word, and a thing that happens and spits out a new state of the word.

And because it's a JavaScript function, super easy to unit test, right, without having to mount the component. Hit the button, you might wanna make sure that buttons are disabled and stuff renders appropriately. But what's really nice about it, it's just a JavaScript function. And we can grab it, and we can test it because again, it's just a function, takes two arguments, spits out one argument.

So we can go ahead and we can see that for ourselves. And then we can go ahead and look at some of the asymmetrical matchers to show, hey, if somebody adds another field to this object because for reasons, right, disabled, enabled, I don't know what have you. My tests won't break cuz they're expecting a very specific structure.

My tests will look at the things that I care about and not look at the things I don't care about. And again, can you do this individual expectations as well? Sure, but a lot of the times, what you're aiming for here, is not necessarily writing less code. It's like getting a better error message when it breaks.

Be like, cool, you just saw one thing broke, right, versus across a bunch of tests versus like, cool, this is one object, hey, it got all these things except for this one thing here. You'll get an error message that shows you, the difference in the object. So my first test is in fact already running, let's just do npm test.

I'm gonna say item slice, and I'm gonna say .test because I have one called .solution. It's got the answers in it. And so normally, I wouldn't have to put the .test, but because they're similarly named. So one passing 7 todo, all right. Let's go ahead and let's make it, in this case fail, cuz it does not have any assertions, we'll keep that here for now just to make a point.

So with reducers, if they get an action they don't know how to deal with, they don't have a case for, nothing happens, right? In this case, if we look at the reducer, not the type, if we look at the reducer, the default state is an empty array, and so it will always return that state as an empty array.

Cool, cool, cool. In this case, we are gonna add, and we'll send it an object with the name iPhone. Now, what's super great is, you know what I don't have to do? Write a bunch of tests where I call with the add type, but without something with the right properties of name and iPhone because, you know who's checking that for me?

TypeScript is, right? So if I said something like model 14, you can see that I already got a red squiggly line without having a giant set of tests that I used to have to write. I don't have to write any more, and I'm happier for it because again, I like to ship product features confidently, I don't actually like writing tests, right?

I just like not feeling really scared whenever we do a deploy, right? Unless it's a change to auth cuz I break auth every single time I touch it. Cool, cool, cool. So we have this, so we can say we can expect the reducer, we haven't got the result in this case, we expect a result, which is that return value, right?

And in this case, we can say that, it should equal, Equal an array, cuz we knew that it spits out an array. And honestly, whatever else gets added to this type, maybe there's more features other than just name and packed and then ID, I don't really care, as long as we get an object.

So I can say, it should be an array, and we expect an object containing, In this case, name: iPhone. Now we can guess, just by looking at the type, that there is an ID on there and there is a packed value. I say we could all look at the type, but I also wrote the code, so I happen to know that, and that an item is an array.

We can go sec, they've got an ID, they've got a packed, so on and so forth. But as long as we had an array of one thing, if I put a second one in here, for instance, that will fail. Because we said an array of one thing, as long as that item, as long as that one thing has the name iPhone, right, other things can change in there.

But we are validating that there is exactly one thing in that array, that is safer than popping the first thing off the array, right, cuz it could have been a second thing in there, you didn't test for that. That means that test didn't fail, which means your code didn't work, and your test passed, worst case scenario.

Here, we are assuming when we say that it is an array, and that it has exactly one item in it, and as long as an item has a name iPhone, I don't care anything else about it. Yeah, I can be cavalier about this cuz I have TypeScript, then I can assume that this test is probably more resilient than me trying to get clever with things.

Again, try to index zero off of it, cuz next thing after you go like, okay, expect results that length to be one, then expect that the first item has a name property, then expect, you know what I mean? In this case, I can kind of say that a lot more succinctly.

And then if we scroll up here, right, I kind of get a better, we have that one ObjectContaining with name iPhone, great. And then there was a second object in here, that is a lot clearer for me to read than which one of those assertions broke.

Maybe the length one would be helpful, but again for me, this is super clear to be like, hey, there was one I was looking for, and then there was another one, +, +, + above it in red, that shouldn't have been there. Cool, and then we wanna make sure that it prefixes those UUIDs with item in this case, so we'll go ahead and we will, it fails because there's no assertions.

Cool, so in this case, we could combine these a little bit. So I'm gonna grab this, And I'm gonna say it's got an id, and that should be, StringContaining. And I'm gonna say that it starts off. That way, I'm getting a little more specificity. And we fail, why do we fail?

StringMatching, if I'm gonna use a Regex. Cool, and now it passes. Again, I'm just verifying the parts that I care about, and I can move on with my life, and this test will be resilient, Mark?
>> If most state mutations occur on the server and are refetched to update the DOM, does it still make sense to make all of the unit tests async, or is there a better way to test async code faster?

>> So if most of the state management is happening on the server, ideally, a lot of this is happening on the server. In this case, this will also come up when we get to mocking and spying, which is insofar that we talk to servers, the majority of the asynchronous things we do are talking to servers.

It all depends, if there are things like a transition that should take a certain amount of time, or something along those lines, then sure. But if it's a network request, and it's code that you don't control, then might not have a lot of asynchronous stuff happening in your code, right?

This reducer is completely synchronous, for instance. We don't usually for the most part tend to purposely add promises to our code when we don't need to. However, there are certain browser based APIs that return a promise by default and stuff along those lines. So in those cases, then we do need to use async code, but as much as you need, as little as you can get away with, is I think, a decent heuristic for that.

But yeah, And these aren't necessarily asynchronous. These are just basically partial matching an object for the parts that we care about so that our tests don't break, cuz something we don't care about changed. Yeah, this one will be roughly the same concept that we saw before, has a packed status of false.

I was just gonna show you that you don't necessarily have to do this. If you hate this, and you're like, no, this is not for me, let's look at what it would look like in the other direction, you can see more of the same in the solution. So I could say, expect and grab the first.

We can do, expect(result.length), Cuz we wanna make sure this is valid .toBe(1), okay? But whenever you write two assertions, you gotta make sure you write the second one, cuz this test isn't doing what it says on the tin just yet. So then I could expect, so then we could grab, const item = result, expect(item.packed).toBe(false).

Now, the reason that I hate this is, cuz if I mess this up in any way shape or form, right, or maybe it starts out with a default set of things all of a sudden, right, this is problematic. Let's say that code broke and it's not actually setting it to false anymore, this test would still pass versus, when I'm just saying, it should be one thing, it should have an object.

I don't care about anything else as long as it's got the thing I want, this is one expectation, one error message. Very clear, this can get a little squirrely. For instance, if I make this true, this will probably be pretty clear cuz it's pretty easy. But you can see in a bigger object, it might not be.

Supports removing an item, right, in this case, we might wanna verify that does not contain that, right? I could verify in this case that it's an empty array again, that would technically do the trick, but spiritually, not really. So in this case, I could say, expect(result).not. I could use a toContain, or I could actually toEqual an empty array.

In this case, I wanna make sure that it does not have, Too much object. Let's say. I'm doing this live at this point, but let's say, real time follow up. Yeah, so this guy is like, hey, as long as it's not any object that contains the id of 1, I don't care about the rest of the details here, right?

It just should not have the 1, that I just explicitly removed. This way, if this became a lot bigger and we didn't wanna have this in this test, to the question I was getting earlier, and we wanna just have one maybe larger objects that we're kinda pulling in, and maybe I don't wanna see it in the test.

I can be very be like, listen, I just removed this one. I don't really care what's in there as long as it's not the thing I just removed, right? That is effectively spiritually what our code is doing, not anything we might write to get to the same result.

Cool, and we can see more of the same basic ideas here, definitely check it out in the solution. In the sake of time, I'm gonna kinda put a pin in there as well, we kinda get the point of, how we can be somewhat flexible. And again, this goes to that architectural question of, how do we have a test suite that doesn't break, right, so that we trust it, right?

Cuz even if it's like, you as a human, you get to the point where build breaks. You get to one of those things which is like, you don't necessarily believe it was you anymore, which has already made your test suite worthless, right? And so, sometimes the way that we structure the way we think about these things, it's not about necessarily a number, it's about that gut feeling of, if you find yourself not believing that the build process failed for a real reason.

It's like that is the sibling of, I don't feel comfortable changing this code, right? And both of those are like, this is a very technical course, where we're relying on our gut feels as our barometer of success, right? Can I bump that dependency? Can I just run npm-check-updates -u, all wild and just be confident that if my tests pass, I'm good?

If the answer to that question is no, then that's where some of these strategies begin to take place. And some of those will be unit tests, some of those will be like, listen, I had a browser go work through the entire application. Touch everything like, yeah, we mocked out the network requests, or something like that, but works, I feel comfortable, right?

Great, right? But generally speaking, it's usually a cocktail of both unit tests, as well as these larger tests to make sure that some little thing isn't breaking and the big thing isn't breaking, and that's what gets me to that confidence level in this case.

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