در حال بارگذاری ...

Install Cypress in Laravel0:00

Every Friday, I dedicate half of my workday to learning. Maybe a new technique, or tool, or library. This week, that tool is Cypress, which is an acceptance testing framework, a very cool one at that. So, if you'd like to hang out for a bit, why don't you come along and I'll show you everything I learned about Cypress with Laravel. As we always do, we'll start from scratch. laravel new demo. Alright, we'll cd in there. Next, we need to install Cypress with Node.

Alright, we'll cd in there. Next, we need to install Cypress with Node. Great. So, if I now open this in my editor, here we go. Alright, so no reference to Cypress yet. But if I go into the node_modules/bin directory, you will see the Cypress executable there. So, let's try to run it. And I'll do it from the built-in terminal here. We can do node_modules/bin/cypress, and there should be a command called open. And there we go.

Create First Cypress Spec1:43

So, for example, you can go to each step of the process. So you can imagine a form where you fill out the name, the email, the password. And each one of those represents its own action, which means if you want to go back a few steps, you can do so by hovering over each of these items. So cool. Now I can rerun them right here, or I can press the letter R. And that's what I'll mostly do in this lesson. Alright, so have a look at this. We'll bring this to the side, and go back to PhpStorm. And within my integration folder, I'm going to create a new test.

We'll bring this to the side, and go back to PhpStorm. And within my integration folder, I'm going to create a new test. Now, we don't really have an application here, so I'll just call it our login spec. Okay, so immediately as I create that file, you'll see that it's picked up here. And I can click on it. So we're trying to run it, but yeah, there are no tests. Alright, check this out. Now we can begin by describing the feature. In this case, a simple login system. And then we can write each assertion like this.

In this case, a simple login system. And then we can write each assertion like this. It shows the login page. Now notice, as soon as I save to the page, the browser instantly updated. So check this out. I could say Cypress for Cypress. It's a global variable that will always be available to you. And I could say visit the home page. Now if I click save again, it tries to execute that command, but it immediately fails. And the reason is, right now, we haven't set a base URL.

Now if I click save again, it tries to execute that command, but it immediately fails. And the reason is, right now, we haven't set a base URL. So we need to tell it, well, in our case, the base URL is demo.test, as you see there. So here's what we can do. If I open up the sidebar and scroll down, you'll also see a new cypress.json file. And we can specify it here. The base URL is, at least right now, demo.test. But now notice Chrome immediately closes. So I assume whenever you change the configuration file, you have to boot it up again, as you see here. So I'll just close that out, rerun it, and we'll go back to our login.spec.js.

Add Laravel Auth Scaffolding4:01

Often, your website will load in the necessary data. So it'll wait a maximum of, and I'm not sure what the default is, 3, 4, 5 seconds, before finally failing the test. Anyways, let's bring it back. It runs again, and it passes. Now what's cool is, because you have access to these examples, whatever you need to do will be available here. So you can research the actions, or working with location, or navigation, and things like that. Anyways though, in our case, to show the login page, well that should be an endpoint of this, right? So I could say visit the login page, and of course it fails, because we don't have an endpoint. Okay, let's pull in Laravel UI. So I will add a new terminal here, and I can say composer require Laravel UI. All right, so now we'll have access to a php artisan ui command,

So I will add a new terminal here, and I can say composer require laravel/ui. All right, so now we'll have access to a php artisan ui command, and in this case, the front-end framework, it's irrelevant to this lesson. We'll use view, but we're not going to make use of it. But I will pull in the authentication scaffolding. Now alternatively, by the way, when you create a new Laravel application with laravel new demo, I believe you can also add the --auth flag, and that's a recent addition at the time of this recording. Anyways, that's done, so I will go ahead and compile this down. Whoops. There we go.

Whoops. There we go. Okay, so now if I go to my routes/web.php file, you will see it's registered some authentication routes, including /login. So let's go ahead and give this a hard refresh since we've updated, and there we go. It loads the login page. All right, let's go back. Now if we want to fill in these inputs, check this out. I can click on this inspect tool here, and then hover over the various elements. So in our case, we can see this input has an ID of email, and I can then copy it.

I can click on this inspect tool here, and then hover over the various elements. So in our case, we can see this input has an ID of email, and I can then copy it. We don't have much real estate here, but yeah, I can copy it to the clipboard. So notice if I paste this in now, we're saying, Cypress, get the input that matches this selector. It could be an ID, it could be a class of email, or it could be an input with a name of email. If you're familiar with using jQuery selectors or CSS, you're good to go here. So we will grab that input, and we will type foo at example.com. Now as I save it, notice it immediately runs. Let's try it again. Let's change it to bar at example.com.

