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

New Project Setup0:33

But at this point, we haven't yet played it, we've just read it. So as a learning exercise, let's create a brand new project and figure out if from scratch we can implement the same sort of workflow. Not to release or deploy, it's just to make sure that we've committed a lot of these ideas to memory. Okay, let's get started. So in my web folder, I'll create a new, we'll call it LayerCastDocs, and this will be running on Laravel 5.6. All right, and we will cd in there. So I'd like to write some tests together, just to get us into the right workflow.

Test Default Version Redirect1:00

All right, and we will cd in there. So I'd like to write some tests together, just to get us into the right workflow. Let's start with this example test, and I will rename it to DocumentationTest. All right, so if we give this file a run, we of course get green. All right, let's write our first test. To start, what about that feature where it will assume the latest version? So if you make a GET request to docs/ and then a documentation page, well, it will assume the latest version. So that would actually perform a redirect to docs/5.6/auth. Let's do a test for that.

So that would actually perform a redirect to docs/5.6/auth. Let's do a test for that. It assumes the latest documentation version. So here, this is a feature test, so we'll go from the outside in. If we visit docs/some-page, well, I will assert that we are redirected to docs/default-version/some-page. All right, we have our first test. So I'll give this a run, and of course it fails because default-version doesn't yet exist. You can declare this anywhere you want.

exist. You can declare this anywhere you want. In the case of the LayerFile documentation, as we learned, it was just at the top, and that's fine. So we could say, if not defined DEFAULT_VERSION, then define it, and we'll set that to how about 1.1. Let's give it another run. It fails. So we got a 404 even though we expected a redirect, and that's because we don't have any route for that.

So we got a 404 even though we expected a redirect, and that's because we don't have any route for that. So down here, we'll add a new one. If you visit docs/{version}/{page?}, now, you'll remember, we decided the page will be optional, so we can use a question mark there. Okay, that will, actually, let's do a controller here. That will be our DocumentationController at show. All right, let's run it again. Now it's going to fail. In this case, we get a 500 status code.

Now it's going to fail. In this case, we get a 500 status code. Now in these cases where you get a 500 status code, but you don't necessarily know what it refers to, you can always, even temporarily, just disable exception handling, and that way you'll see the exact exception that was thrown. So if we run it again, now we can see, of course, DocumentationController does not exist. So let's make that and give it another run. All right, this time, there's no show method, all right? Run it again.

All right, this time, there's no show method, all right? Run it again. Now they're saying, okay, well, a basic 200 status code was returned, but that's not a redirect, so that's our next step. Let's see. When you show documentation, we're going to accept the version and an optional page. It sounds like our first step is to check, well, is the version valid? So we could say, to start, we can refactor this in a minute, move it to maybe a Documentation repository, but we could say, well, if the version here is in an array of our supported versions, and let's say 1.0, 1.1, and maybe that's it.

Feature Test Markdown Rendering5:03

All right. So let's give it a run, see how we're doing, and we get green. Excellent. So let's come back and do our main test here. It loads and parses a markdown documentation page. That's basically what we're doing here, right? All right. So we might want to write this in two layers. We might want a lower level unit test, and if that's the case, we'll create it. But I still want a higher level feature test for this.

We might want a lower level unit test, and if that's the case, we'll create it. But I still want a higher level feature test for this. So let's see what we need to do. We're going to say, let's imagine we're going to go to docs and once again the default version, and then some kind of stub. This will be the equivalent of a documentation page. Now when we visit that page, I'm going to assert that I see the compiled HTML for this stub. So with that in mind, let's create a directory called tests, helpers, stubs, and we'll call it stub.md.

So with that in mind, let's create a directory called tests, helpers, stubs, and we'll call it stub.md. Okay. So this will be our fake documentation file. So we'll have an h1, and then here is the documentation stub, and let's keep it like that. All right. So now when we compile this down, well, when we load the page and when we get that page, this should be an h1, this should be a paragraph. So let's do this.

