Why Use Prophecy 0:00Now, the reality is, so far in this series, we've primarily been focused on the outside-in, which is a really great way, despite what anyone says, to get into the testing world. But the truth is, as you dig down more to the unit level, you will have needs to, for example, create mock objects and set expectations. So what exactly does that look like? Alright, well let me show you using Prophecy, which is, well, originally it was a mocking framework for PHP spec, but it's standalone, so it was eventually included with PHP unit itself. Okay, let's try it out. Now, I have an empty directory here. Bootstrapping PHPUnit Test 0:35Okay, let's try it out. Now, I have an empty directory here. Let's go ahead and pull in PHP unit, so we can get started. Okay, next, let's go ahead and create a test directory, and within it, well, while we are playing around, we'll call it example-test.php. Okay, well, let's go ahead and open this up and get started. So let's go over to our example test, and I have a little snippet here. Okay, so to get started, we'll say function test something, and now we have a prophesize method directly available if we extend PHP unit framework test case. Pretty cool.method directly available if we extend PHP unit framework test case. Pretty cool. So this instantly gives us access to that mocking framework. Now you might be familiar with a tool like Mockery we've covered at Laracast. That's a really great tool as well. I just end up lately using this a bit more since it's built in and immediately ready to go. Anyways, let's do this one step at a time. We're going to use an example from another series we worked on, but don't worry, nothing is expected of you. Basic Method Expectations 1:34We're going to use an example from another series we worked on, but don't worry, nothing is expected of you. So let's say we have this blade directive class that we would presumably create. Now to start, let's just imagine that you want to assert or expect that a particular method is called. For example, maybe you want to say, well, I need to ensure that a foo method is called at some point. Okay, well, if we want to prophesize, so to speak, about that, then we could say this prophesize, and now we won't pass the object. We'll pass either the class name or, of course, in PHP 5.5, yeah, you can just pass that inprophesize, and now we won't pass the object. We'll pass either the class name or, of course, in PHP 5.5, yeah, you can just pass that in if you want. Either one is fine. Okay, so now we have our prophecy object. I'm going to set an expectation like this. The directive, I'm going to expect a foo method to be called. Okay, let's try this out. Now right now, we don't even have a class, so we know it's going to fail. Let's do PHP unit colors on the test directory.Now right now, we don't even have a class, so we know it's going to fail. Let's do PHP unit colors on the test directory. Okay, so sure enough, yeah, it's trying to call a foo method, but we haven't even defined that. Let's do this inline for the time being. Create a method foo, and if I run that again, here we go. We've changed the error, and we can see some predictions have failed. So it looks like we expected at least one call to a foo method on our object, but no calls were made. Okay, so that's a very simple example of an expectation. Revealing Dummy Objects 3:00calls were made. Okay, so that's a very simple example of an expectation. Now right now, in fact, let's just die and dump the directive. And by the way, of course, we can't use die. We have to do die var dump. Anyways, if we run that, yeah, sure enough, we are getting a object prophecy instance. So how exactly does this work when we are dealing with dependencies, where maybe a dependency pass in or a mock object that we're using in that case, well, it does need to conform to an interface or be of a certain type. Well, we can reveal a simple dummy object simply by saying, well, directive reveal.to an interface or be of a certain type. Well, we can reveal a simple dummy object simply by saying, well, directive reveal. And in fact, let's just die and var dump that again. All right, one more time. And now you'll see the underlying instance is a dummy blade directive instance. Okay, so now that can fulfill any types or interfaces that you need. All right, so now that we know how to fetch our dummy object, well, all we have to do is trigger a foo method on it to make this pass, right? So right here we create a prophecy and we say, well, I expect a foo method. And by the way, notice how you just call the method exactly the way you would expect it.So right here we create a prophecy and we say, well, I expect a foo method. And by the way, notice how you just call the method exactly the way you would expect it. So somewhere I expect on the directive instance, I expect a foo method to be called. That's it. Now, like we said before, we didn't trigger it, so it failed. But now, and remember, this would be in some class somewhere. We're just putting it in the test method for now. Anyways, now that we do call that foo method, it passes. But now what if we really want to expect that an argument is sent through as well? So now we're saying, I expect a foo method with an argument bar to be called.But now what if we really want to expect that an argument is sent through as well? So now we're saying, I expect a foo method with an argument bar to be called. Well now that's different and it'll fail, right? Yeah, so here's what we had, but this in fact is what we wanted. All right, well, we fixed that. Bar. Now we are good to go. Okay, but what about when we need return values? Well, you could always do this. We'll return foo bar. Arguments and Return Values 5:09Well, you could always do this. We'll return foo bar. And now we can accept the response there and do an assertion. Assert equals foo bar and compare that against the response. So now we're saying, we expect a method foo to be called with an argument of bar. And that method, when it is called, will return the string foo bar. All right, so now we do in fact trigger that method and we save the response. And finally, we perform an assertion. So this should return green as well. Cool, right? Real-World Dependency Mocking 5:38So this should return green as well. Cool, right? So now to make this a bit more real world, why don't we modify this example? Because of course, you're never going to set an expectation and then fulfill that directly within the test method. Of course not. That'll be done in a class somewhere. So continuing with this example from the Russian doll caching series, imagine this. Well, we're going to have our Russian cache class, something like that. And we're going to have our directive that has a dependency of that object.Well, we're going to have our Russian cache class, something like that. And we're going to have our directive that has a dependency of that object. So something like this, new blade directive that accepts the dependency. And now, I'll get rid of all this stuff. What I really want to say is, at some point, a method on this object should be called and it should receive the appropriate value. For instance, if we call a, I don't know, a setup method on this instance, that's going to receive a key, and that'll be like our cache key, right? So cache key. Well, maybe we've decided that, in fact,So cache key. Well, maybe we've decided that, in fact, you could pass through a number of things. So if you pass this through, that will be the key for the cache that you want to use. Or maybe if you give it a collection, well, then maybe we have a way to generate a cache key from the collection. Or further, maybe if you pass through a model instance instead, it's going to look for a custom method on that object and trigger that and return the value.it's going to look for a custom method on that object and trigger that and return the value. Either way, we want a way to normalize a cache key. And any of these should be allowed according to the API that we want to have. Okay, so in order to test this, yeah, we could use Prophecy, a perfect use case, like this. Well, I know I'm going to perform expectations on this Russian cache dependency, right? So I'm going to make sure that I set a prophecy for that. Prophesize, and now, once again,So I'm going to make sure that I set a prophecy for that. Prophesize, and now, once again, I'm going to pass in a string representation of the class. And by the way, for now, I'm going to get rid of that dummy stub there. And let's go ahead and comment these two other examples out. So our expectation will be, we're going to ask the cache if it has the given key at some point. So I will hard code cache key, all right? Does that make sense? So we are testing this blade directive class that has a dependency ofDoes that make sense? So we are testing this blade directive class that has a dependency of this Russian cache class. Now we're saying, well, when we call this setup method, we're going to pass through this cache key. And as part of the responsibility of that method, we do want to make sure that the cache dependency checks to see if we have anything in the cache with that specific key. Okay, so let's comment that out and try it out. We know it's going to fail, right?Okay, so let's comment that out and try it out. We know it's going to fail, right? All right, class blade directive not found, fair enough. So let's create source blade directive, and I will just do a little macro here. Okay, namespace app, or in fact, let's not use any namespace at all. Okay, so while we're here, let's set up PSR for autoloading. You've seen me do this many times, so I'll go pretty quick. We're going to have no namespace whatsoever, so that will be located within the source directory. And finally, I will composer dump autoload.that will be located within the source directory. And finally, I will composer dump autoload. Okay, one more time, and we have a new error, no has method setup. And in fact, we don't even have a, if I go back, we haven't even set up this Russian cache class yet either. So source, Russian cache, and again, no namespace. Okay, so I'm going to set our method has. Yeah, if you want, this could be an interface. Some people prefer that. You don't bother with this class.Some people prefer that. You don't bother with this class. You just create the contract while you're testing something else. You're going to create it at some point anyways. That's sort of a workflow thing. You're fine either way. Okay, so now if we come back, the blade directive class has a dependency of this Russian cache class, right? And let me assign that. Okay, so I will run this again.And let me assign that. Okay, so I will run this again. And now we're on to the next error. And notice, this is exactly what we were talking about earlier. So we're expecting an instance of Russian cache, but we got that object prophecy object instead. And it's failing because I specifically want this type. Okay, so if we go back, the problem is we are trying to pass in the object prophecy instance as a dependency when we actually want to reveal the dummy object behind the scenes.prophecy instance as a dependency when we actually want to reveal the dummy object behind the scenes. Okay, so let's run that again. Scroll up, and there we go. We changed the error once again. All right, so no method setup. Fair enough. All right, so create our method called setup, and we know that will accept our cache key, right? So let's see, and by the way,we know that will accept our cache key, right? So let's see, and by the way, that's squawking only because we haven't used the variable yet. Okay, so create a prophecy for this dependency. We're gonna reveal the dummy object behind it and pass that through to our blade directive. And then we're gonna say, for that dependency, I expect a method called has with this argument to be called, should be called. So let's try it out. It fails.So let's try it out. It fails. Okay, so we expected one call to a has method with that string, but no calls were made. Obviously, that's because we called our setup method. And if we switch over there, yep, we didn't do anything. We did not call that method. So let's defer. This cache has, and then pass in the key. And that should return us to green.This cache has, and then pass in the key. And that should return us to green. Run it, and there we go. So here's where we can ensure that we normalize this cache key properly, like this. How about it normalizes a string for the cache key. And now I'm going to duplicate this, and we can dry it up later if we need to. I don't worry about test duplication too much until I really start to feel it. So maybe on the third copy, where I see this done over and over, that's where I might extract a method.So maybe on the third copy, where I see this done over and over, that's where I might extract a method. But yeah, once again, that's kind of a preference thing. So now we're going to say it normalizes a cacheable model. And maybe that's a class that has a specific method for fetching the cache key. So now we'll say, we're not gonna give it a string. We're actually going to give it a model. Or some kind of object. So let's do this.Or some kind of object. So let's do this. Why don't we new up a model stub, or something like that. And we can even create that right here. Model stub. And maybe the idea is, if this has a method, or it implements a contract, or pulls in a trait, that has a method called getCacheKey. And we'll just return stub cache key. Well, our directive should be smart enough to check for that, and then call this method if it exists.Well, our directive should be smart enough to check for that, and then call this method if it exists. So if we now pass through our model, essentially, to the setup method, well, we expect, behind the scenes, after we normalize the key, a stub cache key method to be called. All right, once again, we know this won't work, right? We've run it, and now we expected this, but in fact, we passed the model stub instance to the has method. Okay, well, let's come back right here. And now it looks like we have to normalize the key, something like this.Okay, well, let's come back right here. And now it looks like we have to normalize the key, something like this. Or in fact, we can inline that entirely. All right, let's create that, normalize key. And that will accept the key here. Okay, so now we can just do a very quick check. For example, if isObjectKey, and the key implements a contract or something like that, I'm just going to do method exists, key, and getCacheKey. Well, yeah, if that's the case, then the user is giving usI'm just going to do method exists, key, and getCacheKey. Well, yeah, if that's the case, then the user is giving us a object that knows how to generate a cache key itself. So in that case, we will return key, getCacheKey. And now we're seeing, we might want to change that name entirely to something like item. Now, if it's not an object, well, we can just assume that it's a string, and we can just return it completely. All right, so I think that'll return us to green, and it does. So does that make sense?All right, so I think that'll return us to green, and it does. So does that make sense? We are now normalizing this key, and this is really useful when you want to accept a variety of values. Where you want to say, well, yeah, you can give us a string, or if you give us something else, like an array, then we will do this. Or if you give us, in this case, any kind of object that has a method called getCacheKey, then we'll call that, and we'll let you determine what that value should be. And I usually use the term normalize for that.and we'll let you determine what that value should be. And I usually use the term normalize for that. But now notice how with our test, yeah, we don't really have to be concerned with the Russian cache class at all. And that's why I was saying earlier, if you wanted this just to be a contract, an interface, because we're not even worried about this right now, we'll worry about that later. And we'll worry about that maybe with our integration testing. But right now, yeah, if you wanted this to be an interface, and you didn't want to create the class at all, that would be fine.But right now, yeah, if you wanted this to be an interface, and you didn't want to create the class at all, that would be fine. Lots of people prefer that approach. Okay, so let's do maybe one last one. So we're figuring out how to normalize a cache key, and then pass that through to our Russian cache class. So we know that we can pass through a string. We know that we can pass through any kind of object that implements the proper method. Maybe we can also do this with a collection, or maybe an array,that implements the proper method. Maybe we can also do this with a collection, or maybe an array, if that makes sense, right? So it normalizes an array for the cache key. Okay, so now we're just gonna pass through foo and bar. And maybe if you do that, we've decided, I don't know, we will implode it and then generate an MD5 or something. I don't know, we don't have this in the real project. But as an example, that's fine. Okay, well, let's make sure we test for that.But as an example, that's fine. Okay, well, let's make sure we test for that. You know what, why don't we save this to item instead, something like this. Now, if our rule is that the generated cache key in the case of an array is maybe an MD5 version of the imploded contents. Well, in this case, we can just verify that like so, MD5 foo bar. Okay, so implode the array, that will end up becoming foo bar, and then get the MD5, and that should be the cache key that we generate ultimately. All right, let's run it, and let me fix that. Okay, run it, we know it's gonna fail, right?All right, let's run it, and let me fix that. Okay, run it, we know it's gonna fail, right? We expected this, but no, we passed the array through. Okay, so we come back. Now we're gonna say if is array item, and yeah, this is useful. It allows us to be as dynamic as possible, which is nice, especially for certain types of APIs. Like if you think of something like the mockery library, if you've ever used that, they do the exact same thing, where the arguments you pass through to mockery can be one of many,if you've ever used that, they do the exact same thing, where the arguments you pass through to mockery can be one of many, many different things. And then it just tries to figure out what it is you really wanna do there. Okay, so if you pass through an array, yeah, we're going to implode the array. So that means if you have one, two, and we just call implode on it, that'll give you the string one, two. And then finally, let's get rid of that. We will do an MD5 to generate a unique key. Okay, let's run that, and we get green, so now that works.We will do an MD5 to generate a unique key. Okay, let's run that, and we get green, so now that works. So yeah, very simple stuff once you break it down. When you originally see prophesize and then prophesize reveal, yeah, it can be confusing, but if you really break it down, it kind of makes good sense. If we are creating a prophecy for some kind of object and the behavior it has, then we call a method prophesize, and you pass through the class that the prophecy is related to. Next, if this class ends up being a dependency, well, then we will reveal the underlying dependency andNext, if this class ends up being a dependency, well, then we will reveal the underlying dependency and pass it through wherever we need to. Finally, to perform any kind of expectations, yeah, we just use a readable method like this, shouldBeCalled. And by the way, in this case, it turns out we can inline that because we never even referenced that. Okay, so yeah, I think that gets the basic point across. Now remember, if it makes sense and when you can, I always favor using real collaborators.Now remember, if it makes sense and when you can, I always favor using real collaborators. It makes things easier. It makes it read more like documentation, which is incredibly important. But it's true, as you get to the unit level, you probably will have situations where you want to ensure that certain commands, quote unquote, are sent. And when that's the case, yeah, prophecy is fantastic for that. You can create a prophecy, set an expectation, or if you just want a dummy that does nothing, you can do that as well.You can create a prophecy, set an expectation, or if you just want a dummy that does nothing, you can do that as well. It's actually pretty flexible. So have a play around with it, and maybe you'll find a place for it in your workflow.