Let's try it again. Let's change it to bar at example.com. I save it, it reruns, and it fills it out. It's so cool. Let's do another one. Now again, I can select here, and I can see it's an id of password. So grab that, and then we will type password into it. All right, run it again. There we go. And then finally, we want to click the button called login.

There we go. And then finally, we want to click the button called login. So I think we can say, find a button that contains the text login. So remember, I'm still learning this myself. If you see anything that is very much wrong, you're not going to hurt my feelings. Please leave a comment and let me know, because I'm still figuring this out on my own. So if I want to click on a particular link or button, is this the correct way? So if I run it, it did find it, so it passes. And then I think I can just hit click. So is there some alternative like getButton or clickButton or submit?

Test Invalid Login Errors7:33

This one is just a basic assertion against that endpoint. But how about it displays an error for invalid login credentials? All right, so if we visit the login page, and we type in gibberish here, and we submit the form, then we expect to see this warning here. These credentials do not match our records. So cy.contains, these credentials do not match our records. Save it, run the test, and it passes. All right, so now we're testing failed validation. All right, so now let's go back to our routes file, and let's deal with the dashboard here.

Test Dashboard Guest Redirect8:08

All right, so now let's go back to our routes file, and let's deal with the dashboard here. This is that home endpoint. Okay, maybe we'll create a brand new spec here, and we'll call it about dashboard.spec.js. Okay, so once again, we will describe our dashboard, and then we'll say it disallows, or it should not allow guests to view the dashboard. Okay, so now we understand this. I can say if we visit /home as a guest, it shouldn't work.

Okay, so now we understand this. I can say if we visit /home as a guest, it shouldn't work. We have a middleware, presumably, and it should redirect the user to the login page. So again, I don't know if there's some redirect helper. I think I have to do something like this. URL should include /login. It feels a little verbose to me, so I wonder if there's something easier, but at least this should work, I believe.

so I wonder if there's something easier, but at least this should work, I believe. So if I click save, we have to go back to our tests, and if we scroll up, let's hide those. There we go. Here's our dashboard. All right, so we run this code. We visit /home. It processes a 302 redirect to the login page. So this works right out of the box, again, because Laravel has already set this up for us.

Set Up Testing Environment9:15

So this works right out of the box, again, because Laravel has already set this up for us. All right, so now let's dig into the nuts and bolts of it. So for our login spec, yes, it shows the login page, and yes, it displays errors for invalid login credentials. But what about a successful login? Well, think about it. What would we have to do? As I see it, it's something like, given I have an existing User, and they visit the login page,

given I have an existing User, and they visit the login page, and type in their credentials, if it's valid, they should then be redirected to the dashboard. So these three lines here are pretty easy. We've learned how to do that. But this part, that's where it gets a little bit tricky, especially when you're doing acceptance testing. How do you specify a different environment or a special database when you're performing acceptance tests?

How do you specify a different environment or a special database when you're performing acceptance tests? And the answer is, at least to my knowledge, you have a couple options. So let's play around for a minute. I'm going to open up the sidebar here, and here is our .env file, right? This is our local settings. So I'm going to just reuse environment.example. We'll make this identical.

So I'm going to just reuse environment.example. We'll make this identical. But I will then rename this to environment, either Cypress or acceptance, whatever you want. We'll do something like that. Okay, now we have a special environment specifically for acceptance testing. So you can see, for our standard environment, let's keep it simple. Let's say we have an SQLite database,

let's keep it simple. Let's say we have an SQLite database, something like that. But real quick, if I go to config/database.php and I scroll down to sqlite, right here, it wants us to give the path to our database, or it will default to database.sqlite. So if we come down here, we can create that, database.sqlite. So now I can php artisan migrate,

database.sqlite. So now I can php artisan migrate, and this will migrate our local database. However, if I run php artisan help migrate, you'll see there is an option to override the environment. So check this out. If I were to say php artisan migrate --env=acceptance, but right now if I run it, it'll say nothing to migrate, and that's because we haven't changed this.

it'll say nothing to migrate, and that's because we haven't changed this. It's still identical to our standard environment. Okay, so let's come up here and we'll say, at the top, we're still going to use the database of SQLite, but I'm going to override the database. This will be called acceptance testing. Okay, so now, if we try to run it again, it's going to fail, and that's because

