تماشای این درس نیاز به اشتراک حرفه‌ای دارد.

Defining Unit Test Scope0:00

You would be forgiven for thinking that a unit test may only act upon a single class. One unit test, one class under test. But really, that's not the case. You don't have to think of it like some law or rule from above that's passed down that you have to honor. In some cases, a unit test might touch two or three different classes that coordinate with one another. That's entirely fine. And in fact, a unit test could even touch a database, contrary to what some might say. Again, these terms can be a little bit blurry.

Starting Quiz Test0:26

And in fact, a unit test could even touch a database, contrary to what some might say. Again, these terms can be a little bit blurry. But anyhow, let's say we are testing, in this case, a Quiz. There's some good domain material here. A Quiz has Questions. It could be graded. A Quiz can be taken. It can be reviewed. Let's start with a simple one. It consists of Questions.

Oh, it consists of questions, rather than test. It consists of questions. But both are entirely fine. And in fact, you might even want to put these on their own line, like so. Anyways, let's say, well, given I have a Quiz, and maybe when I add questions to the Quiz. So let's add a Question. And maybe this is its own class as well. It'll be very simple. How about what is 2 plus 2?

It'll be very simple. How about what is 2 plus 2? And the answer is 4. Finally, my assertion will be, if I were to fetch the number of questions for the Quiz, that should return 1. And I'll reformat. All right, we have our first test. Let's give it a run. Okay, it fails because there is no Quiz class. So that's our next step.

Implementing Quiz and Question1:52

Okay, it fails because there is no Quiz class. So that's our next step. In my source directory, we'll add one now, Quiz. And then we'll come back and properly import this. Okay, run it again. Now we can see those squiggly lines, so we know it's not going to work. Call to undefined method, addQuestion. All right, there's step two. Add a method, addQuestion. All right, run it again.

Add a method, add Question. All right, run it again. Class Question not found. Okay, let's create it. Again, notice it's kind of like a game, which I like. If there's an option to gamify what I'm working on, I will always take it. All right, come on back, and now we import Question. Run it again. Okay, call to undefined method, quizQuestions. All right, let's extend the API.

Okay, call to undefined method, quizQuestions. All right, let's extend the API. Run it again. What do we do next? Okay, invalid argument exception. Argument two must be countable. Okay, so here's the issue. Right now, we are trying to count using something like this, whatever is returned from quizQuestions. But of course, null is going to be returned, and you can't call count on null.

whatever is returned from quiz questions. But of course, null is going to be returned, and you can't call count on null. So let's make this pass. When you add a question, maybe to start, we will store it here. So maybe we'll have an array of questions, and then maybe I can push to it. quizQuestions, push, question. Now, when I fetch the questions, that's simply a getter. Okay, give it a run, and it passes. So we're not going to flesh this out too much, but again, the point is, notice I'm writing a unit test that consists of multiple classes.

Adding Quiz Grading3:27

So we're not going to flesh this out too much, but again, the point is, notice I'm writing a unit test that consists of multiple classes. Don't feel bad about that. You're not breaking any rules, and try not to think of them as rules in the first place. Instead, think of them as general guidelines. Let's do one more just for fun, and then we'll wrap up. How about a Quiz can be graded? So again, I could say test, it can be graded. Or yet again, if you prefer to do it as a doc block, this will work as well.

It's not a problem. So I always refactor toward a setup method or data providers. But I don't usually start there. Anyways, given I have a Quiz with a Question, if I were to, let's think about it, what API do we want? We need to take the Quiz, of course, and then I need to grade the Quiz. So obviously, that's going to take the shape of something like this. But what about taking the Quiz? How would that work? Let's see, hm, let's say, let's grab the first Question.

So again, notice how the test is almost like a scratch file of sorts. It gives me a way to just play around and figure out how do I want to interact with this quiz? What method should it be called? How do I provide an answer? How do I grade it? All of these things are being fleshed out before the actual code has been written. So let's give this a run, hide the sidebar. Let's see, call the undefined method quizNextQuestion. All right, let's get going.