this should be an h1, this should be a paragraph. So let's do this. Assert h1, and also assert c within paragraph tags, that sentence. All right. So it loads and parses a markdown documentation page. We verify that by visiting a documentation page, and we expect to see the compiled HTML. All right. Let's give that a run, and it fails. So failed asserting that nothing was returned there, but we expected it to contain an h1 there.

So failed asserting that nothing was returned there, but we expected it to contain an h1 there. So let's get to it. We'll come back to DocumentationController, but we're still going to do some refactoring here. I think information like this would be better stored on the dedicated Documentation class, but we'll get there in just a moment. Let's say we want to fetch the Documentation. Now we need to think, should Documentation be an Eloquent model? No, not really.

Now we need to think, should documentation be an Eloquent model? No, not really. We're not storing anything in the database, so this can be a simple class. So we could either make this as a real-time facade, or we could just new up Documentation and call a get method, anything you want there. So let's think, what should happen when we call this method? Well, we would need to give it the version as well as the page that we want to fetch. And then behind the scenes, well, think, it's going to look for a markdown file that matches that. Once that file exists, it's going to fetch the file, compile it down, and then return.

Unit Test Documentation Class7:37

that. Once that file exists, it's going to fetch the file, compile it down, and then return the result. So at this point, we have a good use case for dropping down a level. I'm going to test the Documentation class itself. So let's do that now. php artisan make:test, I'm going to call it DocumentationTest again, and I might rename the current one. A quick note, you can add the --unit flag, and that's your way of saying, okay, it's not a feature test, it's a unit test, as you see here.

A quick note, you can add the unit flag, and that's your way of saying, okay, it's not a feature test, it's a unit test, as you see here. Okay. Generally, I don't like the exact same name for tests. So here, let's rename this to readDocumentation. That's what we're testing, that we can read the documentation. Okay. So we have our higher level feature test, and we got to a point where we decided, well, you know what, we want to call this, this is the API we want. So let's put the feature test on hold and drop down and write a dedicated test for this.

you know what, we want to call this, this is the API we want. So let's put the feature test on hold and drop down and write a dedicated test for this Documentation class. So here, I could say, it gets the documentation page for a given version. Okay. So assuming we knew up this Documentation class that doesn't yet exist, so that would be app/Documentation, if we call a get method for it, we're going to need to give it the version as well as the page. Now consider this, this class is actually going to check if a file exists and fetch the file's contents.

Now consider this, this class is actually going to check if a file exists and fetch the file's contents. So I don't want our test to have to handle that, and I don't want to have to rely on an actual documentation page existing. So I'm going to mock some of this out. We can do that with a partial mock. So let's save that to content. And now if you think about it, we know that when you call this get method, it's probably going to use the File class to fetch. So we could, we can mock that out if we want.

going to use the File class to fetch. So we could, we can mock that out if we want. There's actually a couple of different ways we could do it. We could intercept the path that we are calculating and just change it out on the fly. We can mock it and say, I actually want you to load this file path instead of where you would normally look. Or we could say, well, we're going to expect you to fetch the contents of a file, but we're actually going to return something else instead. So I'll show you what that might look like. We could say file should receive a request for get with, and now the path should be resources

So I'll show you what that might look like. We could say file should receive a request for GET with, and now the path should be resources/docs, the version is 1.0, and the page is example.md. That's what we want, and instead we're just going to hard code what we return instead. Example page. All right, so does that make sense? Let's begin by running this test. Okay, it fails because documentation does not exist. Okay, app/documentation.php, and remember, this isn't an Eloquent model. It's just a basic PHP class.

Okay, app/documentation.php, and remember, this isn't an Eloquent model. It's just a basic PHP class. All right, one more time. Now the error has changed. So we tried to call method get, but that doesn't exist. Fair enough. We run it again, and we get green. Oh, that's right, I'm sorry. We expect exactly one call. So now if we run it, yeah, zero times, what's it called?

We expect exactly one call. So now if we run it, yeah, zero times, what's it called? Okay, let's get started. So we'll go to our documentation file. We'll accept the version as well as the page, and yeah, we could start by saying file make a GET request to the resource path, and then within it, docs, the version, and the page.md, and return that. Okay, so let's make another run. Okay, it's still failing, but we changed the error. So it didn't find a handler, and this is the path that you used.