Okay, so now, if we try to run it again, it's going to fail, and that's because we haven't created that database. Database acceptance does not work. All right, let's go ahead and do that now. But again, if I try to run it, it'll fail, and that's because we haven't included the path. So take a look at this. If I go to config/database,

So take a look at this. If I go to config/database, you'll see, yeah, so the default is actually the path to the database directory. So why don't we tweak this a little bit? Why don't we say, no matter what, I want the database path, and we'll set this as a default. Is that right? Yeah. So we're just tweaking it a little bit.

So again, you have a couple choices here you might consider. I'll show you both. One would be to do something like this. So let's go back to our login spec. We could say here, before any of these tests run, I could say cypress.execute a console command. And what if we just update what our default .env file is?

And what if we just update what our default environment file is? And here's what I mean. Let's copy .env to .env.backup, and then do another one where we copy environment.acceptance to .environment. So all we're doing here is when we run the tests, and it boots up the browser,

when we run the tests, and it boots up the browser, well, let's just swap out our default .env file, because we're going to use these new settings here. So yeah, that would be an option. And then when we're all done, I could say after these tests are complete, and by the way, we can specify these as global commands,

and by the way, we can specify these as global commands, so they only run once. But after it's complete, let's just reset things to how they were. And because we did copying here, I can just say move .env.backup back to its original location, and that'll bring us back to normal. So that would be one option.

And then just for a quick visual, I will go to the login page. Let's look for login. Yeah, just right there. I'm just going to spit out the application's environment so you can see the difference. All right, so let's go back to Chrome. And if I go to demo.test/login, by default, we're in the local environment.

And if I go to demo.test/login, by default, we're in the local environment. However, when we're running the acceptance tests, you'll see we're in the testing environment. So that would be one way to do this. And remember, because we've changed the environment, you can now use any database or any credentials you want. So what we can do here,

and this is where the support directory comes into play. So I could paste them. I'll just do it right here. And now we have global before and after callbacks or hooks. Okay, so that's one option. However, one downside is sometimes when you're running your acceptance tests, they will fail,

If you want to take that approach, just create the custom domain there. A little tip, if you use Laravel Valet, what you can do is just link a new domain name. So I could say valet link, and how about we call it demo.acceptance? All right, now we have a new symlink. So let's try it out. Yes, we have our main site,

So let's try it out. Yes, we have our main site, but we also have acceptance.test. Same project. Yeah, now we have a hook of sorts to say, well, if this is the domain, then clearly we want the acceptance testing environment. And what we could do, let's see, let's go into bootstrap/app.php. Yeah, we could just add a little check.

let's see, let's go into bootstrap/app.php. Yeah, we could just add a little check. How about right here? We could say, let me show you this. If we take a look at the $_SERVER superglobal, come back to Chrome, give this a refresh. Yeah, what we want is right here, the HTTP_HOST.

Yeah, what we want is right here, the HTTP_HOST. So why don't we say, if $_SERVER['HTTP_HOST'], and actually in certain environments, like if you're running a phpunit test, that will not be set. So let's first check if it's set. And if so, if that HTTP_HOST equals our special domain there,

And if so, if that HTTP host equals our special domain there, in that case, we want to use this environment. And what we can do is, behind the scenes, layerfile uses .env. So I could say create. It wants to know what the path is, as well as the environment file.

It wants to know what the path is, as well as the environment file. So this will just be the base path, and the environment file is called .env.acceptance. And we'll call overload, because that's the one we want to use now. And I think that's all there is to it. So let's try it one more time. We'll go back to that login view. I will bring back what we had earlier,

We'll go back to that login view. I will bring back what we had earlier, where we quickly spit out the current environment. And now, there we go. Login testing. So have a look at both of these. For demo.test, we're in the local environment. But for demoacceptance.test, we are in the acceptance testing environment.

But for demoacceptance.test, we are in the acceptance testing environment that references a totally different database. All right, so this option number two, a little more work, but it does come with the advantage of, if there is some kind of failure that's not clear when you run your Cypress tests, well, you can actually load exactly how the site would look

well, you can actually load exactly how the site would look in this special environment. Something to consider. So last little thing here, if we want to take this approach, back in PhpStorm, don't forget to return to cypress.json and then update your base URL. Okay, so one more time.

but these are things you just have to figure out. So now, we should be able to streamline things. Let's have a look at our login spec. Given I have an existing User. So you'll know, for Laravel and PhpUnit, you would do things like this, where you'd say, all right, just whip up a User. But we can't do that, because we're in the node environment.