Let's see, call the undefined method quizNextQuestion. All right, let's get going. Quiz, and then let's say nextQuestion. Let's run the test again. So often you'll notice, even when I sort of know what the next step is, I will still run that test just to get into a flow. And this is, for me, a huge benefit to test-driven development. It allows me to get into a flow or into a rhythm where I write a little code, I run the test. I write a little code, I run the test.

into a rhythm where I write a little code, I run the test. I write a little code, I run the test. And when that test returns green, I move on to the next thing. So it's a nice little flow or rhythm to get into. Anyways, call to a member function answerOnNull. So if I come on back, at this point, I'm trying to answer the question, but null was returned here. Okay, let's say, hm, to start, we know this won't be quite right. But if I were to grab the questions, or I can just go directly, and then grab the first item there.

But if I were to grab the questions, or I can just go directly, and then grab the first item there. But later, we should probably track which question the user is currently on. But this should get us started. So now it's probably going to say, yeah, there is no answer method on the Question class. Okay, so notice, we are writing tests for quiz. I'm not writing tests, I don't have a QuestionTest class. You may decide at some point it needs its own set, but often that can be wrapped up into your current unit test.

You may decide at some point it needs its own set, but often that can be wrapped up into your current unit test. As with everything, this all depends. So we're going to have an answer, like so. I'm not sure what we're doing here yet, so I let the test guide me. Call to an undefined method, grade, all right? Let's go on back, and let's grade it, how about here? Run it again. Okay, failed asserting that null matches 100. Now, we have this concept of sliming.

that is the least amount of code to make both of those tests pass. So in this case, we know we don't want a hard code 100, and yet it'll solve the test, great. Let's go back to quizTest, and keep going, because maybe there will be situations where that was enough to make your project work. And if you can get away with hard coding it, or a simple ternary or something, then keep it. And actually on this node, let's change this test now. It's, because really what we're doing here is grading the quiz. So it grades a perfect quiz, perfect as in all the answers are correct.

It's, because really what we're doing here is grading the quiz. So it grades a perfect quiz, perfect as in all the answers are correct. But then let's do another one, and we'll say it grades a quiz or a failed quiz. So we're going to get all the questions wrong. So if I go here and I say 2, although sometimes I try not to do magic numbers like that, even though this would be fine, I might come back and think, well, why not 3 or 1 or 5 or 500? So sometimes when you just want to give yourself a hint that you intend to provide a gibberish answer, maybe just even do something like that. Give it a string, something like that.

All right, let's fix it. So what is the simplest thing I can do here to make the test pass? Well, let's see. Let's go into Question. I don't have anything to look off of, so we're doing this together. When you answer a question, well, here's one thing. We have set up a Question with no constructor. And we haven't had to use it yet, but now we do. So let's have a constructor that accepts the description. Or maybe the body of the question.

So let's have a constructor that accepts the description. Or maybe the body of the question. And then another one for the answer, no, solution. And of course, if you're using PHP 8, you can do this all inline as part of the constructor and get rid of that. But I'm on 7.4 here. Anyways, here, let's see. We could return whether or not the provided answer. This is maybe a little naive, but we could start with this. Just check if the given answer is equal to the solution.

This is maybe a little naive, but we could start with this. Just check if the given answer is equal to the solution. And this is good, but I think I still need to track the result of this on the instance itself. So maybe, hmm, should we have a correct property or just the answer itself, or maybe both? I'm not sure, we're figuring this all out together. So if we were to take that approach, when you provide an answer, we begin by storing it. And then we also track whether or not it was correct, like so.

when you provide an answer, we begin by storing it. And then we also track whether or not it was correct, like so. So return a Boolean, but also track that. And yeah, maybe we could go with that. Let me know if you have a better idea. So anyways, we could come back to quiz, and now when we grade it, let's just iterate through all of the questions and check. So I could say, let's do array_filter, at least as a start. We're going to filter the questions down, like so. And basically, this is going to return a new array consisting exclusively.