Okay, it's still failing, but we changed the error. So it didn't find a handler, and this is the path that you used. Oh, you know what? We're using a local path here, but up here, we're using a full path. So let's say resource_path, like so. All right, one more time. Okay, it's still failing. It looks like it's this, resources/docs/1, when I assume that should be 1.0. There we go. Okay, now it's returning green.

There we go. Okay, now it's returning green. So if you take a look at this, if we dd the content, you can see that we did intercept that request. Now we don't have to load an actual file or create a stub there and delete it when we're done. We'll just say, okay, if somebody makes a request for this markdown file, just return this dummy data here. So now what I'm going to assert is that what gets returned should be the compiled version. So I want examplePage to be returned, actually, and we'll compare that against the content.

So now what I'm going to assert is that what gets returned should be the compiled version. So I want example page to be returned, actually, and we'll compare that against the content. So we'll give that another run, and here is what we expected, and here is what we got. All right, it sounds like we need to run this through a markdown parser. Now we can see what Laravel used, or again, you can grab whatever you want. So often what I'll do is just find whatever the most popular package is for what I need. And in this case, I think this is what Laravel uses. You can see it has far and away the most downloads, so it's fairly reliable, I think. All right, so we can go ahead and pull that in. And while that's doing its thing, if we scroll down, let's see, you instantiate Parsedown,

All right, so we can go ahead and pull that in. And while that's doing its thing, if we scroll down, let's see, you instantiate parseDown, you call a text method, and you give it some markdown. And it looks like parseDown is a top level, so there's no namespace. Okay, so let's switch back, and I promise we'll clean some of this up, but why don't we return new parseDown, call the text method, and pass the content to it. One more time, class parseDown not found. That's because it's at the top level. One more time, and we get green. Excellent.

One more time, and we get green. Excellent. So now we know, at the very least, this, in fact, is working. Now what about the condition where we fetch a file that doesn't exist? Well maybe an Exception should be thrown. So how about this? It throws an Exception if the requested markdown file does not exist. Okay, so let's try this. We will make a GET request for this, but this time I won't mock it. So it's going to try to find this file, it won't exist, and we can say this, expect Exception.

We will make a GET request for this, but this time I won't mock it. So it's going to try to find this file, it won't exist, and we can say this, expect exception. And maybe we'll just expect a standard exception for the moment. Okay, so if we run that, we will get green because it will automatically be thrown, but let's change this up. So let's say here this will be the markdownPath, like so, and then I can say, well, if the file exists, markdownPath, then fetch it. So actually, real quick, let's use file and use parseDown. Okay, so if that's the case, load it and return it. Otherwise we're going to manually throw an exception here.

Okay, so if that's the case, load it and return it. Otherwise we're going to manually throw an Exception here. The requested documentation page was not found. All right, so now we can grab this, clean that up a bit, and let's give it a run. All right, we're still getting green, but now we're throwing our own Exception in the message. If I were to comment that out, of course, it would fail. Anything else we want to do here? Well, you know what? Use parseDown.

Well, you know what? Use parseDown. I think I accidentally removed that. Okay, so if the file exists, so here's one thing I often do. Let's rename this to page, and sometimes I will define it inline. So I could do this here, $page equals that. Let's run it again. We still get green. So if this page exists, fetch it, compile the markdown, and return the result. We could now even say return new parseDown text and then pass it the result of that call.

Wire Controller to Docs15:24

So if this page exists, fetch it, compile the markdown, and return the result. We could now even say return new Parsedown text and then pass it the result of that call. Run it, and we still get green. So now at this point, we have basic assurance that it works, which means I can jump up a level to our feature test again and run it. Okay, it's still failing, of course, but that's just because we haven't triggered that Documentation class. So we'll come back here and we'll say, well, we could use it as a facade. I'll show you how to do that in a minute. But until then, we'll say new app\Documentation and make a GET request.

