Check out a free preview of the full Testing Fundamentals course
The "Asymmetric Matchers Exercise" Lesson is part of the full, Testing Fundamentals course featured in this preview video. Here's what you'd learn in this lesson:
Students are instructed to write unit tests for the Character class. The tests include verifying a character has a first name, last name, and role, as well as testing the levelUp method correctly increases the characters level.
Transcript from the "Asymmetric Matchers Exercise" Lesson
[00:00:00]
>> Steve Kinney: But now, what I'm gonna challenge you with is, that's our little person. It's great, it's wonderful. We've also got these characters. I've never played Dungeons & Dragons, so no one fact-checked me on this at all or what any of this stuff means. I did Google, like, what the kind of diet you might use was, and that was about but we've got this rollDice function right.
[00:00:24]
And I believe it takes 4,6 sided. I don't know what I'm doing here, okay? I'm getting nods from the group, which means, like this is I might as well be making a sports reference at this point. So ideally we're gonna generate a whole bunch of random numbers as well as a new date just to make our lives extra hard.
[00:00:43]
And then some things that should definitely be what we passed in, right? And so your job is to the best of your ability, can you get these tests to pass? What, he's not gonna walk me through this and hold my hand no, I'm not gonna do it anymore.
[00:01:03]
This one, you're gonna take a run at it, get frustrated and sad, and then I will swoop in with the answers I wrote in advance.
>> Steve Kinney: We're gonna look at it from three different ways. The right answer is whichever one feels best to you, right? So we'll go ahead.
[00:01:24]
We'll mark this. It technically passes only if we're running our tests. Technically, it passes. Why does it pass? Because it doesn't fail. And that is the only prerequisite to passing. Cool, so we'll go ahead and we'll make a character and that will be a new Character let's call Ada and then correct me this is a real D&D class, right?
[00:01:59]
Kind of like a necromancer I believe If I'm not mistaken. We've got this computer scientist. And like I said, of the different ways we can tackle this one, we're gonna start with just the silliest or the simplest. Not silly, it's simple. It's very serious. It's just simple, which gets the job done and thereby is a right answer that counts.
[00:02:19]
Which is to say expect that character.firstName, very similar to what we were doing before, to be as long as you accomplish what you said you were gonna do on the tin, all right? If you say you're gonna verify, it has these three things you only verify two, then you technically lose, right?
[00:02:43]
But if, as long as you check all three, you win. And then we'll talk about some other ways that we can handle this as well.
>> Steve Kinney: Cool,
>> Steve Kinney: Roll,
>> Steve Kinney: To be computer scientists, right? Now, things that I might choose to do here, depending, which is what you don't want is this test to fail because you made a typo, right?
[00:03:26]
Which arguably is not bad for the long-term health of your code base cuz you will find the typo when your tests fail, but it is probably important to your short-term mental health sometimes. So what you could do do for instance, and I think for a larger thing, it might make more sense.
[00:03:41]
You could do something like firstName.
>> Steve Kinney: And this is also useful if you're gonna do like a lot of tests and maybe start using semicolons instead of commas
>> Steve Kinney: Which is I'm just gonna verify that they are the same and I don't wanna type these things over and over again.
[00:04:06]
This will just stop you from making typos, that's all. So you can do firstName.
>> Steve Kinney: And then here you can kinda do the same thing.
>> Steve Kinney: Definitely more useful for bigger objects, but the principle works regardless if you've made an overly contrived example or not. Cool, so that'll work.
[00:04:30]
The other thing that we can do is start to bring in some of those things that we saw earlier. There's some ones that we didn't talk about here as well. So if we did something, and this will show you like kind of the nature of toEqual. So we'll say expect character.
[00:04:47]
Let's start with a test that is gonna fail. toEqual, and we're gonna say firstName, lastName and roll. And while those are all true, the issue is that we have many more things, right? And so, that's they're not technically equal because there's a bunch of additional values here that you're not necessarily looking for, right?
[00:05:15]
Now you have a bunch of ways you can handle this we saw before there was that object or expect array containing or object containing, so we could do expect object containing, that one will pass, right? This will basically say, hey this character we want to equal. Some object that has the right first and lastName and role.
[00:05:40]
And so, for instance
>> Steve Kinney: This one fulfills the nature of the test as well, right? Where if something became not what we expected of the three things we're looking for in the name of this test, then we fail. But honestly, as long as it has these three properties.
[00:05:58]
We don't necessarily care about the other things that it has, right? This is good for like really gigantic API requests or something like that, or in some cases where you have a giant object and you're only trying to add one new property to it, like what you don't want.
[00:06:12]
Let's say that object came from somewhere else in the code base you're just trying to add maybe a create date or last modified date to it. What you don't want is that object changing somewhere else. If all we're supposed to do is add one new property to it, right?
[00:06:24]
You're like, I don't care what else is in this object, as long as it has a new property that I expect to come out of this, cool. And that's a way to make your test not, cuz if your tests are failing all over the place for reasons that are not important, what you're gonna do?
[00:06:36]
You're gonna stop writing tests, or you start removing them or stuff along those lines. Sometimes these strategies are you could argue, well, is this not as exactly cool? Were you gonna delete the test, otherwise? You were, and so sometimes it's about stopping your own worst desires. So this will solve for it.
[00:06:55]
To show you some other ways that we could do it, we could do something like we can do that firstName, lastName, and roll again, and that is gonna fail because we've got all these additional properties. What we could also do is to say something along the lines for instance, strength Night, we'll decide if we want to watch me go all the way with this.
[00:07:26]
>> Steve Kinney: We can do expect,
>> Steve Kinney: Which is to say we'll take anything of this type. So like, expect any number, right? Because we're getting it's a random number, we don't really know what to do here. So now you can see that in terms of things that don't match, as long as this is a number, we don't really care.
[00:07:48]
That way, we can control for the randomness as well. Can he remember all of the D&D properties on his own? We'll find out together. Austerity, that's one.
>> Steve Kinney: Intelligence, that's why I have an audience. Was it, I can also look it up. [INAUDIBLE]
>> Steve Kinney: You ever try to spell while people are watching you?
[00:08:19]
Cool, cool, cool.
>> Speaker 2: Charisma
>> Steve Kinney: Charisma, now, are we going for a victory lap over what this test actually says? Okay, so, some interesting ones in here. Level, we do wanna be one, right? Because they should start at level one. I should probably rename the test at some point, but we could say that level, that one we care about, that should be a default.
[00:08:40]
I might change this one, like, it should start out with the correct initial properties. The problem with this test is, if one thing breaks, you now have to squint at a very big diff, and there's an argument of breaking this out into little tests, right? Cuz you will know exactly which one failed if you break it out into a bunch of smaller tests.
[00:08:59]
It might feel tedious, future you will be happier. And so, the interesting thing about that, expect at any, is we're looking for numbers, right? Obviously, in this case, we might say that last modified is, expect any date. All right, so as long as it's basically a type of, or instance of, in this case, you're good.
[00:09:29]
And so at that point, we should only have that one that we saw earlier, create at created.
>> Steve Kinney: Awesome, and now it's just that person, or the ID, which we saw in the previous example, which is ID expect. I could do any kind of string, but again, in this one, I'll probably do, and it'll pass this way too.
[00:09:54]
Now, we can argue this is a bit much, maybe breaking it down to smaller tests makes sense, I agree. The point that I'm trying to make is that, when you just wanna verify that it is a date and not be particularly caring about what date it is. Now, I will tell you, can you make the random number the same every time?
[00:10:14]
You absolutely can. Can you stop time in your test suite, as a human, can you stop timing your test suite and set the time to something? You absolutely can. You can do all those things. But there's a question the very beginning of this workshop of, at what point do you take mocking too far?
[00:10:32]
At a certain point, the more you mock, the more you are intentionally divorcing yourself from reality, which sometimes means you've divorced yourself from reality. So in this one we're saying, listen, I don't care what number it is, as long as it is a number. And depending on what we care about, we can fine-tune it a little bit more.
[00:10:48]
Some other strategies we can take when we don't totally know what everything's gonna be is, this one's pretty easy. This is mostly a sanity test, right? We could say,
>> Steve Kinney: I don't have these values in here. What I'm gonna do real quick, for my own sanity, I'm just gonna move these up to the outer scope, so I've got them available.
[00:11:22]
>> Steve Kinney: First name, last name, and role. And we can say,
>> Steve Kinney: Level up. We could expect,
>> Steve Kinney: Cool, it passes. And then, this is an interesting one, cuz again, it should update the last modified date when leveling up. Now, later on I'll tell you about how to set the time to recurrent time and how to, you could basically set the time, create the thing, move the time forward a second, and verify all that stuff.
[00:12:04]
Really like, if I know that my implementation is new date, I could assume some things about time, is it tends to go into the future, right? You can choose your metaphor in this case, which is, time keeps on taking into the future, or cuz we're in Minneapolis, times are a changing, whatever you want.
[00:12:24]
And so, for this one, this is where I might choose to use that not thing that I showed you earlier. Again, under things that are helpful, depending on what you're trying to achieve. And I will remind you, what is the right answer? The right answer is whatever makes you feel good about refactoring your code.
[00:12:38]
If you're on a team, then it's more democratized of whatever makes your team feel better. But generally speaking, it's about getting to the thing that makes you comfortable, not some kind of, weird rite of passage. Cool, so we can do character = new Character. First name, last name, and role, and then we can expect the, we'll go ahead and we'll level that character up.
[00:13:14]
>> Steve Kinney: And really what we're assuming is that we've now modified the character, so that created at time should not be the same, as the last modified time, right? So that's a case where I might choose to use that not. So then we expect,
>> Steve Kinney: Character.lastModified.
>> Steve Kinney: Not to be,
>> Steve Kinney: What I'll probably do is, I want the same reference.
[00:13:46]
I could stringify it, but let me do this,
>> Steve Kinney: So, we make the character, we're gonna store on to their initial last modified date, just put in a variable. And then we're gonna level them up. And what really, all we wanna make sure is that the new last modified date is not the original one, right?
[00:14:19]
And again, because we can make some assumptions about time, we can assume that it's in the future, right? If it were some other value, you might also choose not to do that as well. And there's nothing stopping you from verifying stuff before and after. Most tests fall with this thing called a triple A pattern, there's the arrange, act, and then assert, which is effectively kind of what I'm doing in these spaces.
[00:14:45]
There's no rule that you have to do that, it's just a way to kinda think about your test, right? We get everything in place, we do something, and then we verify stuff. But if it's helpful to you, you could do something like, I might break this out into two tests personally, cuz if i test fails, I want to immediately know what it is.
[00:15:03]
But theoretically, have I done this before in my code base? 100% I have, which is, make a new character, expect it to be level one, then up them to a level two. Again, it's all about what makes sense for you and your code. You could also choose to, like, we already have that kind of, in this test as well, right?
[00:15:24]
And what I don't need is two test breaking for the same reason. So how you break this stuff up is, again, going to be case by case based on what is most helpful for you. But again, breaking stuff up into small, well-named things has never hurt anyone, ever.
[00:15:37]
We're just trying to make sure they're not the same object in memory, right? Which is like, that was a data object, we're placing it, right? With numbers though, you could do something to, like if we wanted to take that suggestion and apply it somewhere else, we could do something like
>> Steve Kinney: const.
[00:15:59]
Initial level. And little weird for this example, but you can think of plenty of cases in a business logic application, where this might be true. Whereas initial level is character.level. We level the character up, and then we say we want the character level to be greater than initial level.
[00:16:26]
>> Steve Kinney: And that one does work. And so now all of a sudden, isn't companies like Microsoft, the first engineering level is a level 55 engineer, right? So now if people like, if you change the rules of the game, we don't start at level one. You start at level 55, this test will still pass, right?
[00:16:45]
In this exact example, little silly, but you can see words where it's as long as it is higher or something along those lines. There are some helpers for those kinds of things. At the end of the day, you could always do something like expect character level greater than 0 to be true.
[00:17:02]
Like in a pinch, these are all just helpers around as simple logical statement. But these are also a lot prettier.
>> Speaker 2: Good question.
>> Steve Kinney: Yeah.
>> Speaker 2: So this new level up test right here, if you modify the original function to incorporate a starting level, this test would cover that refactoring.
[00:17:23]
>> Steve Kinney: Yeah, right, let's do it. So we'll actually have this also take a level, and we're gonna say that that level is 1.
>> Steve Kinney: And what's cool about that is all my tests pass. So I had tests I refactored I know that I haven't broken anything that at least I tested for.
[00:17:42]
I might have broken stuff, but nothing that I tested for, and now we can set that and like that, that test will still work where it might have broken otherwise. And the breaking test isn't bad if you know you were changing something, right? But in this case, our test is more resilient to that change.
[00:17:56]
That's a great point. Yeah and so, and that's kind of gives us at least a reasonable reason to talk about a concept that we will touch upon several more times later, which is when we can't control stuff. There is also this pattern in software, that's not just about testing, called dependency injection.
[00:18:23]
Code that is not testable is because there is stuff, if we go back to this character, right? Roll dice is imported in the module, and it just lives inside of this constructor function, right, which means it is real hard for us to get our fingers on it, right?
[00:18:39]
We'll look later at mocking stuff, totally. We'll look later under the hood, they call math.random. We can look at all those things, but one pattern that works really well, and it's not necessarily a testing pattern, it's just a software pattern, is what's called dependency injection, right? Which is every function that this thing might use, you could theoretically pass in, right?
[00:19:03]
And so for instance, an example of this might be, we could do something like roll in this case = rollDice, right? Test pass because if we don't pass the roll function, it's gonna use that roll dice that we passed in, right? But for instance, now we could theoretically say that last one.
[00:19:28]
It's nice that I have both a roll with an e and a roll with two l's. As you can tell, this was not the example I had planned on this for, but it works. Which is to say that roll is just going to be a function that is always gonna return 15, right?
[00:19:46]
What you mad about?
>> Steve Kinney: There was that level, was the argument first. So now theoretically, if I got rid of all of these.
>> Steve Kinney: Watch multi cursor action, there we go.
>> Steve Kinney: But did I not replace them all? These are all still roll dice.
>> Steve Kinney: We'll take the one that was passed in.
[00:20:28]
Okay, somewhat silly for this example, but it came up, so we did it. More useful is, let's say you have a React component that, when it mounts, it goes to fetch from an API, right? If that's just a use effect that is hard coded in there, well, good luck mounting that without calling the API, right?
[00:20:50]
But maybe your reaction component instead takes a prop called fetch data, right? And that fetch data by default, if you know what the fetch data is passed in for that prop calls the API, right? But it allows you if you take that component now in your test suite, you can pass in one that just returns an object that fulfills the contract.
[00:21:10]
And now you don't have to mock anything out or stub anything, which we'll talk about in a little bit, right?
>> Speaker 2: You have an example of that hopefully [LAUGH]
>> Steve Kinney: Of course we will.
>> [LAUGH]
>> Steve Kinney: We just got to a little bit early, we're just ahead of schedule.
[00:21:21]
Absolutely, that is almost a crescendo. But it was a good excuse to bring it in a little bit early. So if that stresses you out, don't worry. We'll cover it again. But the concept is basically take all the things that maybe you can't control or that you don't have access to and allow yourself to pass in those things as well.
[00:21:41]
Cool, cuz we don't really care about, in this case, roll dice becoming a random number. We could test that function separately. We just care that this thing gets back the properties it ought to, and the shape that it has.
Learn Straight from the Experts Who Shape the Modern Web
- In-depth Courses
- Industry Leading Experts
- Learning Paths
- Live Interactive Workshops