We're going to filter the questions down, like so. And basically, this is going to return a new array consisting exclusively of the questions that are correct. Let's go back. I could make that public, or we could have a method here. So again, notice this is some part of the unit test, because it will be, I can add one later, but this is going to be wrapped up in the logic of the current test we're trying to get to pass. So check if the question is correct. And sometimes if I don't know, I just play around.

of a total of four questions, of course, we get 0.25, 1/4. Let's now multiply that by 100 to make it a full round number. 25 would be your grade in this case. So now I could say total would be the number of questions we have. So now let's do our math. And actually, so far, we may just need the total number of correct. So we would say return the correct answers divided by the total, and then multiply that by 100 and reformat there. Now we may have to round things later, but I hope that's enough to make our test pass, and it is.

Now we may have to round things later, but I hope that's enough to make our test pass, and it is. Great. So we wrote a lot of code there, and that's fine. We wrote code trying to get the test back to green. So now, before we move on, we could take some time to refactor. And I'm throwing a bunch at you here, but it's all kind of jumbled up. It's all connected. This is part of the red-green-refactor cycle. This is related to test-driven development.

Red-Green-Refactor Cycle13:56

This is part of the red-green refactor cycle. This is related to test-driven development. You start with red. You write some code. You get yourself to green. And then, before you move on to the next test, take a little time, now that you're at green, to figure out if there are ways to improve the code. And once you're happy, then you move on to the next test. For example, we might decide we don't need this temporary variable.

And once you're happy, then you move on to the next test. For example, we might decide we don't need this temporary variable. So let's just do it inline. Make a change, run the test again, and I'm pretty sure it's going to pass, and it does. Next, this right here, maybe that should be a method. We could say, correctlyAnsweredQuestions. We'll make that protected. Put it in here, return, and reformat. OK, so now, this could be updated, giving

OK, so now, this could be updated, giving the count of the correctly answered questions. All right, we made a change. We run it again. Still green, so that was the safe refactor. Now, if red was returned, in that case, figure out the problem or just hold down Command-Z or Control-Z to bring it back to what you had earlier, or a git reset. Now, again, we're not going to take this too far,

to bring it back to what you had earlier, or a git reset. Now, again, we're not going to take this too far, but maybe you want a check. If you call grade and all of the questions have not been answered, maybe an exception should be thrown. Maybe that's a test you could write. You can't grade this quiz until it is complete. All of these edge cases can be represented in a test. Next, just a little thing, but I can bring this to a short function or an error function and reformat,

Next, just a little thing, but I can bring this to a short function or an error function and reformat, and that should still work. Yeah. Now, again, part of the fun of the refactoring cycle is it allows you to take a look at things like, do we like is correct? Is this question correct? Is that enough to make it clear? Was it solved?

Homework: Edge Cases16:25

Come on back to quiz test. And we've begun fleshing out what it means to have a Quiz in our application. Well, a Quiz consists of Questions. It can grade a Quiz with a perfect score. And it can also grade a failed Quiz. So now, if you like, I'll leave you with a couple to do on your own. How about it, and let's add our doc block, cannot be graded until all Questions have been answered.

How about it, and let's add our doc block, cannot be graded until all questions have been answered. And I'll let you figure that out. And then finally, one more, it correctly tracks the next question in the queue for the User to take. So here, we said next question, but we are hard coding it, as you see there. So your job, and we'll go over this together in the next lesson, your job is to change this so that we track, well, what is the next question for the User to take?

in the next lesson, your job is to change this so that we track, well, what is the next question for the User to take? If I call this method three times, we need to make sure that it doesn't return the first item every single time. So that's two tests to work on as homework. And we'll review the answers in the next episode.

دوست دارید گاهی خبرهای Laracasts را ایمیل کنیم؟