I'll show you how to do that in a minute. But until then, we'll say new app documentation and make a GET request. So that would be the content. So I'll show you real quick. Let's return the content because I think at this point, we're going to have to make a tweak, won't we? Yeah, so it looks like this is the whoops page. So I'll show you right here. Let's say this without exception handling. Run it again.

Let's say this without exception handling. Run it again. Okay, so now we can see here's the actual exception that was thrown. All right, so what's the issue? Well, if we come back, we're trying to get docs/default/version/stub, but we know that doesn't exist. So why don't we see if we can intercept it and say, okay, well, when we run this, I actually want it to go into helpers/stubs, and return that markdown file instead. Here's what I think we'll do. Let's retain the same structure.

Here's what I think we'll do. Let's retain the same structure. So we'll say we'll call that docs, and then we'll create a default version here, 1.0. And now this stub will move there, like so. And I can delete that. Okay, so now we're keeping the same structure, docs/version/file. And that's what we have here. So now I can just set a different root path. So you'll see if we come back, in documentation, we're assuming resource_path, but maybe we can make that the base_path instead.

So you'll see if we come back, in documentation, we're assuming resource_path, but maybe we can make that the base path instead. Or what we could do is extract this to a method called markdownPath. I often do this. Now you can intercept this method and just return whatever you want. So in this case, this is what that would return, right? By default, it's going to look in the resource_path directory for docs, version, and page. Now we can remove that and say this markdownPath and give it the version and the page. So quickly, let's run our test. We're going to go back to documentationTest.

So quickly, let's run our test. We're going to go back to documentation test. Are we still good to go there? Yes. So we haven't broken anything. But now for our feature test, we can just override this if we want. Lots of ways we could do this. So let me show you that example. We can say mockery, and we're going to mock the documentation, but I just want a partial mock.

We can say mockery, and we're going to mock the documentation, but I just want a partial mock. So let's override, basically, this markdownPath method. And I'm going to say, well, when you fetch that, I'm sorry, when you mock it, here's what I expect. Our mock should receive a call to markdownPath, but rather than triggering any of the functionality within it, I'm going to instead return, and let's see. We want our base path, stubs/docs/1.0/stub.md. We're going to assume. We're going to set the markdown file that we read.

We're going to assume. We're going to set the markdown file that we read. Okay. But now, yeah, if we run it again, so we're going to go back to our feature test here, it's still going to fail. The requested documentation page was not found. And that's because, yes, we've mocked that file, but we haven't updated it in the service container. So I'll show you. Right here, we make a call to get.

So I'll show you. Right here, we make a call to get. You'll see right here, I was called. We're still triggering this functionality. We haven't intercepted it, as you see there. There's two ways around this. If we come back to our controller, we could use constructor injection. So let's inject the docs, and this will be an instance of documentation. Okay. And we'll import that.

Okay. And we'll import that. Now the benefit to this is we can intercept this. So Laravel will store it in the service container. And we can just say, well, if you try to find this, I want you to use the mocked version instead. So here, we could say, this docs get. Okay. So if we give that a run, though, we're still triggering it. Here's the next step.

So if we give that a run, though, we're still triggering it. Here's the next step. If we come back here, we have mocked it. So now we can say, in the service container, the instance of app documentation you have, or if one is requested, I actually want you to return this mock that we've created here. All right. So let's give that a run. So now we can see it's still failing, but we no longer have hit that method. Let's see what happens if we do make a request for it. So right here, we run it.

Let's see what happens if we do make a request for it. So right here, we run it. And sure enough, you can see we did go in that stubs directory. But actually, in this case, it looks like I forgot to do tests. Okay. So that's going to go into tests, stubs, docs, 1.0, stub.md, and actually, this is in helpers as well. So that would be the full path. All right. So let's run our test again.

All right. So let's run our test again. And we do get green. Okay. So what did we assert there? Let's go back. Well, we said, when I make a get request here, behind the scenes, it's ultimately going to try to load this markdown file. So if we take a look at that, we see this and this. So we assert that this is what we see in response.