But we can't do that, because we're in the node environment. So we need some way to reproduce this. Think about it. From the node end, how would you reproduce a setting like this? Whip up a User. Whip up a Post. Whip up a Comment. I can think of a couple options.

Whip up a comment. I can think of a couple options. One, you could have a special artisan command you create. So you could say cypress.exec php artisan, I don't know, something you create, like cypress.factory. And then you pass in the credentials there. And now you'll have a command that will trigger that factory function. So that's an option.

Create Test Helper Endpoints19:52

that will trigger that factory function. So that's an option. But instead, consider this. What if we created just a couple of helper endpoints? These are route endpoints that will only ever exist in this environment. Let's see what that might look like. And I'll show you a couple ways to deal with this. So I'm going to go to my routes file. And I want to be clear.

So I'm going to go to my routes file. And I want to be clear. You have to protect these. Do whatever you need to do to ensure that these routes we create are not available outside of this environment. So write tests, whatever you need to do. We could say if the app environment is testing, so we'll say Route::get, and we'll use some kind of convention here like this.

so we'll say Route::get, and we'll use some kind of convention here like this. Testing::create, and then we give it the model, whether it's User or Post or Comment. And we'll accept that. And now all I have to do is say factory(App\Model::class)->create. So again, if the model is User, we're just doing this to build up the factory,

So again, if model is User, we're just doing this to build up the factory, and I will return the results. Now I know this might feel a little weird, a little wonky at first sight, but give it five minutes. You may find that two or three of these acceptance testing specific endpoints can really be useful. And of course, we can extract these

and you can copy it to every new project. Okay, so have a look here. Given I have an existing User, we're not going to visit. We're just making a standard request here, and we want testing/create/user. Now that's going to return a promise. We can use this helper function it's here. This is kind of useful. This allows us to get a property's value

This is kind of useful. This allows us to get a property's value from the previously yielded subject. We're just saying from the ultimate response after the promise, what I really want is the body of the response, and I'll show you that a little more in a moment. So have a look. When we make that request, we'll get a User in response,

When we make that request, we'll get a User in response, and now I can log something to the Cypress console. The created User is just for your review, and we'll have this. All right, so does this make sense? We make a request to this endpoint. That endpoint is defined right here. It accepts a wildcard. This will be the name of the model,

It accepts a wildcard. This will be the name of the model, and we simply pass that through. Now we'll get the body of the response, which will be the user itself, and then we log it to Cypress. So let's give it a refresh. We're on this one right here. Here it is, and you'll see that prints to the console,

Here it is, and you'll see that prints to the console, so I will open that up with Shift Command C. Go to the console here. The created User is, and here we go, Crystal Windler. It works. Kind of cool. Now admittedly, a little clunky right here, but we can clean this up.

Now admittedly, a little clunky right here, but we can clean this up. First, one quick thing, though. What if I want to override it the same way I would with the standard factory function? Let's say $name is John Doe. We want this to work as well. All right, well, this will be passed through as the attributes, so that means they will be available.

and sure enough, we were able to override it. All right, so we get the exact same affordance, but we're running our tests with Node instead of php. Okay, so now if I come back here, yeah, let's make this a little cleaner. I'd rather say cypress.create a User and have it work, or cypress.factory, whatever you prefer. We'll do create. Okay, if we want this to work, here's how.

Add Custom Cypress Commands23:37

We'll do create. Okay, if we want this to work, here's how. If I go to our commands file, you'll see this is kind of what we want, isn't it? cypress.commands.add create. It wants to know the name of the model as well as the attributes, or we'll call that the overrides. And yeah, that'll just delegate like so. So let's tweak this a little bit.

And yeah, that'll just delegate like so. So let's tweak this a little bit. This will be the name of the model that we are passing through. This will be the overrides, and then we return the result. It's just a little wrapper for Cypress. So now if we switch back, I no longer have to write that out because that'll annoy me.

I no longer have to write that out because that'll annoy me. Instead I just say, cypress.create a User, and then with that resulting User, at the moment we're just logging to the console. We're going to say, given I have an existing User, we do now. Now we can move on to these steps. And they visit the login page, all right?

Now we can move on to these steps. And they visit the login page, all right? And they type in their credentials. All right, we can grab these three right here. And by the way, these can all be chained continuously if you want. But anyways, we can add these now, user.email. For the password, the default is always the string password,

