Starting CouponCode tests0:00
All right, let's move on to a brand new example. We're going to test drive a CouponCode component. So in your head, imagine an input that will accept a coupon code. You type it in. If it's valid, we're going to provide some feedback to you, and then this component should emit an event. So it should fire an event notifying any listeners which percentage discount should be applied. So this, you might find this on a billing form or something like that. All right, so I have our spec defined, and I have a blank CouponCode component. Let's go ahead and import that. So that'll be in our src/components/couponcode.vue. Okay, our first test. Well, to begin, it should have an input to accept a coupon code. So it accepts a coupon code. This might be an easy first test to write. Sometimes it's good to write these simple tests, if only to get you in the flow. Later, remember, you can delete the test.
So it accepts a coupon code. This might be an easy first test to write. Sometimes it's good to write these simple tests, if only to get you in the flow. Later, remember, you can delete the test if it doesn't provide any value at that point, but just to kind of jumpstart you into the flow. So we might say, well, we need a wrapper, so mount the coupon code, and then we'll say wrapper.contains an input, and it'll be called coupon code. Yeah, I expect that to be true. Okay, so let's give this a run. npm run watch. And let's see. Nope, it failed. Okay, let's go over here, and we'll start building. So we have our template, our script, and then here, at the very least, we're going to need an input. So we add that, we save it, it runs again, and yeah, now we're at green. Okay, let's move on to something else. Next, it should validate a coupon code. So it validates a user-provided coupon code.
Validating coupon input1:34
So we add that, we save it, it runs again, and yeah, now we're at green. Okay, let's move on to something else. Next, it should validate a couponCode. So it validates a user-provided couponCode. All right, now we're starting to get to some actual behavior. So what do we want to say? Well, to start, I want to find this couponCode, I want to fill it out with a code, and then at that point, I expect some validation to take place. And if it is valid, we need to provide some feedback to the user. So let's do that here. Let's begin by tracking down that input. And then we learned in the last episode that we can fill its value. I wish there was a type method out of the box, but for now, we can say element.value equals some kind of code, and then I can say couponCode.trigger(input event), so that view will pick up on it.
out of the box, but for now, we can say element.value equals some kind of code, and then I can say couponCode.trigger(input event), so that view will pick up on it. All right, so once we've typed into that, at this point, we expect validation to occur. So yes, I could say something like expect wrapper.vm.valid to be true, but again, that's testing implementation details. Or in other words, why do I want to test that the component's valid property has been set to true when maybe we use some other form to test whether it's true? You know, there's no need to test that implementation detail. So with that in mind, well, they would expect some feedback because it is valid. So why don't we say wrapper.html, and I'm going to expect that to contain 50% off, some kind of message, and maybe even more specific, like coupon redeemed 50% off. Okay, so does that make sense? Find an input, type in 50% off, and at that point,
we will need to store that, and by default it's false. Now if it is valid, we should see coupon redeemed, and then we will need some kind of message. All right, so we'll come back to that, but now the User is typing into this input, so it sounds like we need to track what they type in. So we'll call that couponCode, and once again we'll default that to an empty string. Or you know what, why don't we change this to code instead. Now as the User types into it, we're going to validate it. So on input, call the validate method. All right, we'll accept that. So again, notice I didn't start by writing a unit test for validate. No, you can do that sometimes if you want, but why do we care whether it's called validate or something else? Why do I care if we do an end line or we defer to it? We don't care. It's an implementation detail. So here, how would we validate it? Well, I need a list of real coupon codes. Now I'll tell you in
care if we do an end line or we defer to it? We don't care. It's an implementation detail. So here, how would we validate it? Well, I need a list of real coupon codes. Now I'll tell you in real life what you're going to do is submit an HX request. So you take what the user types in, you send an HX request to your server, and your server lets you know is this a real code or not, and it'll probably look in the database. I don't want to do that yet because I want to dedicate a full lesson to HX requests and how you can mock those out. So for now, we're going to take a naive approach and just create our coupon codes here. But yeah, just remember, you would never want to hard code this because they could look at the source and see, oh, you have a 50% off code, I'm just going to apply this. So keep that in mind. It's just for the demo. All right, so what would a coupon need? The code itself, so something like 50% off, a message for the user,
I'm just going to apply this. So keep that in mind. It's just for the demo. All right, so what would a coupon need? The code itself, so something like 50% off, a message for the user, like a readable message, so 50% off. And then finally, the actual percentage discount, which would be 50, one half. All right, let's do one more here, and this will be entirely free. So entirely, and the discount would, of course, be 100% off. So now let's bring this down. If we're going to validate what the user typed in against the codes here, well, it sounds like we need to say, update this valid property and make it equal to whether this array of objects contains a code equal to what the user typed in. So we could do this in a couple ways. One, I could say this.valid equals, and we could, we'll map over the coupons and say for each one, I actually want to return a new array that contains the codes for each of them. So now.
I could say this.valid equals, and we could, we'll map over the coupons and say for each one, I actually want to return a new array that contains the codes for each of them. So now at this point, you would have an array of 50% off and free of strings. Okay, so then at that point, you could just say, does that new array include what the user typed in? All right, so now it's still failing, but you'll notice at this point, yeah, we do see coupon redeemed, so it did update valid. And actually, on that note, remember, for your test, you don't have to keep everything. So I would say in your test, asserting what valid is, that's kind of an implementation detail, because maybe later, like I said earlier, the way you check if the message should be displayed has nothing to do with checking whether it's valid or not. So I don't want to assert against that, but sometimes when writing
the way you check if the message should be displayed has nothing to do with checking whether it's valid or not. So I don't want to assert against that, but sometimes when writing your test, sure, have at it. So you could say, expect wrapper.vm.valid at this point to be true. So if we run that, yeah, ignore the warning, we'll get to that. But yeah, now that's returning green. So that's sometimes good for a sanity check where maybe it's failing and you're not sure why. Well, you can do some assertions here and then just delete them when you're done with it. But anyways, at this point, we're seeing couponRedeemed, but message, that's nothing right now. So it sounds like we need to say message should be a computed property that will spit out the message associated with the code that the user entered. So we'll have message here. And yeah, so once again, it sounds like we need to find the couponCode or the object associated.
Showing redemption message7:54
spit out the message associated with the Coupon code that the user entered. So we'll have message here. And yeah, so once again, it sounds like we need to find the coupon code or the object associated with the coupon code the user entered. So we'll do this. I'm starting to see a little duplication, but we can refactor later once we're at green. So find me the coupon, the first Coupon where the couponCode equals what the user typed in to this input. Now once you do, give me the message associated with that object. So we will return that. Okay, and now we get to passing. Now let's write one more test and then we will do a little refactoring. So we also said it should broadcast an event. So it needs to tell any of the outside world what just took place. And what just took place is, well, the user entered a valid couponCode which entitles them to 50% off. So you can imagine like a billing page listening for that event and then dynamically updating the price.
Emitting applied event8:47
place is, well, the user entered a valid coupon code which entitles them to 50% off. So you can imagine like a billing page listening for that event and then dynamically updating the price of all the plans as a result. Like, okay, 50% off. Okay, well now every single price should be half of what it was before. So let's say it broadcasts the percentage discount when a valid coupon code is provided or applied. All right, so how can we do this? Well, I'm going to show you two ways, actually. So once again, we'd have to duplicate this. But notice I recreate wrapper over and over. So let's do what we've been doing lately is assigning it to beforeEach. So we'll say wrapper equals mount coupon code. Now I can get rid of that. And are we still a green? Yes. Okay. So now, yes, we could say, well, once again, fill out a coupon code. Next, it's a valid code, so we expect an event to be emitted. So the way we can do that with ViewTestQTLs is I could say
So now, yes, we could say, well, once again, fill out a couponCode. Next, it's a valid code, so we expect an event to be emitted. So the way we can do that with ViewTestQTLs is I could say wrapper, get your emitted collection, basically, and I'm going to expect an emitted event called applied. So let's say expect that to be truthy. Now, we do to be truthy. That means we want something that is of a truthy value. It doesn't have to be the Boolean true, but it should still evaluate to something truthy. Okay. Anyways, we're going to run this and it's going to fail because we don't fire the applied event. All right. Let's come back to couponCode and write down here after it's valid. Well, if it is valid, in that case, we should emit an event. So emit applied, run it again, and now we get green. But that's not enough. I want to make sure that it fires the applied event, but I also want it to provide the necessary data with it. So it needs to tell
run it again, and now we get green. But that's not enough. I want to make sure that it fires the applied event, but I also want it to provide the necessary data with it. So it needs to tell the outside world, yes, a coupon code was applied, and it entitles them to 50% off. So here's probably the easiest way to illustrate this is to show you what gets returned. So if we run this, yeah, so if we console.log(wrapper.emitted), we can see that an applied event was emitted. And in this case, no value was provided. But if we were to send one through, like 50% off, yeah, now that will be sent here. If we do another one, you can see how that works. So if you want to access that value, you could do something like this. this.applied, get the first item there, and I expect that to equal, to equal, in this case, 50. So let's start from scratch. It'll run, and it fails, because we expected the component to
this. This.applied, get the first item there, and I expect that to equal, to equal, in this case, 50. So let's start from scratch. It'll run, and it fails, because we expected the component to tell the outside world that a 50% discount was applied, but it wasn't. So now, hmm, we're going to have to get this discount code. So yeah, once again, we're going to have to do another thing. Let's see. Let code equals this.coupons, and then I'm going to repeat this. So again, you see this duplication where we try to track down the selected coupon code, and that's keyword there, selected coupon code. Let's file that away. Anyways, find the selected coupon, which will be this, and then we're going to reference the discount property. Pass that through. And, whoops, let discount. Run it again, and now we get green. So I can come back here and remove that, and that's looking good. Now, I noted for this test, you could do
Pass that through. And, whoops, let discount. Run it again, and now we get green. So I can come back here and remove that, and that's looking good. Now, I noted for this test, you could do it in two different ways. So when you're writing the test, yeah, in terms of how closely you lock yourself to the implementation, it's never all or nothing. There's always a little bit of give and take, and you'll especially notice this as you mock things. Well, as soon as you mock it, you almost bind yourself to that exact usage. So when you say, I expect this object to call that method, you're binding yourself to the implementation, and sometimes you kind of need to do that. That's what I mean when I say it's never all or nothing. So in this case, we are essentially triggering the validate method by going through the UI, and that's a really good thing to represent in your code. Now, there's also situations where if you're going to do that in 10
Alternative testing approaches13:10
essentially triggering the validate method by going through the UI, and that's a really good thing to represent in your code. Now, there's also situations where if you're going to do that in 10 different tests, in those situations, you might just call the method directly. So let's see what that would look like. I'm going to comment all this out. It's all going to fail, of course. And instead of saying, fill out the input, type in 50 off, and then trigger the input event so that we call a validate method, why don't we just say, call the validate method. Now, you'll see we can call any method directly by saying wrapper, get me the viewModel, and then call the validate method on it. So if we take a look, though, validate method is going to look at the user's entered coupon code. So if we want to say, well, let's just create a world where the user typed in this code, what you could do is say, wrapper.setData where the code is equal to 50 off.
entered couponCode. So if we want to say, well, let's just create a world where the User typed in this code, what you could do is say, wrapper.setData where the code is equal to 50 off. So now if I run it, I bet we get green, and we do. Okay, so you've seen two different ways to tackle this, and I'm not going to say one is definitely better than the other, because it always just depends. But I'll show you with this option, it's a little cleaner and easier to set up, but it also locks you to the implementation a bit more. Because now, think, the test knows that the underlying code is going to reference this property here. So now if at any other point you store that in a different way, or you just think, well, this is a little more readable if I call it couponCode, as soon as you change that, everything's going to blow up, right? So you want to be careful of that. Next, we know at what point a method called validate will be triggered. So
call it couponCode, as soon as you change that, everything's going to blow up, right? So you want to be careful of that. Next, we know at what point a method called validate will be triggered. So we have more awareness now of the implementation than we did with this particular approach. Now again, I'm not going to tell you never do one or the other, it just depends on how closely you can bind yourself. Now if you want things to be more readable, and you want to stick with the first approach, don't forget you can always create little helper functions like we reviewed in the last episode. So you could say addCoupon or enterCouponCode. That will accept the code, and then we'll just grab this here like so. So we'll update this, and now if I save, everything should fail, but I can now say enterCouponCode called 50Off. Save it, and now we're back to green. But yeah, you get that readability without having to dig down to see exactly how we're going about it.
Refactoring with computed props15:32
should fail, but I can now say enter a coupon code called 50 off. Save it, and now we're back to green. But yeah, you get that readability without having to dig down to see exactly how we're going about it. So that means we could come up here and do the same thing. Save it, still at green. Now why don't we see if there's any refactoring we can do. Let's hide the sidebar for a minute. So one thing I'm seeing is a lot of sections like this. So this means find the selected coupon code. So look through the coupons array and find me the one where the code equals what the user typed into that input. Maybe we can make that a computed property, like selectedCoupon. Let's see what that might look like. Sounds like we can take this entirely and return it. So now I could take this entire reference here and replace it with selectedCoupon. And if we run it, still at green, so that was a good refactor.
this entirely and return it. So now I could take this entire reference here and replace it with selectedCoupon. And if we run it, still at green, so that was a good refactor. Next you'll see I do the exact same thing here. This.selectedCoupon. Run it again, still at green, and now at this point I think we can inline. Great. Next we have this section here where we're basically going to this coupons array, and then we're creating a new array where each item is equal to the code from each. So we have an array of 50 off and free. So we determine if it's valid by creating that new array and checking to see if it includes what the user typed in. Another way could possibly be, well let's save it, it's going to fail, right? Yeah. Let's try saying, well give me the selectedCoupon using our new computed property, and let's just cast that to a boolean. So if we found a coupon that corresponds
it's going to fail, right? Yeah. Let's try saying, well give me the selectedCoupon using our new computed property, and let's just cast that to a boolean. So if we found a coupon that corresponds to what they typed in, then that should be okay. So if I save that, yeah, we still get green, but now we can clean things up a bit. Now let's go back. Let's take a look at our tests. So we have an acceptedCoupon code, all right? It validates a user provided coupon code. So why don't we say it validates a real user provided coupon code, and then what should happen if it's not real? And you know what? User provided is redundant. Who else would provide it? So now let's enter a coupon code not real, and we'll expect the wrapper to contain a message that says invalidCouponCode. Okay, so let's bring it back. It's failing. So we could say here, well if it's valid, show this. Otherwise, v else, invalidCouponCode.
that says invalidCouponCode. Okay, so let's bring it back. It's failing. So we could say here, well if it's valid, show this. Otherwise, else, invalidCouponCode. So we run that again, and yeah, now we do get green. Now we could get kind of fancy, like we could say if you wanted to put this all in one paragraph, you could say v-text, and then do something like inline. Is it valid? Then spit out couponRedeemed, but yeah, you can see like that would get kind of messy pretty quick. Another option would maybe be to have a computed property called feedback, or really what we had originally is perfectly fine, but just to show you some options and how we can keep refactoring because the test constantly let us know if we've made a mistake. So let's add a feedback computed property, and let's take it one step at a time. So the first one right here, it validates a real coupon code. So we expected
us know if we've made a mistake. So let's add a feedback computed property, and let's take it one step at a time. So the first one right here, it validates a real couponCode. So we expected couponCode redeemed 50% off. So we could say if this valid, then return couponRedeemed, and then the message. All right, let's run it again. Okay, so now we have exactly one failing test. So in this next example, if it's a fake coupon, we expected to see invalidCouponCode. So we could say return invalidCouponCode. Save that, and yeah, now we're back to green. We could even inline this. So we could use this syntax here, save it, still at green. Now the only remaining thing I would do here is you'll see in our spec, we're just assuming that there's a couponCode called 50 off. In real life, like I said, I'll make a note of this. In real life, this wouldn't even exist. But even if it did,
we're just assuming that there's a coupon code called 50 off. In real life, like I said, I'll make a note of this. In real life, this wouldn't even exist. But even if it did, your test can't assert against a real coupon code, because who knows, that may not exist down the line. And as soon as the maintainer changes this maybe to be 10% off, well, all the tests are going to fail. So that doesn't make sense. So what you could do here is override this. So we could say, let's do this. Let's put all of this on its own line. And then we'll say wrapper.setData. And specifically, we're going to just use our own coupon data. And that way, we're not dependent upon this. Okay, so now this can be 50% off, and then we'll never even use this one. So if we run that again, and oh yeah, whoops. Run that again, and now we get green. But now we're using, we're basically providing our own fake data for the purposes of our tests.