So if we take a look at that, we see this and this. So we assert that this is what we see in response. Our only remaining step now is to return a dedicated view. return view called docs, where the content, and we'll just do this inline. This docs get version and page. And do note, I told you there were two ways to handle this. I'll show you the next way in just a second. Bear with me. So let's run that. It's not going to fail because there is no docs view.

So let's run that. It's not going to fail because there is no docs view. resources/views/docs. And we don't have any layout here. We're just going to do it in basic HTML. Let's echo out the content here. You know, in real life, you'd have your sidebar with all of your documentation links, and this would be in your main content area, but we're going to be quick. So we run it. This is what was generated, but it's all being escaped.

So we run it. This is what was generated, but it's all being escaped. Let's fix that. One more time, and we do get green. So let's run the full suite. We have one issue. It gets the documentation page for a given version. All right. So this one, it looks like it failed. It's related to the call to the exists method.

So this one, it looks like it failed. It's related to the call to the exists method. So why don't we say, and this is where you have to be careful with mocks, by the way. When you bind your test too much to what the production code looks like, you're going to create a world of hurt for yourself. So you want to be very thoughtful about how you do these things. And often, that's why it's better to remove the mock entirely. So if we decide that, we'll do so. Anyways, we can accept a call to exists, and we will just return true. All right.

Anyways, we can accept a call to exists, and we will just return true. All right. Run it again, and now we get green. Let's run the full suite, and all of those tests are returning green. Anyways, we can talk about this more in a bit. Now if we come back to the DocumentationController, I noted that there were two ways to handle this. So yes, we could inject it, or we could use a real-time facade. It just depends on your comfort level. I think for a certain type of app, the real-time facades are very, very cool.

It just depends on your comfort level. I think for a certain type of app, the real-time facades are very, very cool. So let's see. We would get rid of this entirely. We're going to use it as a facade, but we're not actually creating a facade. So if we run that, we got to import that. And this is going to all fail, right? Because we're trying to, in this case, call a static method, and we know that's not going to work. So it all blows up.

But I think that's a responsibility that could exist on documentation. So let's say documentation versions. All right. So we run it. Everything's going to blow up. We're going to create a method here. Now this will be a genuine static function called versions. And we will return an array. All right. Let's run it again.

All right. Let's run it again. And now we get green. So we're basically doing the same thing, but we're just reassigning the responsibility. In this case, I think the Documentation class should be responsible for knowing what are my valid versions. All right. So if we come back to DocumentationController, if not an array, the requested version, and the list of approved or valid versions, then we will redirect to the default version. Otherwise, we will load the documentation page.

Handle Missing Pages24:24

the list of approved or valid versions, then we will redirect to the default version. Otherwise, we will load the documentation page. Now let's come back to read documentation test. I'd like to write one more. It aborts if the requested documentation page is not found. So let's imagine we make a GET request to docs, and then the default version, and then does not exist. All right. Well, in this case, they're trying to request a file where we know behind the scenes it won't find an associated markdown file.

Well, in this case, they're trying to request a file where we know behind the scenes it won't find an associated markdown file. So I do expect a 404 in that case. We'll assert not found. Let's run it. And it fails. So we got a 500 status code. Let's right here disable exception handling and run it again. Okay. So here, yeah, it's trying to load it.

Okay. So here, yeah, it's trying to load it. We caught that exception. So it got to the documentation page. And right here, sorry, the page you're trying to get doesn't exist. So we throw it up the chain. We throw an exception and send it up the chain. Let's catch that. We'll try to load the view and fetch the contents. But if we catch any kind of exception, here, we can abort.

We'll try to load the view and fetch the contents. But if we catch any kind of exception, here, we can abort. Or we can handle this in our ExceptionHandler class. Whatever you want. But we'll say the requested documentation was not found. All right. So if we give that a run, and we can see that it did throw a not found HttpException. So let's come back here. We can turn off exception handling because we do want it in this case. And we get green.

We can turn off exception handling because we do want it in this case. And we get green. So once again, phpunit is fully at green. And we haven't even looked at it in the browser yet. That's our next step. laracasts/docs/test/1.0/authorization. And whoops, I'm sorry. We said docs, right? But yeah, still, we get the standard 404 page. And that's because we requested a markdown file that doesn't exist.

