Defining Achievement Goal0:00
In the last episode, we began by deciding we want an achievement system for our site. But as we discussed, it's often very tricky, because you don't even know where to begin. It's difficult to know, where do I start? And that's why, specifically, we create a new test and we provide one example, our first example. Almost like you were talking to somebody. If you said, yeah, I want badges on the site. And they responded with, well, what do you mean by that? You could then say, well, for example, an achievement badge is unlocked once a User's experience points pass 1,000.
You could then say, well, for example, an achievement badge is unlocked once a user's experiencePoints pass 1,000. That's a perfect example for how the system should behave. So we create a test for that, we make the world, we put things into motion, and then we perform an expectation. But at the moment, of course, it's failing, and that's okay. At the very least, we know the end goal. So at this point, we're going to drop down a level and begin the implementation process. Already, I can see that we need some way to hook into when a user's experiencePoints are earned.
Switching to Lower-Level Tests1:32
So here's what I'm going to do. I'm going to put this test on hold, and trust me, that's entirely okay. We have our full outside-in test, and to get that to green, we're going to need to drop down a level. And as part of dropping down that level, we're going to write some more tests. So I'm going to switch over to my ExperienceTest class. This is for code that I've already written, basic stuff. A User earns experience when they complete lessons. So we put things into motion, sign a User in, have them complete a video, and make sure that as a result of that, they have earned experience.
Adding Experience Event Test1:54
So we put things into motion, sign a User in, have them complete a video, and make sure that as a result of that, they have earned experience. You get the basic idea. I'm going to add another one here. An announcement or event. I often like to say announcement because it makes it more clear to my eyes. We're making an announcement to our system that such and such thing took place, but whatever you want. An announcement is made when experience is earned. All right, so how would I put that into motion?
An announcement is made when experience is earned. All right, so how would I put that into motion? Well, to start with Laravel, we can say the event facade. Let's find that right there. We're going to fake that because I don't actually want to fire an event. Instead, I want to make sure that we dispatch an event. And remember, if you forget the API, that's okay. I forget all the time. You can just look for something like Laravel event fake, and then you can figure out what you need to do there.
You can just look for something like Laravel event fake, and then you can figure out what you need to do there. In this case, I do believe it is assertDispatched. Yeah. So here we can say, well, this is a basic example. I expect this event called OrderShipped to be dispatched. So why don't we just grab that right there, put things into motion, and then our assertion phase will be this. Assert that the given event was dispatched. And I don't know what the event is.
Assert that the given event was dispatched. And I don't know what the event is. So this is a good point to think, well, what would be a good name? Well, how about something as simple as UserEarnedExperience? And do note, we have not created that yet. So that will be one of the things to do. All right. So how do I put things into motion? Well, why don't we just take some of this right here? Sign a User in, and then have that User complete some random lesson that we quickly whip up.
Creating Event Class3:23
Well, why don't we just take some of this right here? Sign a User in, and then have that User complete some random lesson that we quickly whip up. All right. I'm going to give this a run, and of course, it will fail. This event was never dispatched. That's our next step. php artisan make:event UserEarnedExperience. Okay. So now if we take a look at that, here we go. In this case, I'm not broadcasting any kind of event over WebSockets.
So now if we take a look at that, here we go. In this case, I'm not broadcasting any kind of event over WebSockets. So I can get rid of that. Let's reformat, and I can get rid of that. Let's keep it nice and simple. Okay. So within the constructor, what kind of data would you need? Well, technically, all we really need is the User, but it might be nice to also provide the experience as well. All right.
the experience as well. All right. So let's go ahead and add these, the User, and then another one for the experience. All right. I can put these down here, and they should be public. Okay. So now let's come back and import that right up here at the top, and I'm going to come back and give this another run. All right. It's still failing, of course, because at no point do we fire that event.
Dispatching Event on Award4:31
All right. It's still failing, of course, because at no point do we fire that event. Let's now switch over to my Experience class. Yeah. For this, just come along for the ride. I know you're not familiar with this code base, but you'll see it pretty quick. Here is where we apply experience, and this is a good example of... Even though all I'm doing is upgrading or incrementing a column on a table, I could do that anywhere. I could do that from the controller.
do that anywhere. I could do that from the controller. It's actually a good thing that I wrapped this up, because now I have a single point of entry where we award a User experience. And now as a result, if I do want to fire this event, like I'm going to do, well, great. It will be affected everywhere in my system where I call this method, whereas if I didn't wrap it in a method and I just did something like experience.incrementPoints, well, yeah. If I wanted to fire an event when that took place, it's suddenly a lot harder, right? Let's bring it back, and now we're going to fire this event. So now we'll say user earned experience and dispatch a new event.
Let's bring it back, and now we're going to fire this event. So now we'll say user earned experience and dispatch a new event. Okay. Now, a quick note. If you're not familiar with this, I find a lot of people aren't used to that. They're used to this approach, event, new UserEarnedExperience. So a quick little tip here. If we switch over to the event class, you'll see that out of the box, they include this trait called dispatchable. And if you take a look at that, you'll see they have two static methods.
trait called Dispatchable. And if you take a look at that, you'll see they have two static methods. Here's the one we care about, dispatch. So if we call a static method called dispatch on an event class, it's just going to do the same thing that possibly you're used to doing yourself, which means you have two different ways to fire an event. And neither is better than the other. It's whatever makes you feel most comfortable. So that means I could do this, or I could say event(new UserEarnedExperience), exactly the same thing.
So that means I could do this, or I could say event new User earnedExperience, exactly the same thing. Okay. So I want to run my test, but real quick, when we dispatch the event, we do expect the User and the experience. So why don't we provide, you can see I have access to the associated User, so this User, and the number of points would be, well, it's not this, what you see here, because that would be the points to increment. So instead, it would be the resulting points after we increment it. Okay.
So instead, it would be the resulting points after we increment it. Okay. But anyways, I think if I give this a run, yeah, it's still going to fail. And that's confusing. We wouldn't expect that. We have the User complete a lesson. At some point in the code, we call this method. It sounds like we're never hitting this method. And in fact, if I say die here, you know, I often do this just to see, are we touching this method?
Fixing Event Faking7:01
And in fact, if I say die here, you know, I often do this just to see, are we touching this method? No, we're not. So what's the issue here? Well, the issue is when a User, let's come back, when a User completes a lesson, well, we award the User experience as a side effect. So that itself is listening for when a User completes a lesson. So that means we are relying on an event listener to apply the experience, but we faked it. So at no point did an event listener pick up and call this method, right? So in these situations, when you don't want to fake all events, you just want certain
So at no point did an event listener pick up and call this method, right? So in these situations, when you don't want to fake all events, you just want certain events, you can do it here. So I'm only going to fake user earned experience. Okay, now if I come back, any other event we fire will run through its course except for that one. And we do get green now. However, one thing to keep in mind, if I were to just say foo and bar here, it's still going to return green, which means we're not doing any real checking about the data associated with that event.
Validating Event Payload7:56
to return green, which means we're not doing any real checking about the data associated with that event. So if you want, this is where you can pass through a closure to further verify the event that you expect to be dispatched. So whatever you return here, you know, if I were to say return false, then there's no way this test is going to pass. It fails. If I were to return true, well, then as long as we fire this event, because we return true here in our checker, then that's good too. So we get green, right?
here in our checker, then that's good too. So we get green, right? Alright, so our comparison will just be, well, to begin, the event that refers to this class. So let's make sure the User matches up. That's the main thing we care about here. So we will return that the eventUser is equal to our authenticatedUser. But yeah, we can't quite do that either. So it's going to fail. People often do this. Nope, it's not matching up because eventUser is a little bit different from this.
People often do this. Nope, it's not matching up because eventUser is a little bit different from this. So rather than checking for equality in this way, we're going to see if the authenticatedUser is the eventUser. It's a small difference, but now we're not checking to see if those objects are identical or equal. Okay, so now if we run it, it is failing. Call the member function getKey. So I'm assuming because user in this case is foo, it's trying to call getKey on that. So let's bring this back to what we had before.
So I'm assuming because user in this case is foo, it's trying to call getKey on that. So let's bring this back to what we had before. So we'll give it another run, and we do get green. Excellent. Now, oh, actually, you know what? We made, well, maybe it's not a mistake. So here, when we are dispatching that a user earned experience, we have to decide, do we want to include the points you earned? So in this case, did you earn 100 points? Or do we want to fire the event with the total experience points for the user?
Expanding Event Data9:38
So in this case, did you earn 100 points? Or do we want to fire the event with the total experience points for the User? Now all of this is accessible off of the User, so I'm not too worried about it either way. But again, these are things to think about. And maybe you want to do both. So maybe you fire the User, the points they earned, as well as the total sum of points that the User has. That would be an option as well. Why don't we take that approach? So real quick, we'll say experience, how about, let's update this to points, and then maybe
Why don't we take that approach? So real quick, we'll say experience, how about, let's update this to points, and then maybe totalExperience. But you know what? I don't like that this is called points. And this is points as well, but we don't include that word. So how about totalPoints, or totalExperiencePoint, anything you want there, okay? So now if we come back, we need to make sure that we send through these three arguments, and we also want to make sure that we verify them. So if I come back at the moment, we're just checking if the user matches up.
and we also want to make sure that we verify them. So if I come back at the moment, we're just checking if the user matches up. Let's also ensure that the points equals the number of points you would earn for completing a lesson. That happens to be 100. I'm going to hard code that as a magic number for just a second, and then we're going to talk a little more about why in this test we even have a user completing a lesson. We'll talk about that in a minute. And then finally, the totalExperiencePoints should also equal 100, because you begin at zero.
Simplifying Test Setup11:52
to a video being completed. And especially maybe somebody that joins the team in six months, that might be a little confusing to them, because they might just be thinking, well, we're not interested in testing that completing a lesson applies experience. We already have a test that looks like right there that checks that. So instead, why don't we just award experience? Let's just call that method directly, and let's see how that changes our test. It'll make it a lot easier. So we'll say right here, this user, get their experience, and that will give me an instance of this, and then we will award experience.
So we'll say right here, this User, get their experience, and that will give me an instance of this, and then we will award experience. Now I can fall back to a magic number, because it's not connected to some specific action on the site. So I can get rid of that entirely. Let's run the test. We're still going to get green, but now we can take it a step further. Before we were having to fake a specific event, because other events were being fired. And so yeah, we started to see, well, we're testing the entire system here, because all these other unrelated event listeners are being picked up just to handle the situation.