For the password, the default is always the string password, unless you override it. Finally, we click the login button, and they should be redirected to the dashboard. Okay, we could say, cypress.url should equal or contain. So if you do equal, just make sure you include the base URL. Otherwise do, I think, contain and include.

It works. So again, don't forget we have time traveling here so we can see it as it was. We visit the login page. We find the email input. We fill it in. We find the password. We fill it in. We find the button that has a login. We click it.

Reset Database Between Tests25:40

We find the button that has a login. We click it. And then finally, we expect the new URL to match. Maybe we could even assert against this, if you like. Cypress.contains. All right, the last couple of things, and then I will let you go. Let's bring back the terminal here. If I boot up our php artisan tinker command, and I'll set the environment,

If I boot up our php artisan tinker command, and I'll set the environment, if I do a count on User, it's filling up that user table, and that's because we're never truncating it when we're done. So for example, if we run all of these again, eventually we're going to start getting errors related to the email address. But if I do it again, now you have 17. So you need to remember to clear out your tests.

But if I do it again, now you have 17. So you need to remember to clear out your tests. This is standard practice. So again, you can do it in a couple ways. You could do before. So if you only want to do it once per suite or once per feature, do that. Just be a little careful. You could say, once again, execute a command, and maybe all you do is just php artisan migrate:refresh,

oh, it didn't work. Yeah, this is one thing to be careful of. So in this case, we are in our acceptance testing environment when we are running Cypress, right? But in this case, we're executing a standard console command, but that would be executed in the local environment. So be a little careful. This can get tricky sometimes. Make sure you're clear about the environment that the command should be run in.

as you see there. Okay, so that's an option. If you want to run it before every single test, you could do something like beforeEach. And again, if this is something you'll do everywhere, you can move it into your support/index.php file. All right, we're really high in time. One last thing I want to show you. So you've learned how to set up acceptance testing environments. You've learned how to swap things out if you need to.

Add Login Helper Route28:33

but let's say it allows logged in users to see their dashboard. All right, so now we're saying something like this. Given I have an account and I'm signed in, if I visit the dashboard, I should see it. That's basically what our test is here. So why don't we take the exact same path we did up here? Why don't we add a new route for logging in a User? It's a shortcut for the testing environment to set a logged in User. Something like this. What we can do is just save that.

Something like this. What we can do is just save that. We're always going to whip up a User. And then I can say login that User. All right, makes sense? We will automatically set the signed in User to a brand new person we create. And again, if you want to override their settings, you can do so here. So again, just two or three test specific helper routes,

It ends up being kind of annoying. So this can be a good alternative. Okay, so now have a look. If I come back here, I could just say request testing login, and now that takes care of those two. If I visit the dashboard, so .visit /home, I should see it. So we have something like this. Clear that up. All right, let's give it a shot.

Sorry about that. Let's come back here. I bet you saw that. We're not accepting anything there. Okay, so if we come back and we give it another run, ta-da, it works. Notice here is where we hit that login path to set the authenticated user, and then now that we're in the session, we can access the home page, and we can say right here .contains,

we can access the home page, and we can say right here contains, you are logged in. Give it a refresh, and it works. Okay, so the last little step here, again, I don't like writing that. I want that to be a little more clear, so I will change this to login, and we might want to allow it to override. Again, this would be like the attributes

and we might want to allow it to override. Again, this would be like the attributes to the factory function, so I can log in a User with this particular name. Okay, but if we try it right now, it won't work because login is not a function. So let's go back to commands. We'll add a new one here, cypress.commands.add login. This will accept any attributes or overrides, like so,

cypress.commands.addlogin. This will accept any attributes or overrides, like so, and we'll just return a request to that endpoint. All right, so come back, and that's working as well, but yet now we had to do a little bit of work at the beginning of our application to set up some of these endpoints, but now most of the things you'll want to do are pretty easy. If I want to say, all right, log in a User,

are pretty easy. If I want to say, all right, log in a User, I can do so. If I want to say create a Post, I can do so, and it should all feel pretty clean, I think. So why don't we finish up by saying, if I log in, let's make sure this works, if I log in SallyDoe, and she visits her dashboard,

so we need to make this work. All right, let's make sure this works. So we call our custom command, login. login does optionally accept some attributes, and we can send those through along with the request. That will hit this endpoint right here. Those attributes will be again available in the request, so those will override it. That should all work.

in the request, so those will override it. That should all work. So now, if we go to that home route, you'll see this right here corresponds to that. So what do we want it to say? Hello, and then the person's name. Hello, and let's just say it directly here. Auth::user.name. All right, come back, R to refresh,

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