But yeah, still, we get the standard 404 page. And that's because we requested a markdown file that doesn't exist. So that's the next step. Resources. And we said it should be in the docs version. And then in this case, let's create authorization.md. All right. We'll say authorization. Here are the docs for authorization. We'll have step one, step two, step three.

Here are the docs for authorization. We'll have step one, step two, step three. You get the idea. So now if we come back to Chrome and give this a refresh, we have the compiled markdown. And we knew this would work, right? We had a full test suite that proved it. If we request the version 1.1 docs, we want that to be different. OK. Let's come back here. I'm now going to create resources/docs/1.1/authorization.md.

Let's come back here. I'm now going to create resources docs/1.1/authorization.md. Now remember, the way the layerfile.com website works is each of these versions is up on GitHub. So you would clone the versions into this directory. And that's how you can keep everything separate but still in sync here. All right. So here we'll say we'll copy this. But I'll say here are the version 1.1 docs for authorization. And this will be the version 1.0 docs. And we'll talk about substituting that in a moment.

It's just a dummy placeholder here. So we'll have that here and here. And that way, when you merge this into a new version, you wouldn't have to do a manual search and replace to update the hard-coded version numbers. So to test this, though, I'm going to go into my test directory and let's see. We can do this down at our unit level. Now first, a couple of things. Use file, like so. Let's use documentation. Just a little bit of cleanup here.

So something like that. OK. So now, if we give that a run, it's going to fail because we expected this, but at no point do we substitute that placeholder. All right. Let's come back to documentation. And we'll say this, replaceLinks. Yeah, we learned about all of this when we were reading the laravel.com source code. All right. So we're going to create a method called replaceLinks.

All right. So we're going to create a method called replaceLinks. We need to accept the version as well as the content. Now we can do a simple string replace. Look for version, replace it with the actual version that was passed using the content as a base. All right. Let's give that another run. Hmm. Oh, sorry.

What if the documentation as a third argument accepts a base path? In this case, we could say it should be, we just want to go straight to our tests directory. tests, helpers, stubs should be the base path. Then I want to grab version 1.0 and stub.md. If we take a look at that, now here is what we expect to see. We can now grab that and say assertContains, clear this out, that paragraph, and compare that against the content. So now we're not mocking it, we're actually going to fetch the file entirely. So if we run it though, it's going to fail, of course, because it can't find it. Okay, so we'll come back to documentation.

So if we run it though, it's going to fail, of course, because it can't find it. Okay, so we'll come back to documentation. As a third argument, we're going to accept the basePath. And then we'll pass that here. Okay, let's scroll down here, the basePath, and we'll say $basePath equals what you gave us or the resourcePath. Now I could say return $basePath, and then from there on, this structure would remain the same. So for our test, we're basically saying the basePath is tests/helpers/stubs, all right? Then docs/version/page.md.

So for our test, we're basically saying the base path is tests, helpers, stubs, all right? Then docs, the version, and the page.md. All right, so let's run this again. And we have an issue on line 24. All right, we get green. So now that's still working, but we were able to remove the mocks entirely. And I think this is actually a good refactor, because now it doesn't matter if I'm using file_exists or file_get or a FileSystem class or my own File class that I create on my own. The test is not dependent or contention upon what I do here. It doesn't care.

Anyways, we're high on time, so I'm going to call this a day. Actually no, I'm lying. Right here, one more final thing. Let's make this more readable. If it's not a valid version, then redirect. That reads better. Valid version, give me the version, and I'll paste that in there, and we'll return if the version is in this array. Okay, so if it is, it's a valid version. Let's run our test suite again.

Okay, so if it is, it's a valid version. Let's run our test suite again. We still get green. But yeah, notice that just reads a little better. This, you have to dig into it a bit. Okay, is the version in the array of the documentation versions? Whereas here, well, if it's not a valid version, redirect. Those little improvements in readability, I promise you, matter a great deal. Okay, so I'll put this code up on GitHub. You can review it.

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