[0:00] Hello everyone and welcome to my Laravel [0:02] testing course. In this tutorial, you're [0:04] going to learn how to write fast, [0:06] reliable and maintainable tests for your [0:09] Laravel applications using pest [0:11] framework. This course is fully [0:12] hands-on. We will start with several [0:14] slides and understand what is testing, [0:16] why it is necessary or what type of [0:19] tests there exist. After this, we're [0:21] going to create larl project and write [0:23] our first tests. In this tutorial, [0:25] you're going to learn how to write unit [0:27] tests, feature tests, as well as browser [0:29] tests, how to test authentication part, [0:32] databases, models, what are data sets, [0:35] and how to configure hooks. We're going [0:36] to get familiar with test configuration. [0:39] We're going to also learn how to write [0:41] snapshot and visual regression tests [0:44] where we take the screenshots and [0:46] compare it to the previous stable [0:48] version. We're going to have a look at [0:49] the device emulation as well and we're [0:52] going to write tests for APIs as well. [0:54] We're going to learn how to filter, [0:55] group, and structure our tests. In a [0:58] nutshell, we will learn all the [0:59] fundamental things you need to know to [1:02] get started with testing. And finally, [1:04] we will take a real project and create [1:07] real tests for this project. And at the [1:10] end of this tutorial, we're going to use [1:11] continuous integration and deploy our [1:14] project on a production ready [1:16] environment where whenever you push [1:18] something, our written tests are [1:21] executed and the deployment will only [1:23] happen if all tests are passed. All code [1:27] written in this tutorial is open source [1:29] and the necessary links will be [1:30] available in the video description. [1:32] Before we start, I want to ask you to [1:34] hit the like and subscribe because that [1:35] really helps me to keep creating such [1:38] type of content. At the end, as I [1:39] mentioned, we're going to deploy this [1:40] project on production environment, [1:42] assign custom domain to it, and set up [1:44] automatic deployment where tests are [1:46] running. As a choice of the hosting [1:47] provider, we're going to use Hostinger's [1:49] shared hosting, which is also sponsoring [1:52] this video. In my opinion, Hostinger's [1:54] shared hosting is the best and the most [1:56] affordable hosting option out there. Go [1:59] to the website [1:59] hostinger.com/thecodeolic. [2:01] They always have some sort of discount [2:04] which gives you ability to get the [2:06] hosting as low as $3 per month which is [2:10] ridiculously low compared to what value [2:12] you're going to get with that price. [2:14] Each hosting option gives you ability to [2:16] create more than one website. You have [2:18] domain for one year and you have bunch [2:20] of other advantages. Choose whichever [2:22] option is the best for you. Then you can [2:25] adjust your business web hosting period. [2:27] The most reasonable is probably one [2:29] year, but the biggest discount you're [2:31] going to get if you choose biggest [2:33] period. If you decide to get the [2:34] hosting, use a coupon code the code [2:36] holik and you will get extra 10% off. [2:39] Now, let's focus on the course and at [2:41] the end we're going to use this hosting [2:42] to deploy our project on custom domain [2:44] on production environment and we're [2:46] going to set up automatic deployment as [2:48] well where tests are running before they [2:49] are deployed to the server. First, let's [2:51] understand what is software testing. [2:54] Software testing is a systematic process [2:56] of verifying that your application works [2:58] correctly and behaves the way you expect [3:01] it to behave. Testing ensures that [3:04] features continue to work as intended [3:08] even after months of development, major [3:10] refactoring or team changes. Here are [3:13] three main reasons why testing is [3:16] necessary. To verify your code performs [3:19] exactly as designed under various [3:21] conditions and edge cases. To catch bugs [3:25] during development before they even [3:27] reach to your users and to make changes [3:32] confidently knowing tests will alert you [3:36] to any unintended consequences. Let's [3:39] make this specific to Laravel [3:41] developers. In Laravel, when you have [3:43] tests written for your project and you [3:45] make some changes, it automatically can [3:47] detect that new code breaks some [3:49] existing functionality and eliminate [3:52] that and report this to you. It can save [3:55] your development time knowing that you [3:58] have something which already tests your [4:00] application. You don't need to test it. [4:02] It runs the entire test suite instantly [4:06] instead of manually testing features [4:07] repeatedly. It gives you ability to [4:10] safely refactor the code of your [4:12] project. Knowing if something is broken, [4:15] test will catch it. It gives you ability [4:18] to build and deploy without a fear. [4:20] Knowing your comprehensive test coverage [4:23] has your back. And finally, everything [4:25] comes down to deliver your project in [4:28] high quality to ship features that works [4:31] in various cases and that keeps working [4:34] over time when new features are added. [4:36] There are four types of automated tests. [4:39] Unit tests which has the main purpose to [4:42] test isolated logic of your project. [4:45] Typically, unit tests test a specific [4:48] method or specific class. It is fast and [4:51] focused on no database, no HTTP [4:54] dependencies, just plain logic. We have [4:57] feature tests which test the entire [4:59] userfacing workflows including roads, [5:01] controllers middlewares models [5:03] database interactions and views. These [5:07] simulate real application behavior. We [5:09] have also integration tests to verify [5:12] how multiple systems work together, how [5:15] they interact, how they share data. And [5:17] we have browser tests which simulate how [5:21] actual user behaves in your actual real [5:25] browser such as clicking on the buttons, [5:28] typing form submissions and complex UI [5:31] workflows. Pest for which we're going to [5:34] cover in this course makes this [5:36] remarkably simple. Now let me give you a [5:38] couple of differences between manual [5:40] testing and automated testing. Manual [5:42] testing is timeintensive. It requires [5:45] dedicated human to test every feature [5:48] when it's developed. Plus, you're going [5:50] to retest all previous features. If you [5:54] make some changes in the code that might [5:56] be connected to the pre previous [5:57] feature, you get inconsistent results in [6:00] case of manual testing. Manual testing [6:03] is also errorprone. It's easy to miss [6:05] subs or forget to test specific scenario [6:08] under time pressure and you have to do [6:11] repetitive work. Same manual steps must [6:15] be performed again and again and again [6:18] for every code change. In case of [6:20] automated testing, it is lightning fast. [6:23] Execute hundreds of thousands of tests [6:26] in seconds with a single comment. You [6:28] are perfectly consistent. Tests run [6:32] identically every time, following exact [6:35] same steps without variation. You have [6:38] comprehensive coverage, meaning never [6:40] forget steps, never skip checks, [6:42] automated tests, always verify [6:44] everything and continuous validation to [6:48] run your tests on every comet, pull [6:51] request or deployment workflows inside [6:54] CI/CD pipelines that changes your [6:56] deployment way. Once you have written [6:58] those automated tests, they continue [7:00] working for you throughout the entire [7:03] project lifetime and reducing time and [7:06] effort for you in this project. Now, [7:08] what makes a test good test? When it is [7:11] clear and easily readable. When it is [7:14] focused on one thing, not multiple [7:17] things. When it is deterministic. Tests [7:20] must produce every time they execute [7:24] them the same result. No randomness, no [7:27] dependencies on external APIs or current [7:29] date, time or some other information. [7:33] Always produce the same result in the [7:35] same circumstances. Tests must be fast [7:38] and tests should be meaningful as well. [7:40] If you have a look just the test cases [7:42] what you have written for your [7:43] application, you should have good [7:45] understanding what your project does. [7:47] Laravel provides an exceptional out [7:49] ofthe-box solution for testing, making [7:51] it one of the most developer friendly [7:54] PHP frameworks for building robust test [7:58] suites. Everything you need is included [8:01] and thoughtfully designed to make [8:03] testing feel natural and intuitive. It [8:07] has well organized test structure inside [8:10] test folder. You have dedicated test [8:11] environment if you want. So you can [8:13] createin testing and just configure the [8:16] database or some other parameters only [8:19] for testing. You can have automated [8:21] database management as well and laral [8:23] provides really helpful testing [8:26] utilities for HTTP requests such as get [8:28] post to act as an authenticated user to [8:32] validate to interact with JSON APIs and [8:35] so on. Now I think it's time to open the [8:37] code editor and create our first test. [8:40] Now let's create new Larl project and [8:43] have a look at testing. First I'm going [8:45] to execute Laravel new Laravel testing [8:48] course. I'm going to choose React [8:50] Starter Kit because it really doesn't [8:52] matter but I'm going to make this a more [8:54] advanced type of application where we [8:55] have this single page application with [8:57] React and Laravel. So I'm going to [8:59] choose React. I'm going to stick with [9:01] Laravel's built-in authentication and [9:03] I'm going to choose Pest as the testing [9:06] framework. Let me give you a quick [9:08] explanation. What's the difference [9:10] between pest and phpunit? PHPUnit is old [9:14] and it is the core of the testing. Pest [9:18] is built on top of phpunit and it gives [9:21] more elegant and nicer syntax to work [9:25] with testing and pest 4 has additional [9:27] great features which makes it super [9:29] super nice to work with. So we're going [9:31] to choose past. I'm going to choose no [9:33] here and let's wait until the project is [9:35] created. I open the project using [9:36] phptorm. Then we're going to bring up [9:39] terminal and we're going to execute PHP [9:41] artisan test. This is the one way to [9:44] execute tests. And as you can see all 41 [9:47] tests are passed or we can also execute [9:51] vendor bean pest. These tests are [9:54] located under test folder and let's have [9:58] a look quick look. So we have test PHP [10:01] which is the configuration file for [10:03] tests and we have then unit tests inside [10:07] the unit folder and at the moment we [10:10] don't have unit tests actually we have [10:11] only one example test and we have [10:14] feature tests. All the tests what have [10:16] been uh run right now are feature tests [10:20] at the moment. So we have this example [10:22] test and then we have the dashboard test [10:24] and some tests related to settings and [10:27] out as well. And each file also contains [10:31] several tests inside. All right. So this [10:34] is the structure. We have test folder. [10:36] We have test PHP and we have feature and [10:39] unit. And inside we're going to create [10:41] all the tests. I'm not going to go these [10:44] tests and just explore them. Instead, [10:47] I'm going to simply delete all files [10:49] from unit [10:53] as well as from feature because we're [10:55] going to create them manually. Now, [10:57] let's bring up terminal. And if we [10:59] execute vendor being test right now, we [11:02] see no tests found or I'm going to stick [11:04] with PHP artisan test. But before I [11:07] execute this, I'm going to type PHP [11:08] artisan test list, which is going to [11:11] give us actually column column list. [11:15] There are no comments defined in the [11:17] test name space. So there's nothing. [11:19] However, if we execute PHP artisan make [11:23] colon test dash help, then we are going [11:28] to see several flags associated to this. [11:31] So if we want to create new test file we [11:34] have to execute make test then provide [11:36] the test name and additionally we have [11:39] options to create a unit test to create [11:42] a past test which is the default one to [11:46] create PHP unit test and we're going to [11:50] talk about the differences between the [11:51] past and PHP unit test syntax right now. [11:55] So I'm going to show you both examples [11:57] and we have a couple of options. First [12:00] let's start with test and I'm going to [12:02] create new uh test. Let's call this test [12:07] or let's call this sample test. [12:11] Okay, sample test was created. We can [12:14] open this inside test folder feature by [12:17] default it created feature test and if [12:20] we click on this this is the feature [12:22] test. We are testing certain thing and [12:25] we are going to give it a proper [12:27] description. for example [12:30] should open homepage and it works. [12:36] So in this case we're accessing the [12:38] homepage of the application. We're [12:40] getting the response and we're checking [12:43] if that response has a status code 200. [12:47] It means our application is good and it [12:50] is working. All right. So we can bring a [12:52] terminal and execute PHP artisan test or [12:57] vendor being passed. Let me actually [12:59] execute PHP artisan test. Now we're [13:03] going to see that one simple test has [13:05] been passed. It should open homepage and [13:08] it works. [13:10] Now let's create unit test. PHP artisan [13:15] make test sample test but let's call [13:17] this d- let's give it flag d-unit. [13:21] We're going to hit the enter. Now we're [13:22] going to see that sample test is created [13:24] inside tests unit folder. Let's open and [13:28] have a look. Now this is the unit test. [13:30] The purpose of the unit test is to test [13:32] certain functionality. In most cases, it [13:35] should test some heavy logic, some [13:38] method or classes in your application. [13:41] It is not integration or feature test [13:43] meaning it is not making requests into [13:46] the application. So this is a very [13:48] simple test. It expects that certain [13:51] value to be true. All right. Now let's [13:54] bring terminal and execute PHP artisan [13:56] test again. Now we're going to see both [13:59] tests are passing. This is the unit [14:01] test. This is the feature test. Now this [14:05] in both cases we have saw we we have [14:07] seen the unit test as well as the [14:10] feature test but the syntax is pest. So [14:14] we are using pest. But as I mentioned, [14:16] pest is being built on on top of PHP [14:19] unit. So if you open composer JSON right [14:22] now, we're going to see that we have [14:24] pest here. We have past php and pest [14:27] plug-in level. Pest by the way is a [14:30] standalone package which is for testing [14:34] PHP applications but it's um is [14:37] developed by Nun Maduro which is the [14:39] core member of Laravel team and he also [14:43] developed this pest plugin Laravel and [14:45] pest works great with Laravel but the [14:48] thing is that if we open right now [14:51] composer lock file and if we search for [14:54] pestph php / test [14:59] we're going to find that here it is. So [15:02] this is the package installed and inside [15:05] its dependencies there's this PHP unit. [15:07] So internally it is still using PHP unit [15:11] just it is a wrapper with a nicer syntax [15:13] more intuitive in my opinion as well and [15:16] you write much less code as well. Now [15:18] let's bring up terminal and execute PHP [15:21] artisan make test. Let's call this make [15:24] test two uh two here and we're going to [15:28] provide a flag by the by default we know [15:30] that this is going to create past [15:32] feature test right now let's provide [15:36] flag called d-phpunit [15:39] now this is going to create this class [15:40] in a php unitit syntax if we open right [15:45] now this inside test folder feature [15:49] sample test 2 we're going to see that [15:53] This is a class. This is a typical [15:54] objectoriented way of testing your [15:57] applications. It is a class. It extends [15:59] this test case. Uh it u has this test [16:04] example. That example is the same text [16:08] you would give to your uh feature test [16:11] right here inside this test. So in our [16:14] case we should call this [16:18] it should underscore [16:22] open home age and it works. So this is [16:28] how we would name the method. Now let's [16:32] open terminal and execute PHP artisan [16:35] test and have a look. [16:38] Now all tests are running. Where is this [16:41] simple test two? Actually, it should end [16:44] with a test. That's why it wasn't [16:46] executed. We're going to call this [16:48] sample two test. [16:50] Sample two test. And we have to we have [16:53] to rename it. Apologies. Sample two [16:58] test. Okay, let's rename this. Now, it [17:02] was renamed as a file as well. [17:05] Now, let's execute PHP artisan test. And [17:08] we're going to see three tests passed. [17:10] And we see the these two tests are kind [17:13] of identical. So this is coming from [17:15] this sample two test and this is coming [17:17] from the sample test. This is the PHP [17:19] unit syntax. This is paste syntax. And [17:23] this is a typical uh unit test. Now [17:26] let's create new test in PHP unit syntax [17:30] which is going to be unit test. We're [17:33] going to execute PHP artisan make test. [17:35] Let's call this sample two test. Again [17:38] we want PHP unit syntax and we want unit [17:42] test not feature test anymore. Now it [17:45] created this file inside unit folder. [17:47] Again let's go inside unit folder. Open [17:50] this. And it looks very similar to what [17:53] we have already seen right here. Just [17:57] the assertion way how we check some [18:01] values is different. So in this case we [18:04] are using expectations API. we expect [18:07] something to be something but in this [18:09] case we are using in PHP unit we are [18:12] using assertions API we assert this [18:16] value to be true so uh PHP unit this one [18:21] only supports assertions API it doesn't [18:23] support expectations API but [18:28] past actually supports both so we can [18:30] take this code this assert true equals [18:33] true and we can put this here we can [18:35] comment this part and we can execute [18:38] tests again and we're going to see that [18:40] it works as well. So two unit tests are [18:42] executed, two feature tests are [18:44] executed. So that's why I personally [18:46] like writing test syntax because this is [18:49] the same as this and we don't need to [18:52] create the class and everything and also [18:54] naming is like very very inconvenient in [18:59] your PHPUnit way. So in this case we can [19:01] just write [19:03] it checks that the value is [19:09] is true. Right? So we save this execute [19:11] this but in this case we have to give it [19:13] a name with underscores. Let's talk a [19:15] little bit about configuration of our [19:18] tests or how we can change the database [19:21] or some environment variables [19:23] specifically for tests. All tests by [19:26] default are using PHP unit.xml XML file [19:30] if the test needs the database. So here [19:33] we have these environment variables the [19:37] ENV is testing we have this maintenance [19:39] driver to be file and all the other [19:41] things and the DB connection is SQLite [19:44] and the database is in a memory [19:46] database. So whenever we execute the [19:48] test it is going to do all the things [19:50] inside the memory create migrations and [19:52] everything and then uh the memory is [19:54] gone basically once the test is [19:56] finished. If we comment out this part [20:00] and execute our tests and our tests are [20:02] using database those tests will be [20:05] executed on our primary database what we [20:08] have written inside file. [20:12] If we don't want that we also have [20:14] ability to create env.esting [20:18] file exclusively for the environment [20:21] variables that is specific for testing. [20:24] So in this case we can delete everything [20:26] from here let's say and we can only [20:28] leave DB connection related information. [20:32] We can specify the database connection [20:34] to be SQLite uh and database [20:38] DB database to be in memory as well. So [20:41] we also have that flexibility or if you [20:44] want to override a specific environment [20:47] variable from the default in you can do [20:50] this from here as well. Let's say you [20:52] want to change the mailer to be log [20:55] instead of some SMTP. So all that is [20:58] possible through this env.esting [21:00] file from this phpunit.xml [21:04] file. We also have ability to customize [21:07] the directories for unit tests as well [21:09] as for feature test. As you can see by [21:12] default we have the unit tests are [21:14] created inside test unit and feature [21:17] tests are inside tests feature. So you [21:19] can customize this if you want. [21:22] Now let's close these two files and [21:26] let's go inside. Actually um I'm going [21:29] to close this. I'm going to delete this [21:30] yen testing as well because I prefer it [21:32] to be using uh the settings what I have [21:35] defined right here and I'm going to [21:36] uncomment this part as well. Okay. Now [21:39] let's go inside test folder and open [21:42] pest.php. [21:44] So this is the main configuration file [21:47] for pest. From here we can decide in [21:50] which browser by default the test should [21:52] be run, what configuration options it [21:55] should have by default and so on. So if [21:58] you have a look right here, all tests [21:59] are extending tests test case and also [22:04] it uses refresh database class every [22:08] time which means that every time we [22:10] execute the test it is going to just uh [22:14] refresh the entire database drop create [22:16] migrations and then it is going to run [22:19] them inside features. Okay, all tests [22:22] are basically all feature tests will be [22:24] inside feature target so-called this as [22:27] an example here we have extended [22:30] expectations and added a new [22:32] expectations method to be one. So using [22:36] this way we have ability to define [22:38] custom expectations here as an example [22:42] let's write I'm going to actually paste [22:44] this I think it's going to be easier and [22:46] I will explain this. So to be uid as an [22:50] example so we are defining new [22:52] expectations method where this value [22:56] needs to be needs to match the UID and [23:00] that's it. So whatever is returned from [23:02] here true or false it's going to be [23:05] tested and we can use this to be UUID [23:08] inside our tests. Additionally I'm going [23:11] to do this right here. We have ability [23:13] to customize certain things globally [23:16] such such as I'm going to call test [23:20] browser [23:22] and let's say that we want to run this [23:24] um always in Firefox. So we can do this [23:27] in the following way. Of course, we have [23:29] other browsers such as in Safari, in [23:32] Chrome and so on. We also have ability [23:35] to customize the global timeout. By [23:38] default, test is waiting for several [23:41] seconds, I think 5 seconds until the [23:44] element gets visible in the browser to [23:47] decide if the test is failed or not. And [23:50] we have flexibility for this. We can [23:53] provide different number here. Let's say [23:54] 10,000 milliseconds, 10 seconds. And [23:58] based on that now pest is going to wait [24:00] a little bit more. We also have ability [24:03] to provide a global user agent to all [24:06] tests what we execute right here. And I [24:10] think that might be helpful in certain [24:12] cases. Now I'm going to delete all [24:14] feature tests and sample to unit test as [24:17] well. And I'm going to leave only this [24:19] sample test and we're going to [24:22] understand how we're going to work with [24:24] expectations API and what expectations [24:27] are available. As I mentioned, test [24:31] replaces the assertions API with [24:34] expectations. Uh it doesn't actually [24:37] replace assertions is still there. it [24:40] still works but expectations API is more [24:44] um more flexible and more readable in my [24:47] opinion as well. I'm going to remove [24:49] this assert true okay expect something [24:52] that something can be an object can be [24:54] number can be string it can be anything [24:56] so let's specify expect five as an [24:59] example and then it gives us the object [25:01] which has a lot of a lot of functions [25:05] like expect five to be something as an [25:09] example to be five so we expect some [25:11] number and that number might be received [25:13] from database from some APIs let's say [25:17] that is a number which um our math class [25:21] gave us and we expect that number to be [25:24] 3.14. Let's say this is a pi, right? So [25:27] pi equals 3.14 and then we expect that [25:32] pi which is by the way received from [25:35] some math class to be 3.14. So once we [25:39] execute this, we're going to see that [25:40] test is passing. If for some reason that [25:44] function or class or whatever is [25:47] returning result from a function [25:51] or from a class. Okay. So if for some [25:55] reason that class doesn't work properly [25:57] and returns something else then our test [26:00] will fail. Right? So this to be um [26:04] something is just one example but we [26:06] have a lot of a lot of expectations and [26:10] I'm not going to go through all of them [26:12] but we have some of the most common ones [26:15] such as to be string as an example. So [26:18] we expect as an example here zura to be [26:22] string we can specify that zura must be [26:24] string it must contain and we can chain [26:27] also to contain uh z inside there. So [26:31] let's remove this pi from here. [26:35] Now if we execute the following test, [26:36] we're going to see that it is passing [26:39] and we have two assertions as well. One [26:41] assertion is that it needs to be string [26:44] and second assertion is that it should [26:46] contain Z inside. I'm going to open test [26:50] expectations [26:51] um documentation page and here we're [26:53] going to see everything that is [26:55] possible. All right. So if we scroll [26:57] down below, we have this 2B to be array [27:00] to be between and I'm not going to read [27:02] all of them obviously, but I encourage [27:04] you to have a look and understand what [27:07] expectations are available because that [27:10] expectations define how easily you can [27:13] write tests. As an example, you might [27:15] get an array and you expect that array [27:17] to have exactly three elements inside [27:18] there to have count three. And we also [27:21] can negate our um our expectation. As an [27:26] example, we can expect Zura to be string [27:31] to contain Z and then we can chain not [27:35] to contain lowerase Z as an example. We [27:39] expect it to be uppercase C. So if we [27:40] execute this now, we're going to see [27:42] test is passing. And now we have three [27:44] assertions. We can also chain uh to [27:48] because we already use not here. We [27:51] don't need not anymore. So we negated [27:54] the entire uh operator entire [27:56] expectations and then to uh to be [28:00] integer as an example. So we expect that [28:03] not to contain Z and not to be integer. [28:06] If we execute this oh actually um it's [28:09] my bad. So we need this not to contain [28:13] and not to be integer. Now we see four [28:16] assertions are passing. We need to use [28:18] this not before every um every [28:22] assertion. Now let's see another [28:24] example. I'm going to use expect and [28:27] provide array here. Let's say we have [28:29] three elements inside array one, [28:32] two and three. And then we expect to be [28:37] array [28:39] that's one assertion. Second is that it [28:42] to have count of three. And I also want [28:46] to uh check that it should contain [28:49] element two. Now uh why do I see this [28:56] multiple expectations can be changed to [28:58] changed together. [29:01] That's page for sure thing. [29:04] Okay. Expect and oh okay. So that's [29:07] that's an syntactic sugar on top of what [29:10] I just shown to you. It uh combine these [29:13] things. Actually I prefer to have them [29:15] splitted because this is one thing [29:16] testing strings. This is one thing [29:18] testing array. All right. So let's bring [29:20] up terminal. Execute this. Now we see [29:22] seven assertions. The test is passing. [29:25] Now let me show you another example. I'm [29:27] going to use expect and provide one two [29:29] and three. Then we can again use some [29:32] expectation method such as to have count [29:34] three. But that's not the main thing. So [29:36] then I'm going to call sequence on that. [29:39] And when I call sequence, we have [29:41] ability to provide callback functions [29:43] for each value of this array. So I'm [29:47] going to use fn the arrow function [29:50] basically right here. And then the value [29:52] inside is already an instance of [29:56] expectations API. So in this case on the [30:00] value we can directly call to be integer [30:03] as an example. So if we save this and we [30:05] execute that we're going to see that the [30:07] test is passing and we have 11 [30:10] assertions right now. Why do we have 11? [30:13] Let's count this 1 2 3 4 5 6 7 8 and [30:19] then this function is executed three [30:21] times for each element of the array. [30:24] That's the thing. If I remove one [30:26] element from this array and re-execute [30:29] that [30:30] uh what's wrong? Oh, this this is what's [30:34] wrong. We expected to have three [30:35] elements inside but and now we have two [30:38] elements inside. Actually, let's let's [30:40] remove this part. Not necessary. Okay, [30:42] now we have nine assertions. Now, let's [30:46] return this back into three. And we are [30:48] going to have now 10 assertions. [30:51] We had 11 previously because we had this [30:54] to have count as well. But the thing is [30:56] that we have ability to provide multiple [30:59] callback functions here. Um and those [31:01] callback functions will be iterated. So [31:04] in this case if we provide three [31:06] callback functions we can define to be [31:10] one. The first callback function the [31:12] value inside the first callback function [31:14] to be one. uh the value inside the [31:17] second callback function to be two and [31:19] the value inside the third callback [31:21] function to be three. So if we execute [31:23] this all tests are passing. That's [31:25] because those callback functions are [31:27] executed sequentially on these elements. [31:31] All right, I hope that makes sense. [31:34] Now let's create a user instance which [31:38] is going to be new user and we can [31:40] provide um for example first underscore [31:44] name to visura okay I'm going to leave [31:46] only first name we are not actually [31:48] creating the user in the database but I [31:51] want to show you using expect providing [31:55] user and then we can use the method to [31:58] be instance of in the user class. So in [32:02] the following way we can replace this [32:04] qualifier using import. In the following [32:07] test we can test in the following [32:08] expectations. We can test if the user is [32:11] an instance of a specific class. If we [32:14] execute this we're going to see that the [32:15] test is passing. We also have a method [32:19] expect something as an example. Uh let's [32:22] replace this into an array and provide [32:24] user here to contain only instances of [32:29] if we have a list of users providing [32:32] here then we can provide user class here [32:37] using the following assertions we can [32:39] test using the following expectations we [32:42] can test if the following array only [32:44] contains instances of the given class. [32:47] That is al also helpful one. So again, I [32:50] encourage you to have a look at the [32:51] expectations API and just read uh them. [32:55] So this is my homework to you. Just read [32:58] what expectations are available. Uh and [33:02] whenever you need to write a real test, [33:05] unit test or um some assertion, some [33:07] feature test, then you know which one [33:09] you can use. Now I'm going to create one [33:12] feature test as well and let's see a [33:14] couple of um expectations on the feature [33:17] side as well. [33:19] um PHP artisan make test sample test but [33:24] this case um it's going to be a feature [33:27] test. Okay. So inside feature let's open [33:30] this. So we are using assertion here to [33:34] check if the response has the status of [33:38] 200. We can chain different assertions [33:41] here as an example assert cextable. So [33:45] we expect that when we open the [33:47] application the status code should be [33:49] 200 and there should be text allarl. Now [33:53] let's execute tests. PHP artisan test. [33:57] And look at this. Um where's this [33:59] feature test? Okay, it has the example [34:01] test. We should give it proper [34:02] descriptive name. Uh but the thing is [34:04] that it is passing two assertions. Uh 14 [34:07] assertions more specifically. So that's [34:10] a total number of assertions actually. [34:13] But we have two assertions here. And we [34:15] have a bunch of different assertions [34:17] here which I'm not going to um focus [34:19] right now. But step by step we are going [34:22] to get familiar all of them. Not all of [34:25] them but the most important ones. Now [34:27] let's write actual unit tests for my [34:31] service class. But now I'm going to [34:32] delete again this feature and unit [34:34] tests. And let's create a new unit test. [34:37] I'm going to call this PHP artisan make [34:39] test and let's call this price [34:41] calculator test. Let's hit the enter. [34:44] And now let me show this. Actually, [34:47] sorry. I should have created inside a [34:50] unit folder because we are um writing [34:52] unit test right now. I'm going to delete [34:54] this feature test. Now, if we go inside [34:56] app services and open this price [35:00] calculator class, it has several [35:02] methods. So, this one is apply [35:05] percentage discount. We pass the price, [35:08] we pass the percent and then we check if [35:11] the percent and the price doesn't fit in [35:14] the uh valid amounts numbers. Then we [35:17] throw this invalid discount percent um [35:21] exception otherwise we simply apply the [35:25] given percent as a discount on the given [35:27] price. Okay, we have second method apply [35:31] fixed discount. We give the price and [35:34] fixed amount and then we apply this [35:36] amount as a discount on the given price. [35:38] We also have added tax. We give the [35:40] price. We give the tax percent and we [35:43] add the tax percent on the following [35:46] price. And we have this final price [35:48] where we give the price. We have this uh [35:51] discount percent and tax percent as [35:54] well. And we calculate um first we apply [35:58] this discount and then we add tax. All [36:00] right. So we have these four methods and [36:03] I want to write now tests for all four [36:07] methods unit tests and remember unit [36:10] tests should not use HTTP should not use [36:14] any external services should not use a [36:17] database we are testing simply pure [36:20] logic of a class or a method okay so [36:22] this is the first test and let's give it [36:25] applies percentage discount now we're [36:28] going to create an instance of this [36:30] price calculator. So price calculator [36:32] equals new apps service price [36:34] calculator. Okay, perfect. Then we're [36:37] going to use expect price calculator [36:39] actually let's call this calculator to [36:41] make this simpler. Then on that [36:43] calculator we're going to call apply [36:45] percentage discount. We provide the [36:47] price as an example 100 and let's [36:49] provide the discount of 20%. So once [36:52] that discount is applied on price then [36:56] we expect it to be at. [37:00] All right let's bring up terminal and [37:03] execute PHP artisan test [37:07] and look at this applies percentage [37:09] discount. The test is failing uh because [37:13] that 80.0 [37:15] is identical to 80. So what we need to [37:18] do it returns a float number. So we have [37:22] to provide 80.0 here as well. Now if we [37:26] execute this now we see that test is [37:28] passing. So that's one example. Actually [37:30] we expected integer to be returned but [37:33] inside the method float was returned. [37:37] So that's one test. Now let's write a [37:40] second test. And by the way we have two [37:42] ways to write the test. one is using [37:46] this test method. And if we execute [37:49] this, we see here applies percentage [37:53] discount. But if we change this into it, [37:56] that's a syntactic sugar on top of that. [37:59] So then if we execute this now, we see [38:01] it applies percentage discount. So [38:04] that's just syntactic sugar on top of [38:07] the test method. So if you open this it, [38:11] we see that it is actually calling test [38:14] internally. So let's use it here and I'm [38:18] going to write second test it uh rounds [38:21] discounted rice provide a function here [38:24] then again we have to create calculator [38:27] equals new app services price calculator [38:31] and let's actually replace this uh into [38:34] import. So we have this import at the [38:36] top then create instances. Okay, now we [38:38] have the calculator. Now we expect on [38:41] calculator apply percent discount. Let's [38:45] give the price to be 100 and the percent [38:48] to be something like 12 [38:51] 345. [38:53] All right. And then the result we expect [38:56] it to be [38:59] um 87.66. [39:05] So why we expect this actually? So this [39:07] 12.345% [39:10] uh discounted on 100 means that we have [39:15] remaining [39:17] uh 80 [39:19] um 87 [39:22] uh 655 [39:24] right but because we apply rounding this [39:27] is what we are testing. If we go in the [39:29] price calculator, we see that on that [39:33] method, we have everything rounded with [39:35] precision two. So instead of 87.655, [39:41] we expect it to be 87.66 [39:44] just to test that the rounding works [39:47] successfully. All right. Now if you [39:49] bring up terminal and execute PHP test, [39:52] now we see two tests are uh passing. It [39:55] rounds discounted price. Now let's see [39:58] how we can groups similar tests into the [40:01] same group. We're going to take these [40:04] two tests which is related to applying [40:07] percentage discount. So I'm going to [40:10] take this and then I'm going to use the [40:11] method called describe and I'm going to [40:14] give it uh like price calculator dash [40:18] percentage discount. Okay. So then we [40:21] provide a callback function here and we [40:23] are writing all our tests inside that [40:26] callback function. I'm going to paste [40:28] them. We're going to save and execute [40:31] them. And now we see that this is the [40:34] group now. Price calculator percentage [40:36] discount it applies percentage discount [40:39] and in the same group we have it rounds [40:42] discounted price. So we have two tests [40:45] and now we can go ahead and write um [40:47] another test which is for the fixed [40:50] discount. I'm going to introduce another [40:53] describe here price calculator fixed [40:55] discount and let's create um several [40:59] tests first. Let me define those tests. [41:01] This is also a good practice to define [41:04] your tests first. So applies fixed [41:07] discount. So this is the one test we're [41:09] going to write. Second is going to be [41:12] eat um it [41:16] uh never returns [41:20] negative price. [41:22] Um [41:24] I don't want it to be skipped. The [41:26] autocomp completion is sometimes really [41:28] good but sometimes it is annoying when I [41:30] don't want when I want step by step to [41:32] introduce something. [41:35] Another test it throws [41:39] or negative fixed amount we need [41:43] function here and I think the three [41:46] tests are kind of enough. So we know [41:48] that inside this group we're going to [41:50] have those three uh tests. Hey, can you [41:54] do me a favor and pause this right now? [41:57] Hit the like button, subscribe if you [41:59] are not yet because I'm working on some [42:01] really interesting videos for you and [42:03] then continue the course. Thank you. [42:05] Now, if we bring up terminal and execute [42:07] this, we're going to see that these [42:09] tests don't perform any assertions. So, [42:12] we see this in yellow. This kind of good [42:14] indicator as well that we don't have [42:16] those tests implemented, right? So, [42:19] let's start with the first one. It [42:21] applies fixed discount. We're going to [42:23] create calculator here. [42:26] Then on that calculator, we're going to [42:28] call apply fixed discount with [42:31] 120. But we're going to wrap this inside [42:34] expect. [42:36] And then on that expect we're going to [42:38] call the result to be 80.0. [42:42] So we save this. We execute this. And we [42:45] see that this test has been passed. We [42:49] have other two tests where we don't have [42:51] anything implemented. Right now the next [42:54] one is to test that it never returns a [42:57] negative price. Right? So we're going to [43:00] provide we're going to create the [43:01] calculator then on expect we are going [43:04] to call calculator apply fixed discount [43:08] and in this case I'm going to provide [43:09] the value to be 50 and the percent to be [43:13] 100 right so and then we expect the [43:17] result [43:19] to be 0.00 0 0. So we don't expect the [43:22] result to become negative even though we [43:26] applied uh $100 discount dollar or 100 [43:31] amount discount on the 50 uh price. [43:35] Right? So let's bring up terminal [43:37] execute that and the test is actually [43:40] failing. Oh sorry not expect to be 2B [43:44] 0.00 0. Okay, let's execute this. And [43:47] now we see that this test is also [43:49] passing. And the last one we're going to [43:52] test that it throws for negative fixed [43:54] amount. So we create the calculator. [43:57] Then on that calculator [43:59] uh we are calling [44:02] expect [44:04] calculator apply fixed discount and we [44:08] provide minus 10 to be the discount [44:11] price and we expect [44:15] expect exception. So there are a couple [44:17] of ways. So we can um we can use the [44:21] method to throw. So we expect that it to [44:24] throw invalid argument exception. That's [44:27] the one way to do this. We're going to [44:29] execute this and we see the following [44:32] error. Well, this is the thing. Yeah, [44:34] that's a good point. So we the test [44:37] wasn't passed. Uh instead we saw an [44:40] exception. That's because this method [44:43] throw an exception and that happened in [44:45] the main thread where the test was [44:48] running. So in order to test this, we [44:51] need this error. We need to wrap this [44:53] inside the function. Now when we put [44:56] this inside the function, test will [44:58] check if it throws an exception and [45:01] catch it and properly gives us the [45:04] result. Now we see that uh it throws for [45:08] negative fixed amount. The test is [45:10] passing. So it throws the exception. So [45:12] if it throws the exception, you should [45:14] not use in the way like this. Instead, [45:18] we're going to put this inside a [45:20] function. Now, let's create another [45:21] group. And I'm going to give this a [45:23] challenge to you. So, we're going to [45:26] test um let's create the group first. [45:28] And we're going to test that it adds tax [45:32] and final price that it tests the final [45:36] price as well. Okay. First, we need [45:39] it [45:41] adds tax to the price. [45:48] Good. Second is that it rounds tax [45:52] price. [45:54] Rounds [45:56] taxed price. [46:00] And the third one is it calculates final [46:03] price with discount and tax. We can [46:08] provide additional text as well with [46:10] discount and tax. Now this is a [46:14] challenge to you. Please go ahead and [46:17] implement these three unit tests based [46:20] on this price calculator class. You can [46:23] get the price calculator source code [46:25] from the project um and you can find the [46:28] project link in the video description [46:30] and implement these three methods and [46:33] then come back and see my solution. [46:35] Okay, I hope you made it. Now let's see [46:38] how I'm going to do this. First [46:40] obviously we're going to create the U [46:42] calculator. Next on that calculator, [46:46] we're going to call method add tax and [46:49] we're going to wrap this inside expect. [46:52] We add 10% tax on top of that or we can [46:57] replace this into like 21% as well to [47:00] have that not that round number. And [47:03] then we expect the price to be 121. This [47:07] is the first test. In the next test, [47:10] again we're going to create the [47:11] calculator. then we expect um to add tax [47:16] on that calculator. So we take this [47:18] price to be 100 uh point one two three [47:24] whatever number here right then we add [47:27] tax percent to be 21%. In order to [47:30] decide what value we're going to expect [47:33] we have to do a little bit of [47:34] calculation on ourselves as well right [47:36] so let's bring up terminal and in the [47:38] calculator we're going to take this 100 [47:43] 100.12345. [47:45] We're going to multiply this on 21 [47:50] which is a tax percent and we're going [47:52] to divide this on 100 and this is going [47:56] to be the tax amount value. Right? Then [48:00] when we add this 21.025 [48:03] 025 [48:05] whatever is this we're going to plus [48:10] 100.12 [48:12] 3 4 and five. So finally we get [48:17] 12149. [48:21] So if we round this 12149 [48:28] into two uh two um precision then we [48:33] should expect here to be 15 [48:37] right so let's execute the test it [48:41] rounds tax price this is correct and the [48:44] last one is remaining the autocomp [48:46] completion gave us wrong value here so [48:49] that's why it's kind of annoying rank [48:51] sometimes but we calculate it ourselves [48:54] and we know what we are expecting that [48:57] the tax price should be rounded properly [49:00] and the final one is to calculate the [49:03] final price with discount and tax. Again [49:07] we're going to create the calculator. [49:09] Let me do this step by step. uh then let [49:12] me call expect on the calculator [49:16] to call final price and let's provide [49:20] 100 as a final price not the final as an [49:25] initial price. Then we provide 10 here [49:28] inside the discount percent and we [49:31] provide 21 as a tax percent. So we [49:34] expect the value to be something. So we [49:38] have to calculate this by ourselves and [49:40] then we decide what we expect. So if we [49:44] open again terminal [49:47] a little bit of calculator. So then we [49:50] take that 100 and we're going to [49:53] subtract the discounted price which is [49:56] going to be 10% and so finally we have [49:58] 90 remaining. Right? And then we're [50:01] going to add 21% [50:04] on 90. Right? So 90 plus 90 ultiplied on [50:10] 21 divided on 100. So that is uh 108.9. [50:18] So finally we expect [50:21] it to be [50:23] 108.9. [50:26] Now we save this [50:29] and execute PHP artisan test. And let's [50:33] have a look. [50:35] Oh, we have semicolon missing here. Not [50:38] a big deal. Okay, one test was failed. [50:43] Expect doesn't exist in double again. [50:46] That should be 2B. Not expect but 2B. [50:50] Okay, now all tests are passing. And [50:53] congratulations. Right now we have [50:55] written a real world scenario of unit [50:59] tests of our service class. And we have [51:01] written how many tests? We have eight [51:04] tests written in our project which is [51:06] pretty cool and um not as complex as you [51:09] might find in uh real projects uh large [51:12] projects but this is a good start and [51:15] good um good unit tests. This is brand [51:19] new Laravel React starter kit which we [51:21] have installed and it has the welcome [51:24] page, it has a login page and a register [51:27] page and we also have uh on the login [51:29] page forget password page as well. So [51:32] now we are going to write feature tests [51:35] and write test to make sure that the [51:38] homepage welcome page opens uh the login [51:41] register pages are opening and like we [51:45] see the information on those pages right [51:47] so let's open terminal [51:50] and I'm going to execute PHP artisan [51:54] make test and I'm going to call this [51:57] public pages test however we can put [52:01] this inside a subfolder by providing [52:04] something the subfolder name. So we can [52:07] call this um roads dot or not dot but [52:11] slashpublic pages test. We hit the [52:14] enter. Now we see this public pages test [52:17] is created inside roles folder. So we [52:21] can open this public pages test and we [52:24] see an example uh test right here. So [52:27] let me replace this with it and that [52:30] should be shows welcome page. By the [52:34] way, this is one way how we're going to [52:35] make request this get. But the second [52:37] way is to simply use get method in which [52:40] case we have to import the get function [52:43] as well from past lar. Okay. So we you [52:47] can choose whichever you want. I'm going [52:49] to stick with this get here. [52:52] Okay. So this shows the welcome page and [52:55] makes the status is correct. Let's bring [52:56] up the terminal and I'm going to execute [52:59] PHP artisan test and we're going to see [53:01] that this feature test is actually [53:03] passing. [53:05] Now let's create another test. It [53:10] uh shows the login page. All right. Now [53:15] in this case we're going to make request [53:18] response equals get login on the login [53:20] endpoint. We can save this into a [53:23] response variable and then on that [53:25] response variable we can call some [53:28] assertions or another way is to change [53:31] several methods. So we get the response [53:33] then we can check the status uh needs to [53:36] be 200. What else we expect on the [53:38] homepage? Let's open the browser. Click [53:42] on login and then we expect the login to [53:46] your account text to be also visible. Um [53:50] and that's it. Yeah. So then we can use [53:53] another assertion like assert C login to [53:57] your account. Optionally we have a lot [54:01] of assertions here. Assert and assert [54:04] accepted. Assert accepted um basically [54:07] checks a specific status code. It's a [54:10] 202 status code. So we are not we don't [54:13] need to cover all of them. But let's [54:15] have a look. Assert those are related to [54:18] status codes. Assert okay is again the [54:21] 200 status code [54:24] unauthorized assert cookie if the cookie [54:27] contains specific value. Assert C and [54:30] assert don't see. So we used assert C [54:33] and we expect the following text to be [54:35] displayed. Um and we can use assert [54:37] don't see as well such as um such as [54:41] welcome to Laravel or dashboard. So we [54:44] don't expect dashboard to be displayed [54:47] when we access to the login page. Let's [54:49] bring up the terminal and execute PHP [54:51] artisan test and let's have a look. [54:54] Login to your account. This is not [54:56] visible. So this is the thing. So this [54:58] is not browser testing. It is making it [55:01] is making curl request getting the HTML [55:05] and then inside that HTML it is [55:08] searching for the following um for the [55:11] following content. And I expect this is [55:15] not going to work as well. And I'm going [55:19] to explain why. [55:22] Okay. Yeah, this is not going to work. [55:24] And the reason is that we have inertia [55:28] laral inertia type of application. All [55:30] right. And the content is rendered [55:33] dynamically. So if we view the page [55:36] source, this is what um paste right now [55:41] sees when we make the following request. [55:44] This is it. This is inertia page and we [55:47] don't see login here. Log in do we see? [55:50] No, we don't see. That's the reason. And [55:53] we are using the login.tsx file which is [55:57] dynamically rendered in the browser. [56:00] That's why uh pest for's browser testing [56:03] is absolutely amazing and using browser [56:07] testing we will be able to test all this [56:09] functionality like the login and the [56:11] text is visible and the buttons are [56:13] working and the redirects are working [56:16] and everything. To get started with [56:18] browser testing, we have to install pest [56:21] plug-in browser as well as playright [56:24] because internally pest plug-in um 4 [56:27] version 4 for browser testing is using [56:30] playright. So we're going to copy the [56:32] following command bring up terminal [56:34] paste this and hit the enter. So at this [56:36] stage, look at this. It is downloading [56:38] Chromium uh browser and it is going to [56:41] actually test everything inside the real [56:44] browser which is uh open-source version [56:47] of Chrome. In my case, some of the [56:49] playright dependencies we're missing. So [56:52] I can execute the following command or I [56:55] can execute the following commands. [56:58] Okay, the dependencies have been [57:00] installed and if we go right now inside [57:02] this public pages test, we can replace [57:06] get [57:08] with visit. So we use visit to have a [57:12] look um and to open this in the browser. [57:14] Assert status function doesn't exist [57:16] after the visit method. So we have to [57:19] remove this. So we expect login to be [57:21] displayed and we expect dashboard text [57:24] not to be displayed as an example. We [57:26] can replace this with visit as well. So [57:29] the thing is that this one was a failing [57:33] because of the client side rendering. [57:37] Now let's bring up terminal and execute [57:38] PHP artisan test. The browser testing [57:41] needs a little bit more time. Uh yep [57:44] this is not good. So we don't have [57:46] assert status anymore. Instead we can [57:48] replace this assert C. We expect to see [57:51] Laravel. If we see Laravel we know this [57:53] is good. and 200 status code was [57:56] returned. Let's re-execute PHP artisan [57:59] test. [58:00] Now we see that the tests are actually [58:02] passed. It shows welcome page. It shows [58:05] the login page as well. So what we can [58:08] do is call debug right here. Then [58:12] execute this. And now it opened the [58:14] browser and it stopped at point of [58:18] debug. So it checked and saw that the [58:21] login was existed there and it then it [58:25] called the debug. So it paused open the [58:27] browser so that we can debug what is [58:29] happening right here in the following [58:31] screen. I can zoom in this part. So this [58:34] is it. Okay, we're going to close that. [58:37] Press any kick to continue. If you want [58:39] to see everything in a headed mode. So [58:42] we have ability to provide um d- headed [58:47] flag. [58:49] So the browser opens, test is completed, [58:51] it's closed, then it's open. So for [58:53] every test, browser is opened and we see [58:55] everything in the browser. However, it [58:57] is fast and we don't actually notice [58:59] what's going on. But that is browser [59:01] testing. Of course, it is slightly [59:04] slower than not non browser testing. the [59:07] typical testing which is available in [59:09] pest version 3. But the browser testing [59:12] gives you ability to test uh everything [59:15] actually in the browser like we are not [59:17] able to test if the login page even [59:19] opens in Laravel React version um of the [59:23] starter kit but we are able to do this [59:25] thing using browserbased testing. Now [59:28] let's write a test to see if login [59:31] actually works. So we're going to use uh [59:34] it tests [59:36] or um yeah it tests that login works. [59:43] Okay. So we need to first create the [59:46] user and then use that user for [59:49] authentication. user equals uh new not [59:53] new but the user factory create and we [59:58] can provide password here. [60:01] Password needs to be brypt of something [60:05] like password [60:07] one to three. Uh the user model needs to [60:10] be imported from app models user. That's [60:14] perfect. And now we can use that user's [60:16] email and the password is one to three [60:19] to actually type them in the browser. So [60:22] we're going to use visit right now [60:25] login. Actually we can do we can go even [60:27] step further and we can do like this [60:31] visit on slash [60:34] then we're going to uh click [60:38] we're going to click [60:41] on the button which is called login. So [60:43] if we access slash then we have the [60:46] following button. So we're clicking on [60:48] this and this is something this can be [60:51] another test. So we uh we tested that [60:54] when we access login URL we see this uh [60:57] login text right there and we don't see [60:59] dashboard. But another test inside here [61:02] can be that when we click on slash and [61:06] when we visit on slash then click on [61:09] login then we see the login text and we [61:13] can also check [61:15] assert path is /lo. So the URL was [61:21] changed into login. So that's also a [61:24] good test which shows the login page [61:26] that's for showing it right. So in our [61:29] case we can directly visit to login. We [61:32] know that now login page is opening. So [61:35] this proves that the login page opens [61:38] from homepage. Now when we visit login [61:42] we can type email to be the user's email [61:46] address. Whatever it is [61:49] email we can type the password to be [61:52] password one two three. And then we [61:55] press a login button. Let's make sure [61:57] that the button has the same text login [62:00] right here. And then we check that the [62:03] path needs to be dashboard right. So we [62:06] save this. Let's bring up terminal and [62:08] execute PHP artisan test. Let's remove [62:11] this headed. It needs two three seconds. [62:14] This error basically means that for just [62:16] created user two factor authentication [62:18] is enabled. And when we entered email [62:22] and password in the form and hit the [62:24] press button, it redirected us into the [62:28] two factor page. So if we put a debug [62:31] here and re-execute the same test, it [62:35] will open debugger. Look at this. So and [62:38] this is it. So now it expects us to [62:41] enter two factor authentication code and [62:44] the test was failed after some time. [62:46] Okay. So in order to fix that we have to [62:49] create the user without two factor [62:51] authentication. If you open user factory [62:55] uh right here inside the definition we [62:57] have all the fields defined and we have [62:58] two factor secret and two factory uh [63:01] recovery codes and confirmed at. So that [63:04] means that whenever we create the user [63:06] those two factor fields are set. So we [63:09] can actually override those fields for [63:12] that specific user creation. we provide [63:14] the password but we can also provide [63:19] these fields. So this is going to be [63:23] like this. So we provide null everywhere [63:27] and now if we re-execute this test now [63:30] the dashboard is visible and the test [63:32] was passed. So and just like this if we [63:34] want to test the two factor thing that's [63:36] a different story but right now we [63:38] tested that the if the two factor is not [63:40] authentic is not activated on the user [63:43] the user is created and the user is able [63:45] to visit the login page enter those [63:48] information click on login and the user [63:50] is redirected into dashboard I'm going [63:52] to remove this debug now besides this [63:54] type and press let's have a look what [63:57] other methods are available to interact [64:00] with elements so we have click we have [64:03] text. Uh this text basically method gets [64:06] the text of the element which matches [64:08] the given selector. So it simply returns [64:10] the text. So finally we're going to get [64:13] the text right here. This one returns [64:15] the attribute. Let's have a look. So we [64:17] have check to check the checkbox uh with [64:20] a specific value. [64:23] We have ability to check radio as well. [64:28] We have ability to attach a file to the [64:31] input type file. The press basically is [64:33] the same as click. We also have [64:36] possibility to select something in a [64:38] selector. So in the country selector, we [64:41] can provide us if it supports multiple [64:44] values. We can do this in the following [64:45] way. We have we have ability to submit [64:49] the page. If the page has form inside [64:52] there, we have ability to get the value. [64:54] So we have a lot of things basically [64:56] what we need to interact with the uh [64:58] element. So we also have ability to get [65:01] the current URL of the page to wait for [65:03] some time. Um and u this is pretty cool [65:08] also regarding this debugging test we [65:11] have already seen debug method. Let's [65:14] have a look at this screenshot method [65:16] which has two properties to take the [65:18] full page screenshot and to provide the [65:21] file name as well. So I'm going to open [65:24] now and let's provide screenshot here [65:27] and I'm going to just leave this with [65:30] default values. So we can execute this. [65:33] It's going to take the screenshot and [65:36] save it. [65:38] Okay, the screenshots are typically [65:41] inside tests browser screenshot and [65:45] let's click on this and this is it. So [65:47] it gave us the screenshot that the login [65:49] works. Optionally we can provide the [65:51] file name custom file name or the full [65:54] page screenshot can be taken as well or [65:56] we also have this screenshot element u [66:00] method as well and it's going to take [66:02] the screenshot of a specific element not [66:03] the entire page we also have ability to [66:06] open tinker at specific point of the [66:09] code if the code is failing at some time [66:11] because something is not correct in the [66:13] database we can open the tinker at that [66:15] point and observe the database and see [66:18] what's there and uh we covered that we [66:21] can open this in a headed mode as well [66:24] if we want. So [66:27] now let's have a look at some of the [66:29] assertions that is available inside [66:31] browsers API browser testing API. So we [66:35] have ability to test if the page title [66:37] is specific thing or title contains [66:40] something. We can check if the checkbox [66:42] is checked or not. We can see if the [66:46] radio is selected or not. We can test if [66:49] the element has a specific attribute or [66:52] if the attribute is missing. We can see [66:54] if the button is enabled um or not. We [66:58] can see if the URL is a specific thing [67:00] or not. So we have a lot of things [67:02] basically what you think. We can check [67:04] how many items basically of a specific [67:07] selector we have in the DOM. Just have a [67:10] look and understand what is available [67:13] here. So we can also check if the [67:15] screenshot matches previously taken [67:17] screenshots which which is insane. So [67:20] that's that one is uh like a really [67:22] good. Now let's test if our application [67:25] opens this cyber menu on mobile version. [67:29] So we're going to open [67:33] and it [67:35] uh tests that mobile menu works. [67:42] Okay, great. First, what we're going to [67:44] do is create the user. So, I'm going to [67:47] copy the following part and then we're [67:49] going to login into the dashboard as [67:51] well. So, I'm going to copy the [67:52] following part completely. Paste here. [67:55] So, once we are inside the dashboard, we [67:56] can remove this screenshot part. I don't [67:58] want the screenshot to be taken. So, [68:00] once we are inside the dashboard already [68:04] and we checked, then we're going to [68:06] click on the following element. That [68:09] element has data slot sidebar sidebar [68:14] trigger. So we're going to select an [68:17] element based on this. [68:20] Let's search for press [68:25] data slot equals sidebar toggle. [68:30] Cool. So we press on this and then we [68:33] expect something to be visible such as [68:37] we expect platform to be visible or we [68:39] can even trust the following text [68:43] Laravel starter kit. This is a text I [68:45] think Laravel starter kit to be visible [68:51] visible assert visible Laravel starter [68:54] kit and finally let's call debug to see [68:58] what happens in the browser. [69:03] Now we have been authenticated actually [69:06] uh my bad. We should execute this on [69:08] mobile. That's the thing. So we're going [69:10] to call on then we're going to call [69:13] mobile. Now we save and execute that. [69:18] Now this was executed on mobile but it [69:23] does not press on it. So I guess the [69:26] test is going to fail. [69:29] Okay, it was failed because [69:35] that data slot sidebar toggle press did [69:38] not work because this is called sidebar [69:41] trigger not sidebar toggle. Let's [69:43] reexecute that. Now look at this. So [69:45] this opened and laral starter key text [69:48] is visible. So probably the test is [69:50] going to pass. Here we go. Perfect. Now [69:53] let's write another test in which case [69:55] we will be accessing several pages on [69:58] the website. I'm going to log out from [70:00] here. We're going to access this [70:02] homepage login and register pages as [70:05] well. And we're going to check that [70:07] there are no errors in the console and [70:10] there are no JavaScript errors and no [70:12] console locks basically. So let's write [70:15] the test. It [70:17] tests that there [70:21] are no console logs and errors. [70:28] Great. So let's write this function. Now [70:32] we're going to access to several pages [70:34] in the following way. We use visit but [70:38] we're providing array right here. So we [70:40] access slash [70:42] then we access uh plugin then we access [70:47] register. Okay three pages are enough. [70:50] So then we can destructure that and take [70:53] it as home [70:57] login and register. [71:00] All right. So then individually we can [71:03] write some assertions on home login and [71:06] register. On home we can see assert [71:12] title to be Laravel starter kit or [71:15] whatever is the title uh for the [71:17] homepage. It is welcome dash laravel. [71:22] Welcome dash space d- laravel. This [71:26] should be the title for the home. Let's [71:28] duplicate these two times. For login [71:32] there should be log in Laravel [71:36] log in Laravel and for registration it [71:40] should be register Laravel register [71:44] Laravel also on home as well as on [71:51] basically uh let me do the following. [71:56] I'm going to assign this into pages. [71:58] Then I'm going to dstructure them from [72:01] pages. And then on all pages I can do [72:06] the following. Where's the register? [72:07] Register is missing here. Okay. On all [72:10] pages I can call assert no console logs [72:15] plus assert [72:18] no JavaScript errors. Let's bring up [72:22] terminal and execute PHP artisan test. [72:25] Actually I want to debug this as well. [72:28] So I love debugging and seeing what's [72:31] going on. So we can execute PHP and [72:34] test. Let's see it is visiting multiple [72:38] pages. So that's one one test by the [72:41] way. And we have to remove debug here. [72:44] We don't want debug here. Okay. So we [72:47] can execute only that specific test if [72:50] we provide only here. So we we need only [72:54] the this test right now. PHP artisan uh [72:58] test. Okay, let's see what's the what's [73:00] the issue. Oh, [73:04] semicolon is missing. Okay, let's [73:07] execute that. Now, it opened multiple [73:10] windows at the same time and it is [73:13] checking the console logs internally and [73:17] it expects us to press any key and [73:20] something was failed. Actually it was [73:23] failed in debug mode but it it is [73:25] passing in the um headless mode. If we [73:28] have a look at the browser testing [73:30] documentation we also have ability to [73:33] configure custom user agent when we are [73:35] accessing the page. We also have [73:37] possibility to spec to provide a [73:39] specific a geo location to configure [73:42] local or to configure the time zone. So [73:44] all that is possible and again I [73:48] encourage you to have a look at the [73:49] browser testing documentation because [73:51] you're going to learn a lot of things [73:52] there. Right now we are only running the [73:55] following test which I'm going to [73:56] uncomment and I'm going to execute PHP [73:59] test but we also have ability to provide [74:01] d- parallel. This is going to execute [74:05] our tests in parallel mode and this is [74:08] going to significantly speed up the [74:11] process of testing. If you have hundreds [74:13] of tests in your application, we have [74:16] several tests in this file and the file [74:18] is called public pages test. It's not [74:21] logical. So I'm going to move some of [74:22] the tests from here into a separate [74:25] file. [74:27] We are going to take this test that [74:29] login works. Test that mobile menu works [74:32] as well. And the rest can be left here. [74:37] So this is for guest users. This as well [74:40] and this as well. So I'm going to bring [74:41] up terminal and execute PHP artisan make [74:45] test. We can call this out pages [74:49] test. [74:51] We hit the enter out pages test. We open [74:55] that. I'm going to replace this with my [74:58] tests [75:00] and we need to make sure that user is [75:04] imported dash parallel. This is going to [75:08] execute our tests in parallel mode and [75:11] this is going to significantly speed up [75:14] the process of testing if you have [75:16] hundreds of tests in your application. [75:19] We have several tests in this file and [75:21] the file is called public pages test. [75:24] It's not logical. So I'm going to move [75:25] some of the tests from here into a [75:28] separate file. [75:30] We are going to take this test that [75:32] login works. test that mobile menu works [75:36] as well and the rest can be left here. [75:40] So this is for guest users this as well [75:43] and this as well. So I'm going to bring [75:45] up terminal and execute PHP artisan make [75:48] test. We can call this out pages [75:53] test. [75:54] We hit the enter out pages test. We open [75:58] that. I'm going to replace this with my [76:01] tests. [76:04] And we need to make sure that user is [76:07] imported. If we have specific cases when [76:10] we want to test databases, this is the [76:13] lesson for you. So if you want to always [76:16] refresh the database, drop migrations, [76:19] reapply them, then you're going to use [76:21] this refresh database class in your test [76:24] configuration file, which by default is [76:27] there. If you want your test to run on a [76:30] MySQL database if you have something [76:32] something specific which SQLite for [76:36] which SQLite is not enough then you can [76:38] create.esting [76:40] file and configure the MySQL database [76:43] from here. Okay. [76:45] So if you want to create new records [76:48] we're going to use user factory create [76:50] inside our test. If you want to create [76:52] multiple uh records then we can do in [76:55] the following way. If we want to execute [76:57] the entire seed file, then we can call [77:00] this seed in our test. That's just for [77:04] the information. [77:06] Now let's see how we can test the model [77:08] attributes. Let's say we have the user [77:11] which has first and last and we provide [77:14] first and last and then the user has [77:17] composed column which is the combination [77:19] of first and last and we write the [77:22] following thing. expect you full name to [77:26] be surah in my case. So in the following [77:30] way we can test if the attributes match [77:32] to the specific thing. Another example [77:35] is to test the relationships. Let's say [77:37] we have the user model which has a [77:39] relation to posts model and we have post [77:42] model and migration defined in our [77:44] database. So using factories we create [77:47] user which has posts three. So this is [77:50] going to create the user with three [77:52] posts [77:54] and if everything is set up properly and [77:57] this is exactly what we are testing here [78:00] then user posts need to have three [78:03] count. So there will be exactly three [78:06] posts for this user and because we are [78:08] refreshing the database every time when [78:09] we execute the test there will not be [78:12] any additional posts in the database and [78:14] the user and its posts are created right [78:17] here. Okay, the next thing is to test if [78:21] the post belongs to a specific user. In [78:23] this case, we do it in a reverse order. [78:25] So we create post but we create this for [78:28] a user. So the user will also be [78:31] created. And to test the relation if it [78:34] works or not. Then from the post we [78:37] access user property and check that the [78:39] user not to be null. [78:43] Okay. If we want to test the query [78:45] scopes, let's say we have defined a [78:47] published scope on the uh post model, [78:52] then we create two posts. First, we [78:55] create with a published property true. [78:57] Second, we create with published [78:59] property false. And when we select all [79:03] posts with published scope, we expect [79:06] the count to be one. And in the [79:08] following way we will be able to test [79:10] any scopes what you have defined for [79:13] your project. You create records for the [79:15] to satisfy the scope criteria and to not [79:19] satisfy the scope criteria as well. [79:23] Okay. Next if we want to test the CRUD [79:26] for a specific model. So we create the [79:29] product actually not create but instead [79:32] we take the array of this product. So [79:35] user factory create actually creates [79:37] that in the database. But user factory [79:39] make simply returns the model. It [79:43] doesn't save it into the database. So [79:45] then we call this product to array to [79:48] convert this into an associated array [79:49] and we call product create to create [79:52] that in the database. Why we do this? We [79:54] don't use a directly factory create [79:57] because we want to test if the uh [80:00] fillable properties are set correctly on [80:05] the product model. So instead of product [80:07] factory create we use product create [80:10] which is the way we would typically use [80:15] in our uh controller where we get this [80:18] validated data and we pass this inside [80:21] create method. So in this case if some [80:23] of the fillable properties are not set [80:26] up on the product model or something is [80:29] wrong that product create will not be [80:32] will not be working and thus the expect [80:35] product count to be one will fail as [80:38] well. So in the following way we take [80:40] this product which we just created we [80:42] perform update for the price and we then [80:45] refresh that product. So we reselect [80:48] that product from the database and we [80:51] expect that the new product's price to [80:54] be 200. And finally we delete the [80:57] product and we expect the product count [81:00] in the database to be zero. Okay. Next [81:04] let's uh test soft deletes as well. If [81:07] we have a post which has deleted at [81:09] column in the database and we want to [81:11] test if it's soft deleted properly. We [81:14] create a post. We call soft we call [81:17] delete on the post and then we use the [81:20] following assertion assert soft deleted [81:24] and we provide the table name and we [81:26] provided the ID of the post we just [81:30] deleted and then assert soft deleted [81:33] will check that the post exists in the [81:34] database and it has this deleted at [81:37] column set after which we call product [81:40] restore which means the deleted at [81:43] becomes null again in the database and [81:45] Then when we reselect this post with a [81:47] fresh method, we ch we check that [81:50] deleted at to be null. Okay, perfect. [81:53] Next is to um next and I this is the [81:57] final thing to check several assertions [81:59] in the database. So we can use assert [82:02] database has or assert database missing [82:05] and provide the table name and the [82:08] parameters based on which we're going to [82:10] check if there is any user in the [82:12] database which has email test [82:14] atample.com and the second test will [82:16] check if that there must be orders uh [82:21] with canceled status missing in the [82:23] database. So this one will pass if user [82:27] exists with email testample.com. This [82:30] one will pass if there are no orders [82:33] with status canceled in the database. [82:35] Finally, I'm going to give you a couple [82:36] of recommendations. If you are working [82:39] with a database and testing some models [82:41] and relations, I recommend to use [82:44] refresh database to always start with a [82:47] clean data and use factories everywhere. [82:50] um and test the behavior such as scopes, [82:53] relationships and not eloquent internals [82:57] and write small and atomic tests as [83:00] well. Now let's talk about hooks local [83:03] hooks as well as global hooks which can [83:06] be executed once per file or once [83:09] globally or once per test. If we have a [83:13] look here, we have two tests and in both [83:17] cases we are creating a user without [83:20] two-actor authentication. First of all, [83:22] if we open this two-factor, we have this [83:24] without two factor method. So I can take [83:26] this I can remove these three lines and [83:30] I can use the method here. Next I am [83:34] going to define before [83:37] before each. before each is executed [83:40] before every test in this particular [83:44] file. So in this case I'm going to [83:45] create this user equals I'm going to cut [83:49] this out and paste here. So I'm creating [83:52] a user assigning into this user and now [83:55] I can use this user here. Scroll down [83:59] below. I can remove this part completely [84:01] and I can use this user here as well. [84:05] Now let's bring terminal and execute PHP [84:08] artisan test [84:11] and we're going to see [84:13] the out pages test are passing. Perfect. [84:17] We also have before all. Before all is [84:22] executed once in this file and before [84:24] each is executed for each test file. We [84:28] can also add after each and after all in [84:32] the exact same way. However, we also [84:35] have global hooks. If we open this past [84:37] configuration file, we can use past [84:39] function here. And then we can add [84:42] before each. [84:44] We can also add before all, [84:49] after each, [84:52] and after all. [84:55] And the thing is that before each is [84:57] executed for every test inside our [85:00] project. before all is executed for [85:02] every test file in our project. After [85:05] each and after all works in the same way [85:07] and if we have a global hook defined [85:09] here and local hook defined here first [85:12] global before each is executed this one [85:16] then local before each in the same way [85:18] before all works but first [85:21] after each local after each is executed [85:25] and then the global one and in the same [85:27] way after all works. first local after [85:30] all is executed then the global one and [85:33] those are handy callback functions hooks [85:35] if you want to execute specific code [85:37] such as creating a user or deleting a [85:40] directory after all as an example you [85:43] can delete the directory from um from [85:45] the file system uh before all you can [85:48] execute a specific artisan command to [85:51] run migrations or to create some data in [85:54] the database so you can do this inside [85:58] these hooks Now I'm going to talk about [86:00] data sets and data providers. So if we [86:04] want to execute test with a specific [86:06] data set, we can use method called with [86:09] right here and provide data. And we have [86:11] different variations how we're going to [86:13] provide the data. I'm going to provide [86:16] an array here. Then inside an array I'm [86:18] going to provide three elements one, [86:20] two, and three. [86:22] another set of numbers like five, [86:27] seven and 12 and we can continue like [86:30] this but that's enough I think. Now the [86:32] thing is that pest will take each [86:36] element right here in the given uh array [86:39] global array this array. So this will be [86:42] taken destructured and given into this [86:45] function as three variables like a b and [86:51] c. So a is going to be one, b is going [86:55] to be two, c is going to be three. then [86:57] this function will be executed second [86:59] time for the second element of this [87:01] array. So what we can do is write simple [87:04] expect [87:06] expect a plus [87:09] a + b [87:13] to be c just like this. So let's execute [87:19] only this test. [87:22] PHP artisan [87:26] test. [87:27] Now look at this. The two tests were [87:30] executed basically in the following [87:32] function because this is executed two [87:34] times for a different data set. So let [87:37] me duplicate this. Actually I'm going to [87:39] create new one. So let's say we want to [87:42] validate multiple emails which we have [87:45] taken from a file or from a database. [87:48] All right. So I'm going to use test [87:51] validates [87:54] emails. So then we have a function here [87:57] and then I'm going to provide the width [88:00] pass an array and then we can take [88:05] um let's give it some key like this [88:08] going to be anything like email one okay [88:12] then we can have an array here this is [88:15] going to be zuracample.com [88:18] so we can have email two as well and [88:22] email two let's say is not a valid [88:25] email. It is like this. So we are [88:27] providing these two elements inside with [88:30] then right here we're accepting an [88:32] email. All right. So test will um take [88:37] uh actually let's dump let's dd what is [88:40] the email here. So we're going to better [88:43] understand how it works. So only [88:47] we're going to remove this and execute [88:50] that. And now look at this. Exactly as [88:52] we expect. So pest is smart enough to [88:54] take this value when we are expecting [88:57] right here only one thing take this [88:59] value and take its first element and [89:02] give it as a first variable. So if we [89:04] have a different elements in the [89:06] following array they will be given as [89:08] second and third variables right here. [89:11] So after this we can write a simple [89:13] filter [89:15] var like the basic PHP one. We take the [89:18] email filter validate email not to be [89:21] false. Now if we execute this we expect [89:25] uh it to be failed for something for [89:28] email two because this is not a valid [89:30] email. However if we make this [89:33] valid email [89:35] then we reexecute this we're going to [89:37] see that both are passing. So we also [89:40] have possibility to define data set with [89:42] a name. As an example I'm going to use [89:46] data set users and provide those two [89:49] users. Now I can define test [89:55] has a valid string as an example [90:02] function. [90:04] We're going to execute the following [90:06] test with users data set. And right here [90:10] we're going to expect name. That name [90:12] will be Zura for the first execution, [90:15] John for the second execution. And then [90:16] we can write expect name to be string [90:20] and we can execute only [90:23] this test. [90:27] Actually the other one was executed but [90:29] we can see that has a valid string Zura [90:32] and John. So this also works. We can [90:36] also have uh inside this data set [90:39] functions callback functions. I'm going [90:40] to define another data set [90:43] uh in this case that will be numbers [90:48] and provide an array but I'm going to [90:50] define a callback functions like fn ar [90:53] error functions and we're taking a [90:55] random integer or let's simply use rend [90:59] uh from 1 to 10 this is the first one I [91:03] have syntax error duplicate this and [91:06] let's take second one from 10 to 20. So [91:11] we have defined this data set. Then we [91:13] use test [91:16] to test if this has a valid number. We [91:18] are executing the following test with [91:21] numbers data set. [91:24] And we expect each number received right [91:26] here to be an integer. So I'm going to [91:31] skip this one and skip this one. And I'm [91:35] going to execute only this test. Let's [91:41] execute it. [91:44] What happened? [91:48] Now look at this. Uh has a valid number [91:51] with with the closure. So this is a [91:53] closure. Um but the thing is that this [91:55] is tested to be a valid number. And to [91:57] show this I'm going to use dump here. [92:00] Dump number. We save and execute that. [92:04] And now look at this. One and 17. So [92:06] those are random numbers uh generated by [92:10] the following function. Now let's see an [92:12] example of snapshot testing and [92:15] screenshot testing. That's interesting [92:17] one. So we have one test which snapshots [92:21] a homepage and this works in a very [92:24] straightforward way. We make request we [92:26] get the response and then on that [92:29] response content we expect to match [92:32] snapshot. So if we execute this very [92:35] first time we expect the following [92:36] result. Snapshot created at the [92:40] following location. There is no previous [92:43] snapshot to compare with and to test [92:46] this but it created snapshot right now [92:49] when we executed this. And if we go [92:51] inside test folder pass snapshots [92:54] feature snapshot test. We can open this. [92:56] And here it contains the entire response [92:59] content what was returned. So this is [93:01] the current snapshot. Right now this is [93:04] a Laravel React single page application. [93:07] That's why this is not a typical HTML [93:10] serverside response content. Okay. But [93:13] if you have a typical like a blog or [93:16] serverside rendered content then um the [93:19] HTML right here would probably contain [93:22] uh like blog posts and so on. So we take [93:25] this snapshot and then next time we [93:28] execute this and it's going to take that [93:31] response content and compare it to the [93:34] previously made snapshot and if there is [93:37] some difference it's going to report us. [93:40] If the snapshot is the same it is going [93:42] to simply pass the test. That's a great [93:45] test uh to simply check that it [93:47] mistakenly you mistakenly doesn't change [93:50] the output content. So right now this is [93:52] failing because uh as I mentioned this [93:55] is more dynamic content and you can see [93:58] that here we have some dynamic versions [94:01] and things like which which changes on [94:03] every refresh basically and that's why [94:05] we see a different content but that test [94:08] would be ideal to test some server side [94:11] rendering um [94:14] pages with great SEO. Not the one uh I'm [94:17] showing right now. For the one I'm [94:19] showing right now, we need to use [94:23] browser testing, which I'm going to show [94:24] you uh in a second. [94:27] Okay, this is also a great way to test [94:30] API endpoints. So, we make the request [94:32] into our API. Let's say we get the um [94:36] user information uh a specific single [94:39] user information by ID or something. [94:41] Then on the second time, we make the [94:44] request and we are sure that we don't [94:45] change anything. uh we don't remove [94:47] mistakenly some fields from the uh [94:50] resource of the user or we mistakenly [94:52] didn't add something there. Now let's [94:55] see another example which is more visual [94:58] regression testing using screenshots. [95:01] I'm going to use test here uh and let's [95:04] give it the following description [95:06] matches homepage [95:10] screenshot. Okay. Now we are not using [95:13] snapshot as an HTML but we're going to [95:14] use screenshot. Okay, I'm going to skip [95:18] the above test and I'm going to only [95:21] execute this one. We are going to call [95:24] visit here provide slash. We're [95:26] accessing the homepage and then we are [95:29] going to call method assert [95:33] screenshot matches. Now, if we execute [95:36] this, we're going to see that it is [95:39] going to create this snapshot in the [95:41] following location. Now, let's bring up [95:43] this snapshot and open this matches [95:46] homepage screenshot and we can see that [95:50] this is how that snapshot looks like. [95:53] All right. So now if we execute the same [95:56] test again, now we're going to see that [95:59] the test is passing because we haven't [96:01] changed anything. So, the snapshot was [96:05] failing, but this one actually was [96:08] passed. Now, let's open welcome.tsx [96:12] and let's find this text. Let's get [96:14] started, which by the way is displayed [96:16] right here. And I'm going to add one one [96:19] right here. And we can make this text [96:22] red as well. Okay, awesome. [96:28] Text red 500. Okay, let's do like this. [96:32] Okay, awesome. Now let's bring up [96:34] terminal and execute PHP artisan test. [96:39] We hit the enter. [96:41] Now the test is going to fail. And it [96:45] also gives us the following um [96:47] suggestion. So if the failing is [96:50] expected and if the change we made on [96:52] our welcome.dtsx DSX file is [96:54] intentional. We can execute the same [96:57] command with d-update screenshot and it [97:00] is going to take the current version as [97:03] this stable version. Otherwise, it [97:06] generated difference which we can find [97:08] inside browser screenshots image diff [97:11] view and this HTML file. If you open [97:13] this HTML file, this is a typical HTML [97:17] and let's open this in the browser. You [97:20] can take this and open in a browser and [97:22] let's click on this slider and look at [97:24] this. So we have this nice slider view [97:26] and here's the difference. Look at this. [97:29] Let's get started 111 in red. Previously [97:32] we had let's get started in black. So [97:35] this is the difference that has been [97:37] made and this is pretty cool. Now if I [97:39] decide that this one is the stable [97:41] version and I want to use this one. Now [97:43] I can execute PHP artisan test with [97:46] d-update snapshots. it is going to [97:49] update snapshots in the in the file [97:51] system. The test will not be completed [97:55] at all. And the next time if I execute [97:57] PHP artisan test, we're going to see [97:59] that the test is passed because the [98:01] latest version is this what we have [98:03] currently and nothing has been changed [98:05] after this latest version. Test also [98:08] gives us possibility to group our tests [98:10] into a specific group and then execute [98:13] only that group. As an example, we have [98:17] this snapshot test and out pages test. [98:20] So what I can do instead of calling this [98:22] skip and only. So I'm going to remove [98:24] both from here. And let's say I want to [98:28] execute only these two tests. I'm going [98:30] to assign group to them. And I'm going [98:33] to call these snapshots. Snapshots here. [98:37] And snapshots here as well. Actually I'm [98:40] going to use snapshots here as well. Now [98:42] I'm going to bring up terminal. Then I'm [98:44] going to execute PHP artisan test dash [98:47] group equals snapshots. We hit the [98:51] enter. And now only those two tests will [98:54] be executed. The first one is snapshot [98:56] homepage which was created and the [98:59] second is matches homepage screenshot [99:01] which was passed. Now if you execute [99:03] this second time both should be passed [99:05] because the first one actually the first [99:08] one is failing. Yeah, we we know why [99:10] this is failing because it has a dynamic [99:13] content in a response. But the thing is [99:15] that we have flexibility to um assign [99:19] groups and then execute based on groups. [99:22] Actually we also have ability to assign [99:24] multiple groups to our tests. So I can [99:28] provide an array here. Let's say this is [99:32] snapshots and home as well. And this is [99:36] snapshots. And we can assign something [99:40] else such as screenshots. So we can [99:44] execute tests based on one group or the [99:47] second group. It's up to you. So if I [99:50] try to execute [99:52] let's say [99:54] screenshots, it is going to execute only [99:57] this test. [100:01] Actually we don't need array here. We [100:03] can directly provide multiple groups [100:07] just like this. So let's execute this [100:09] test and it only executed the last test [100:13] which was passed. [100:15] We also have ability to filter tests by [100:19] the class name of the test or in general [100:22] by the name of the test. Let me execute [100:25] PHP artisan test and I'm going to [100:28] provide d- filter. Inside filter I'm [100:31] going to provide out pages test. So [100:35] we're only executing the test which [100:37] contains out pages test inside the name [100:40] of it. And as you can see only those two [100:43] tests are executed. However for both of [100:46] them we have skip right here and both of [100:48] them are actually skipped. We can also [100:51] provide filter equals login and every [100:55] test that contains a login keyword [100:57] inside there will be executed. Now the [101:00] first one is this which was skipped and [101:03] the second one was it shows a login page [101:06] from public pages test which was not [101:10] actually passed. Also when we provide [101:12] skip right here we can give it custom [101:15] condition or custom message when it [101:18] should be skipped or what message it [101:20] should have. as a message we can provide [101:22] something custom [101:26] message [101:27] and let's execute that [101:31] and here is the custom message why which [101:34] should explain why this was skipped [101:38] we also have ability to create a test [101:41] and mark it as a to-do as not [101:44] implemented yet [101:48] we have some test which we plan to [101:50] implement but it is not implemented yet. [101:53] So we give it to-do and when we give it [101:55] to-do we also have provide to give some [101:59] custom note. [102:01] Now we execute that this doesn't have [102:05] any login keyword and we're only [102:08] executing tests which has a login [102:11] keyword inside there um for logging. [102:14] Let's give it login keyword inside [102:16] there. [102:19] Now it is not implemented yet. Login, we [102:22] have a custom message as well. We can [102:23] describe that specific test. It is a [102:27] to-do and we can describe what the test [102:29] actually should do. But because the [102:32] functionality is not yet done in some [102:34] cases, we just know that we will need [102:38] this test in the future, but we don't [102:41] have it ready yet because the [102:42] functionality is not ready yet. [102:46] Inside the tudo if we follow we can find [102:48] that it has a different uh parameters as [102:51] well. So note is one of them but we can [102:53] assign the user who is going to actually [102:56] do this test or if the test is specific [103:00] to an issue we can provide the issue ID [103:03] as well and then feel we can filter [103:05] to-dos with a specific assigne or with a [103:10] specific issue. [103:12] That's pretty handy. Now I'm going to [103:13] give you a few examples how you can test [103:16] your APIs as well which returns always [103:19] JSON responses. This repository is from [103:22] my laral API course. There's several [103:25] hours YouTube video. You can check it [103:27] out. And the open source the uh code is [103:30] also in my GitHub open source. You can [103:32] have a look. So if we open this login [103:34] API test. So we have several things [103:37] right here. So first we are setting this [103:40] throttle key for throttling because we [103:43] have rate limiting into our API as well [103:46] and that throttle key is to take the [103:49] following email and concatenate it with [103:52] the IP address. Okay. And before every [103:55] test is executed we are clearing the [103:58] rate limiting from cache. That's what it [104:02] does. Now we're going to test if user [104:05] can login with valid credentials we via [104:08] API. So we create the user [104:11] then we are using this post JSON sending [104:15] the request on API/lo with email and [104:18] password and we expect the status code [104:20] to be 200 and we are using the following [104:23] methods assert JSON. Assert JSON is a [104:26] prefix. Assert JSON structure to look [104:29] like this. User should be an associative [104:33] array or in JSON it's going to be an [104:35] object containing ID, name, email and [104:39] there should be token as well alongside [104:41] with user and we also expect the actual [104:45] JSON to contain user with ID this name [104:49] this email this and this is the user [104:51] what we just created another test and [104:55] also we have an one more expectation [104:57] here also we expect the response JSON [105:01] token not to be empty. After this, we [105:04] also expect the user to be [105:06] authenticated. [105:08] All right. User cannot login with [105:11] invalid email via API. We create the [105:13] user and then we send the request with a [105:18] wrong email but the correct password. We [105:20] expect the status code to be 422 [105:24] and we expect inside the JSON to be [105:27] validation errors on key email and we [105:30] expect the user to be guest. [105:34] Okay, another test user cannot login [105:36] with invalid password via API. So we [105:40] create the user we use now correct email [105:42] but the wrong password and we expect [105:44] something similar 222 and assert JSON [105:48] validation errors user needs to be [105:50] guessed login requires email field. Now [105:53] we don't provide email and we expect [105:55] there to be um validation error on email [105:58] field. Now we don't provide password and [106:01] we expect there to be validation error [106:03] on password. Now uh login requires valid [106:08] email input. So we don't provide actual [106:10] email. It's an invalid email. And we [106:12] expect there to be uh the following [106:15] validation error. Login is rate limited [106:17] after too many attempts. This is [106:18] interesting one. We create the user and [106:20] then we try five times um login request [106:25] with wrong password. After this, even if [106:29] we try with correct password because [106:33] this is the sixth attempt, we expect [106:35] there to be a validation error on email [106:38] and login works after rate limit [106:41] expires. Again, we have the user, we [106:44] make five failed attempts. Then we clear [106:48] this throttle key and then we make the [106:51] request with correct password and we [106:53] expect the user to be authenticated. And [106:56] the final one is login endpoint requires [106:58] guest middleware as well. We have the we [107:00] create the user and we are acting as [107:03] this user with the sync town guard. So [107:06] we are sending a request as already [107:08] authenticated user and the request goes [107:11] into API login with the correct email [107:14] correct password and we expect assert [107:16] status to be 302 redirecting. [107:20] Okay, we have one more login response [107:22] contains correct user data structure. We [107:25] create the user. We send the request and [107:28] this is something we have already tested [107:29] above but slightly different way. [107:33] So we expect the JSON to contain user [107:36] with the following data and also we can [107:39] use this expectations on the response [107:40] data. We expect the response data user [107:43] not to have key password and not to have [107:47] key remember token because those are [107:50] sensitive information and we should not [107:52] return. Now if we execute PHP artisan [107:55] test [107:57] we are going to see all tests are [107:59] passing. [108:02] Now we're going to have a look at a real [108:04] project and create tests for a real [108:07] project. This is Laravel medium clone [108:10] which I created for free code camps [108:12] Laravel course video. You can check it [108:14] on free code campams YouTube channel. [108:16] And this is the project. So it is a [108:19] typical medium clone where we have [108:21] registration login. If we are opening [108:24] this as a guest user we are seeing [108:26] several published posts uh with the um [108:30] like sorted in a specific order. If we [108:33] authenticate [108:35] so as an example then on my homepage I [108:38] see um my posts actually the posts of [108:42] the users I am following too. And if we [108:44] click on the post, this is post posted [108:47] by Nunaduro and I am following Numaduro. [108:51] If I click on this, this is posted by me [108:53] and actually I'm following myself as [108:55] well. This is allowed based on the [108:57] application. And we have ability to [108:59] unfollow from here to click on the [109:02] user's profile page [109:04] to go back to clap the post or unclap [109:07] it. And if we go back and click on [109:09] Numaduro click on here, we also see [109:11] unfollow button right here as well. And [109:14] we see a dedicated posts right here for [109:17] that specific user. Okay. And we have [109:20] filtering ability as well for um for [109:24] articles. And now let's have a look at [109:27] what tests are available in this [109:29] project. This is created a year ago [109:32] before pest 4 was released. And we have [109:34] to update this into pest 4. And then we [109:37] have to create a real test. So this is a [109:39] real world scenario. Let's go into test [109:44] folder and I'm going to expand this unit [109:47] test. There's nothing important. So I'm [109:49] going to delete this example test. Now [109:51] let's open this feature example test [109:54] again. Something we can delete. We have [109:57] the profile test and inside [109:58] authenticated folder. This is [110:00] interesting. Let's open this [110:02] authenticated folder and have a look. [110:06] So it doesn't use browser testing. It is [110:09] using typical test three testing with [110:11] this get in most of the cases. So it [110:14] simply makes sure that the login screen [110:16] can be rendered a pretty basic example. [110:19] Then it makes sure that user can [110:21] authenticate using login screen. It [110:24] creates the user then sends the post [110:26] request on here. Then checks the user is [110:28] authenticated and the user needs to be [110:31] redirected into dashboard page. Here we [110:34] have another example. User cannot [110:36] authenticate with invalid password. [110:38] Another one users can log out. We create [110:41] the user. We are acting as the just [110:44] created user and then we perform log out [110:47] and we check the user is guest and we [110:50] should be redirected into slash. Okay, [110:52] nothing major but now let's have a look [110:55] inside email verification test. This is [110:57] interesting. So we check that email [111:00] verification screen can be rendered and [111:02] this happens when we create a user as an [111:05] unverified user. This unverified is a [111:09] method inside user factory which makes [111:11] sure that email verify that is null. But [111:14] our application requires user to be [111:16] verified. So we are acting as just [111:19] created user and accessing the page [111:22] verify email. And if this returns with [111:25] 200 status code, then the email [111:27] verification screen can be rendered. Now [111:29] it's getting more interesting here. Now [111:32] we're checking here if email can be [111:34] verified. Those tests by the way are [111:36] coming were coming uh from a default [111:40] laral installation at the time when I [111:42] created this project. Okay. So we create [111:44] unverified user and we are faking [111:48] events. So we have ability to fake [111:51] certain things such as events, [111:53] notifications, mails and so on. So we [111:56] are faking events. After this we are [111:59] creating temporary signed wrote in a [112:02] similar way how it is done when the [112:05] email verification is sent. So we are [112:08] using the following uh wrote name. We [112:11] are using the following um code to [112:15] simply add 60 minutes to now to make [112:19] this link valid for 60 minutes. And then [112:21] we are taking ID and hash uh which will [112:25] be the hash of user email and now we [112:28] have this verification URL. After this [112:30] we are acting as just created unverified [112:33] user and we are accessing that [112:35] verification URL after which we expect [112:38] user to be verified. So event verified [112:42] should be dispatched. After event [112:44] verified is dispatched then we expect [112:46] the user and we are calling this fresh [112:49] to simply reselect the user from the [112:52] database. And after this we expect that [112:54] user has verified email to be true. And [112:58] we also expect that user to be [113:00] redirected into dashboard with verified [113:04] one because we just accessed this email [113:07] verification URL. Okay. Okay, now let's [113:09] have a look if email is not verified [113:12] with invalid link. So here we [113:14] constructed this verification URL with a [113:16] wrong email. We access it and we expect [113:20] user has verified email to be false. [113:23] Okay, that's all about email [113:25] verification test. Now let's see another [113:27] interesting example inside password [113:30] confirmation. [113:31] So uh I think this is the basic one. So [113:34] we create the user, we are acting as [113:36] that user and accessing the password [113:37] confirmation. That's basic. Uh where's [113:40] the one I was looking for? Here we are [113:42] accessing with um the correct password. [113:45] We are simply uh confirming the password [113:49] and we expect the redirect with no [113:51] errors. Um and here we simply use a [113:54] wrong password. Okay, that's not the one [113:56] I wanted to show to you. Password reset [113:58] test. This is it. Okay, let's start from [114:01] here. This is basic one and this is the [114:04] notification fake. Interesting one. So [114:06] we reset the password link uh we test [114:09] that the reset password link can be [114:11] requested and here we are faking the [114:13] notifications because when we try to [114:16] reset password link we expect an email [114:18] to be received through notifications [114:20] right so we are faking notification we [114:23] are creating the user we are sending [114:25] post request into a forgot password with [114:28] this email and through this notification [114:32] class we test with assert sent two we [114:36] test that notification [114:39] this notification reset password was [114:41] sent to the following user that's [114:44] interesting now also reset password [114:47] screen can be rendered and this is [114:49] interesting one so we send post request [114:52] to forget password [114:54] uh we check that notification was sent [114:57] to the following user but after this [115:01] from this notification we get the token [115:04] we use that token to access to the [115:06] following URL and we check that uh the [115:10] following URL returned with 200 status [115:13] code which means the notification was [115:16] sent and we we are able to click on that [115:19] password reset link and it worked and [115:23] another example [115:26] password can be reset with a valid [115:28] token. Now again we created user we [115:32] performed for password we received the [115:35] notification and then using that [115:37] notification token we performed password [115:41] change we send the post request to reset [115:44] password we provide the token email and [115:46] new password and we expect that session [115:49] has a no errors and we are redirected to [115:52] the login page that's pretty interesting [115:54] one also we have this password update [115:56] test which is I think basic compared to [115:59] others. We create the user acting as the [116:01] user accessing the profile page and [116:03] putting the uh current password as well [116:05] as the new password. We expect no errors [116:08] redirected into profile page and we also [116:10] check that the user's password right now [116:14] is the same as the new password plain [116:18] text to be um like hashed with a [116:22] specific hash facade. [116:25] Okay. Uh another one correct password [116:28] must be provided to update the password. [116:30] If we provide the wrong password here we [116:31] expect the session has an error on the [116:33] update password and um this is the [116:36] field. Okay. And the last one is [116:39] registration trait. uh we access the so [116:43] this is the interesting one because this [116:44] is one is failing because we I in the [116:47] project updated the registration part [116:51] and yeah we're going to fix this and we [116:54] have this profile test as well which [116:56] doesn't have anything significant but [116:58] the thing I wanted to show to you was uh [117:01] this notification fake okay we don't [117:04] have anything interesting here as well [117:06] now let's bring up terminal and I'm [117:09] going to execute PHP P artisan test and [117:13] we're going to see that some of the [117:15] tests are failing. Those tests are [117:17] related to profile information can be [117:20] updated. New user can be registered. So [117:24] in our [117:26] website now the registration [117:30] let's access this registration now has [117:32] username by default the username was not [117:35] there. I haven't touched the test when I [117:38] developed this project. So username is [117:40] missing. If we open right now [117:44] the registration test right here, we're [117:48] going to provide the username. Let's [117:50] give it username to be test. [117:55] Username to be test. Now if we execute [117:59] this again, [118:02] now we don't see error on new user can [118:07] register anymore. We have error. Profile [118:09] information can be updated. Let's go [118:11] into profile test and search for this [118:16] profile information can be updated. [118:20] We provide the name. We should provide [118:24] username as well to be test. So we save [118:27] this. [118:29] Uh actually we have one more error. [118:31] Let's fix this as well. Email [118:32] verification status is unchanged when [118:35] the email address is unchanged. [118:39] Before we fix this, there are two things [118:41] to change. First, we're going to change [118:43] the database configuration which is used [118:45] by pest. And second, we're going to [118:47] update pest framework into pest version [118:50] 4. Let's open [118:53] phpit xml. [118:56] Okay. Now look at this. By default in my [118:59] project which is created like about a [119:02] year ago this part is commented out [119:05] which means that this is using the exact [119:07] same database I have right now in this [119:11] project. So as soon as I execute the [119:12] test it is working on the same database. [119:14] So my data is lost right now. And this [119:18] is kind of annoying because I just [119:20] created uh several useful posts. [119:24] Okay. Okay. So, first we have to [119:25] uncomment this. [119:27] Make sure it is going to run uh inside [119:30] memory always. Then let's update into [119:33] latest version of P uh PS4 and then we [119:37] can work on fixing the test and creating [119:39] new tests. So, if we have a look inside [119:41] composer JSON, we have the PPHP version [119:44] 3 something. Let's change this into 4.1. [119:49] And we have to update as well the pass [119:52] plugin Laravel into 4 [119:55] zero. [119:58] Okay, we're going to save this. Let's [120:00] execute composer [120:03] update. Test4 has been installed and [120:06] we're going to fix the following part. [120:07] We're going to provide username to be [120:10] test as well. Let's bring up the [120:12] terminal and execute PHP artisan test. [120:16] And we see all tests are passing. [120:20] Now I'm going to install browser testing [120:22] in my [120:25] project not install browser but I'm [120:28] going to install b [120:30] plug-in browser as a dev dependency. [120:33] It's going to download this uh playright [120:36] as well. This was installed which is [120:39] great. And now we're going to create new [120:41] test and I'm going to execute phpart [120:43] artisan make test. I'm going to create [120:46] this inside post folder. And let's call [120:49] this post test. The test was created. [120:53] Let's open post test. Here we go. And [120:57] I'm going to define several tests right [120:59] here. Let me clear this up. So I'm going [121:02] to define test that user [121:07] can create a post. So that's one thing. [121:11] Let's assign to-do to it. Next, [121:16] we are going to write test to check if [121:20] the user can view published post. User [121:24] can view published post. [121:29] Let's assign to-do to it as well. [121:32] Another one is going to be uh user [121:36] cannot edit other users post. [121:42] assign to-do to it. And now another one [121:46] is going to be user can update its own [121:49] post. User can update [121:53] uh their post. [121:57] Okay. Awesome. And the last one I'm [122:00] going to do is to test if user can [122:02] delete [122:05] user can delete their post. [122:11] Okay, that's enough. Uh, actually, let's [122:13] add one more thing. Uh, on the homepage, [122:17] we display posts for the [122:21] uh from the following users. Basically, [122:23] I am following too. So, um let's let's [122:28] do the last one as a as a browser test. [122:33] Okay, which we will be able to debug [122:34] this in the browser as well. Um, [122:37] dashboard shows posts from followed [122:43] users. [122:45] Okay cool [122:48] to do. Okay, awesome. Now, let me bring [122:50] up terminal and I'm going to execute PHP [122:52] artisan uh test dash filter to be post [122:58] test. [123:01] Post test. Okay, hit the enter. Now we [123:03] see all to-dos right here. Awesome. Now [123:05] let's start implementing from the first [123:07] one. [123:09] Okay. So we have to create the user and [123:13] then we have to try to create new post [123:16] using that user and we have to attach [123:19] the information um such as image title [123:22] content and few other things inside that [123:25] form. Okay. So I'm not going to do this [123:28] as a browser test. Uh I'm going to do [123:30] this as a typical submission test uh [123:34] like it it would work in test three as [123:36] well. So we're going to start with this [123:41] acting as so we're going to act as the [123:44] authenticated user but we have to create [123:45] that user. So user equals user factory [123:50] create. Let's import this user. [123:54] Okay. Awesome. Now we are acting as this [123:57] user. Actually we will need this user in [124:00] couple of other tests. So I'm going to [124:02] define before each and I'm going to [124:05] define this user equals user factory [124:08] create. Okay, awesome. Now I'm going to [124:10] provide this user right here. So I'm [124:12] acting this user which I just created. [124:16] Then I'm going to send post request into [124:20] the following route. The route is going [124:22] to be post store and I'm going to send [124:24] the following information. I'm going to [124:26] provide the title which is going to be [124:29] um test post title. Okay. Next, I'm [124:35] going to provide content which is going [124:36] to be test post content. Let's provide [124:40] also category ID. Category ID is going [124:44] to be something. Uh you know what? We [124:48] can create new category right here. But [124:51] uh I'm going to execute this seed [124:54] command and it is going to seed several [124:58] categories [125:00] uh inside the database. [125:02] If we open database cedar we're going to [125:06] see that we are creating the following [125:07] categories in the database when the seed [125:09] comment is executed. So I'm going to [125:11] execute the seed command before every [125:14] test. So inside category id actually and [125:17] whenever I do this uh I want to select [125:19] one category and assign this. So this [125:24] category equals category not factory [125:27] create but instead category [125:30] limit one [125:34] first. Okay. Now we have the category [125:37] and we're going to take this category ID [125:41] and assign this in the form. Then we [125:43] have to take image as well. So we have [125:46] we have the image and actually let me [125:48] define a fake image here. Image equals [125:52] uploaded file. We're going to call fake. [125:56] Then we're going to call image to create [125:58] that fake image. And then optionally we [126:01] can provide some extensions as well such [126:03] as uh width to be 800, height to be 600. [126:07] Uh let me replace this with import. [126:11] Awesome. So we're going to take this [126:12] image and assign here provide here and [126:17] finally we have to provide published it [126:20] at published it at will be now okay so [126:25] we're making post request on the [126:26] following URL and now what do we expect [126:30] so we can change several methods or we [126:32] can save a response in a variable and [126:34] then we can use this method on the [126:36] variable so let me change the methods [126:39] assert we expect redirect to happen on [126:43] dashboard page. So we expect assert [126:46] redirect [126:48] um wrote is going to be dashboard [126:52] and that's it. Okay. When we are [126:55] redirected on the homepage then we can [126:58] check in the database if that post is [127:02] available in the database. So there are [127:05] two ways actually or multiple ways to [127:07] test if the post was actually created. [127:10] One is that we can check the database. [127:11] Second is that uh after this we navigate [127:14] into my post page and we check if the [127:18] post is there with the same title. We [127:21] can do this as well. So now let me do [127:23] the database check. So we're going to uh [127:25] call assert database has and we are [127:29] checking inside posts table. We are [127:32] looking for a post which has title test [127:36] post title. All right. And that should [127:41] have user [127:43] ID uh that should have user ID current [127:46] user ID which is authenticated right now [127:49] and that should have category ID the [127:51] current category what we assigned. So if [127:54] this is completed then we know that the [127:56] test is completed and the post was [127:59] created with image and everything. [128:02] Okay. So let's bring up terminal [128:05] and [128:06] execute PHP artisan test d- filter post [128:10] test. Okay, this is still to-do. So we [128:13] have to remove this to-do. Re-execute [128:15] the test and we see that was failed. [128:19] Post store. It is not in plural form to [128:22] uh that is in a singular form. Post dots [128:26] store. Okay, let's reexecute now. [128:31] Let's see. the post store. [128:34] I said it is singular. [128:39] Okay. [128:41] Now there's an uh can attempt to read [128:43] property ID on a null. It seems like we [128:46] have an issue on category because I did [128:49] something very stupid. [128:51] Why didn't you tell me that I was doing [128:53] this stupid thing? Okay. So, first seat, [128:56] then select create the user and then um [129:02] then create select category. Okay. Let's [129:04] execute this one last time. Okay. And we [129:06] see that the test is passing. But here's [129:09] one interesting thing which I'm going to [129:11] which I'm going to add into this test. [129:14] So if we open postphp [129:18] uh which is a model that model uh [129:21] implements has media has media is a [129:24] spatis package where is this has media [129:27] it is imported from spatiy media library [129:30] and the thing is that [129:33] it automatically [129:36] converts that uploaded image into [129:39] different resolutions if the cues uh if [129:42] we are listening to cues and the cues [129:44] are running um in the background. So we [129:48] are registering this media conversions. [129:50] So [129:52] this parties package will take the image [129:55] and convert them into preview and large [129:58] versions and we also uh have this media [130:01] collection and everything is added [130:03] inside this media collection. So [130:05] internally cues are working. [130:08] So what we can do is test if Q [130:13] processing happened as well inside the [130:17] following test. For this we have to fake [130:20] Q's. So I'm using this illuminate [130:23] support facads Q and then I'm going to [130:25] call fake on that. And let me replace [130:28] this with import. And so I'm faking Q's [130:32] and at the end when the test is [130:34] completed I'm going to use that Q and [130:38] I'm going to call assert [130:40] uh assert pushed. Assert pushed means [130:44] that the job has been pushed and the job [130:47] is called uh perform conversions [130:52] job and that perform conversions job is [130:56] coming from spatis media library [130:58] package. So I'm going to import this as [131:00] well. And now let's test this again. And [131:04] as you can see um the test was passed. [131:08] Now if we go into post and if we remove [131:12] this implements media [131:14] also remove this interacts with media. [131:19] Now these methods doesn't make any [131:21] sense. All right. And the job should not [131:25] be processed should not be started. If [131:28] we open terminal right now and test [131:30] this. Now the test was failed. [131:36] Add media from request. So this is now [131:39] this is actually failing uh from a code [131:42] level not from the um from the test [131:46] perspective. If we scroll up actually [131:50] I just want to give you that [131:54] information [131:57] I'm using this inside post controller [132:01] right [132:02] we take this image and add um uh add [132:06] media from request and that happens on [132:08] store. So if I comment out this [132:12] then bring up terminal [132:15] and execute that. Now look at this. The [132:20] expected spatier media library [132:22] conversions job perform conversions job [132:24] was not pushed failed. Asserting that [132:28] false is true. This is exactly what I [132:30] wanted to show to you. I'm going to [132:32] revert this. I'm going to also add this [132:35] interacts with media and implements has [132:38] media as well. So we can obviously [132:40] remove this Q thing and test this [132:43] without that as well. But we are also [132:45] testing that Q's are pushed or we can [132:48] create a separate test exclusively for [132:50] these cues. Okay, now we have the first [132:53] test ready. The second test is that we [132:56] can check if the user can view published [132:58] posts. Right? So we're going to create [133:02] the function here. Again, we already [133:04] have the user created. So we don't need [133:06] to create the user. However, we're going [133:09] to create the post. So post equals post [133:14] post [133:16] factory [133:19] factory. [133:21] Uh let's import post [133:25] factory. Let's call create. [133:28] And we're going to provide um user ID [133:31] which is going to be this user ID. We're [133:35] going to provide category ID which is [133:38] going to be this category ID. And we're [133:41] going to provide published ad which is [133:43] going to be now let's subtract something [133:46] subday one. Okay. Now we have a post [133:50] and now we're going to try for the [133:52] current user to view that post. So this [133:57] get [133:59] we're going to use the following road [134:01] post show providing uh we're going to [134:04] provide two things inside post show. If [134:07] we open web.tphp and search for post.sh [134:10] show this accepts two arguments username [134:14] and post slug. So we have to provide [134:17] both. So I'm going to provide user name [134:20] to be this user's username and we have [134:23] to provide post which is going to be [134:26] this post slug. [134:29] Okay, awesome. Now I'm going to move [134:32] this down. Now we send the request, we [134:35] got the response. Now we can save this [134:38] into a response variable and then we can [134:41] call assert methods on that or we can [134:43] chain some methods like assert okay is [134:47] one method. We can also assert C post [134:51] title and we can also assert C post [134:56] content. So the we fetch the post and we [135:00] expect to see that post information. [135:03] Okay. Awesome. Um I think we should [135:07] remove this to-do and we should execute [135:09] this test. And we see two tests are [135:12] passing. The third one is that user [135:14] cannot edit other users post. Let's [135:19] write this as well. [135:22] Oops. Let's remove the to-do. So we have [135:25] a user already created, right? But right [135:28] now we're going to create a new post and [135:31] we're going to create new user. [135:36] So let me create first owner post owner [135:39] user which is going to be user factory [135:42] create [135:44] user factory create. Okay. Next I'm [135:46] going to create a post and I'm going to [135:49] create this post. I don't want the full [135:53] autocomp completion. I want only small [135:56] part to be able to complete it. I don't [135:58] want too many code to be written at the [136:00] same time. Uh I want you to follow me. [136:03] So we are calling this post factory [136:06] create and we're going to provide the [136:08] user ID which is going to be owner ID [136:11] and we're going to provide also category [136:14] ID as well which is going to be this [136:16] category ID. [136:18] So we have the owner user. We have this [136:22] user which is at the top. Let me [136:25] collapse the other tests. [136:28] And now we're going to try if this user [136:31] can edit the post of this owner user. [136:34] All right. So we're going to use this [136:37] acting as [136:43] by the way. [136:47] So right here in the following part we [136:49] tried to access a post show without the [136:52] authenticated user. We don't have acting [136:55] as which is also not a problem. So we [136:58] tested if guest users can view posts [137:01] which is logical. Okay we can leave [137:03] this. [137:04] So right here we're acting as this user [137:07] and we are trying to access we're trying [137:12] to access and get uh the post [137:16] information and we expect assert [137:19] forbidden. [137:21] I think this looks good. So we have the [137:24] owner user create the post and then [137:26] using this user we act and try to get [137:28] okay let's execute the test and the test [137:31] has been passed. Okay awesome. Now we're [137:34] gonna see if this user can update their [137:37] own post [137:41] function. [137:44] Okay. [137:47] Now we are going to use again Q fake [137:49] here because when we attach an image [137:52] again that image is processed. So at the [137:55] end we want Q [137:58] assert [138:00] pushed and that's going to be perform [138:02] conversions job. Okay. Now here we are [138:06] going to create a post post equals [138:10] post factory create and we have to [138:12] provide user ID and we have to provide [138:15] category ID. Okay, anything else doesn't [138:17] matter. Then I'm going to create new [138:20] image right here which is going to be [138:22] uploaded file fake image. Let's give it [138:26] name test jpg. And then we're going to [138:29] send the request. So we're going to use [138:31] this [138:33] acting as this user. Then we're going to [138:36] use put method to send the request on [138:39] post update. And we're providing right [138:42] here [138:44] post slug. So we try to update actually [138:48] let's have a look inside web.php PHP [138:50] inside post update. This is it. Okay. So [138:55] just providing post ID or post will be [138:57] enough. So we can simply provide post [139:00] here. And inside the data we're going to [139:02] provide title updated title. We're going [139:05] to provide content to be updated [139:07] content. We're going to provide category [139:09] ID and we're going to provide image. [139:12] Okay. And once the update is performed [139:15] then we can write assert redirect [139:17] dashboard. We expect the user to be [139:19] redirected uh into dashboard or we are [139:22] is the user actually redirected. I think [139:24] the user is redirected into my posts [139:28] page as I remember. So let's try this. [139:32] And once that is done then we need to [139:35] check in the database assert database [139:39] has a posts and we're going to search [139:41] this [139:43] with a new title. [139:46] This is going to be the post ID should [139:49] be the post ID [139:52] but the title is interesting. The title [139:54] needs to be updated title as we provided [139:57] right here. Okay, if this is done then [139:59] the update works. Now let's execute [140:01] this. User can update the air post. [140:05] Okay, awesome. The next one is user can [140:08] delete their post. I think this is [140:09] pretty straightforward. So I'm leaving [140:12] this to you as a challenge. implement [140:15] this right now and then come back and [140:17] see my solution. [140:20] Okay, so I hope you made it. In order to [140:23] delete the post, first we have to create [140:25] this. So post factory create. We're [140:27] going to provide the user ID and we're [140:29] going to provide category ID. Others we [140:32] don't care. Now we're going to use this [140:35] acting as this user and then we're going [140:38] to use delete [140:40] on post destroy. So we're sending delete [140:44] request on post destroy sending the post [140:47] and that's basically it. So once we do [140:50] this [140:52] assert redirect to my post we expect to [140:55] be redirected to um my post page. [141:00] Okay. And also we're going to check that [141:03] the post doesn't exist in the database. [141:05] So this assert database missing inside [141:09] posts table. This ID must be missing. So [141:14] let's execute this. [141:17] Something was failed. [141:19] User can delete their post. Assert. [141:22] Okay. So we expected to redirect into my [141:24] post page, but we are redirected into [141:27] dashboard. Okay. I don't know why I did [141:29] this, but we should uh write a test [141:32] according to how it is done. So we [141:35] expect to be redirected on dashboard. [141:38] Okay. Awesome. Now the test is failing. [141:41] So if at some point I go into post [141:44] controller and decide that after [141:47] deleting my record I want to be [141:49] redirected into my post page [141:52] then my test will fail. Okay. So I'm [141:55] going to return this into dashboard. And [141:57] the final test I want to write as a [142:00] browser test. So it's going to be um [142:03] interesting. Let's go into pestph [142:07] uh post test php file and let's write [142:10] this function here. So here this is a [142:14] complex chat uh test a little bit [142:15] compared to others. So on dashboard [142:18] we're going to test if the current user [142:20] sees posts from followed users. All [142:23] right. So first I'm going to create [142:30] uh two users. [142:33] One is going to be followed user [142:38] and second is going to be [142:41] unfollowed [142:43] user. [142:45] Unfollowed [142:47] user. Oh why do you have autocomplete [142:50] all 10 lines? I don't want that. [142:53] I want to write step by step. So we have [142:56] two users followed users and unfollowed [142:58] users. And we have this user as well. [143:01] Okay. So then inside this user [143:07] um inside this user following [143:11] we are going to attach [143:14] uh following users ID [143:21] following. So this user now is following [143:24] this user. All right. So next we are [143:28] going to create [143:30] post [143:33] post [143:35] factory [143:37] create. [143:38] We're going to provide the user ID which [143:41] is going to be [143:43] unfollowed users ID. So we are creating [143:46] the post for unfollowed user. Actually [143:49] we have to create two posts to be more [143:51] specific. So let's create first followed [143:54] user ID. We have to provide the category [143:56] ID and we have to provide published [144:00] published add to be in the past [144:04] comma is missing here. Okay. So this is [144:08] followed post [144:11] followed [144:13] post. Now we're going to create [144:15] unfollowed users post. So this is going [144:18] to be unfollowed user post. Unfollowed [144:22] post. [144:26] After this, let's use now browsers API [144:30] uh and we're going to visit dashboard. [144:35] We don't want this absolute. So, I'm [144:36] going to remove this. So, we're going to [144:37] visit dashboard. We're going to [144:41] uh let me think. [144:48] Okay. So, we need to authenticate. [144:53] we need to authenticate as this user and [144:56] only after this we should be able to see [144:59] a post from this followed uh follow [145:04] followed user this post basically. So [145:08] now let's try to access login page first [145:11] and then we have to fill up some [145:14] information like we're going to fill up [145:16] the uh this user email and we are going [145:22] to fill password [145:26] which is hardcoded password inside the [145:29] user factory. So that's the plain [145:31] password. After this, we're going to [145:33] press on [145:36] log in and we expect assert path to be [145:44] slash. So we expect to be redirected on [145:46] slash. After this we expect to see [145:50] followed post title [145:53] and we expect assert don't see [145:58] assert oh semicolon assert don't see [146:03] unfollowed post title so I think that's [146:05] the complete test and let's execute this [146:10] so and I'm going to provide d-debug [146:14] to see this. [146:20] Okay, it was so fast. Uh, okay. Let's [146:24] write debug method here. [146:28] Now, let's execute this without debug. [146:36] Okay, here we go. So, we are redirected [146:38] into dashboard and we see the following [146:41] post and in the background. Press any [146:43] key to continue. And we see that the [146:45] test was passed. [146:48] That is really good. Now let's execute [146:51] this without stepback. [146:56] And we're going to see [146:58] six tests are passing. [147:02] Here are new tests. We have clap test [147:04] inside which we test if the user can [147:07] clip and uncip post and guest user [147:10] cannot clip the post. We have similar to [147:13] test user can follow and unfollow [147:15] another user and user guest user cannot [147:18] follow the user. We have the dashboard [147:20] integration test as well uh which simply [147:24] checks that on dashboard uh it tests the [147:28] dashboard basically with following and [147:30] unfollowing. We have the profile image [147:32] test, public profile test as well and we [147:34] have security test. You can find all [147:36] these tests in the project's repository. [147:39] The link will be in the video [147:41] description. Let's review two tests [147:43] which I feel is interesting. So we [147:45] create the user, we create the category. [147:47] We can of course put this inside the [147:49] before each as well. You can you can do [147:51] this. So we create the user, we create [147:53] the category and then we create the [147:55] post. Then we are acting as this user [147:57] and we're sending post request to the [147:59] clip wrote and passing the post and we [148:02] expect this to return with 200 status [148:05] code and we expect it to return JSON [148:08] which should have clips count equals one [148:12] and in the database we also expect that [148:15] inside clips table there should be a [148:17] record for the user ID and post ID. [148:20] After this we're acting as the same user [148:22] sending the same request on the [148:24] following URL and we expect the clips [148:26] count to be zero and in the database [148:30] there should not be any record with [148:32] clips. So in the exact same way we are [148:34] doing following and unfollowing. [148:36] However, if we try to uh clap as a guest [148:40] user and send this clap uh post request [148:44] on the clap road, we expect to be [148:46] redirected on the login page. Now [148:48] another interesting one is this [148:50] dashboard integration test. [148:53] So we create the user and we create two [148:56] other users for following and we create [148:58] the category as well. So inside the user [149:01] following we attach those two users. So [149:03] this user is following these two users. [149:07] So after this we are creating several [149:11] posts. All right. Then we take the five [149:15] post and iterate over those posts and we [149:19] are assigning follower followed user one [149:23] or followed user two. So we are updating [149:26] those posts. [149:29] After this we are making requests on the [149:31] dashboard page. We expect it to have a [149:33] status code 200 and we expect the view [149:37] on this dashboard page to has posts and [149:40] the post count must be more than zero [149:43] and the first post should have loaded [149:47] data uh for user as well as for media. [149:51] So we simply check that we see [149:53] information on the dashboard page and [149:56] posts are rendered properly. As an idea [149:59] I'm going to give you a hint. So we [150:01] could do something similar but instead [150:03] of doing this. So we can do the [150:06] following. We can access using visit [150:09] method to the dashboard page and we can [150:12] count how many posts we see on the [150:15] dashboard page and we can do this using [150:17] browser's API. Now let's take this [150:20] project and deploy this on production [150:22] environment and we're going to also set [150:24] up CI/CD pipelines where our running [150:28] tests command will be inside the [150:30] deployment phase and never and if the [150:33] tests are failing our application will [150:35] not be deployed as well. So let's go [150:38] into hosting age panel and I have [150:41] grabbed the following hosting. When you [150:43] create new account you're going to see [150:46] the following screen. So we have to [150:47] create or migrate website. I'm going to [150:49] create website. Click on next. I'm going [150:51] to choose this empty phtml website. And [150:54] now I can grab new domain. Let me search [150:58] for laravelesting.io. [151:03] That domain is available for me. So I'm [151:05] going to grab that and I'm going to [151:06] click on next. [151:10] Because I am registering a new domain, I [151:12] have to enter some of the contact [151:14] details of me, my personal contact [151:16] details. So I'm going to choose this and [151:18] click on finish registration. [151:20] Until this is being registered, let me [151:22] introduce my open source package using [151:24] which we're going to deploy this project [151:27] to Hostinger's shared hosting and this [151:30] really helps us in the deployment phase [151:32] and just takes away a lot of manual [151:35] work. So make sure you grab the [151:37] following code, open your project, paste [151:40] and hit the enter. We're going to [151:41] install this as a dev dependency. [151:44] After this we're gonna configure the [151:46] following in variables. This is [151:48] necessary our computer to connect to [151:51] hostinger hosting and um make some [151:54] operations there and this is necessary [151:56] our repository to connect to GitHub API [151:59] and generate the deployment key as well [152:01] as secret tokens for automatic [152:03] deployment. So I'm going to get this. [152:06] Next I'm going to open [152:10] and I'm going to paste this at the very [152:11] bottom. This package is installed. Then [152:13] let's open Hostinger and we have to [152:15] choose the location at this stage and [152:17] click on next. Now it is going to [152:19] initialize our hosting for us. Once the [152:22] hosting is initialized, we can try [152:24] opening this laral testing.io. But we're [152:27] going to see the following error. That's [152:29] because the SSL certificate needs to be [152:31] installed. Even if we try to open this [152:34] under HTTP, [152:36] it still is in connecting phase. So it [152:39] will take some time until the domain is [152:41] connected. You can check the status of [152:43] SSL from here. If you just type SSL [152:46] right here. So, we have a lot of options [152:49] uh from the hosting panel. What I'm [152:51] going to do is change the PHP version [152:53] first from this advanced PHP [152:54] configuration. I'm going to switch into [152:57] PHP 8.4. Click on update. Next, I'm [153:00] going to click on PHP extensions. And if [153:02] you want to enable or disable certain [153:04] extensions, you can do this from here. [153:07] But from PHP options, we have ability to [153:09] remove some functions from disable [153:12] functions to increase memory, max [153:14] execution time and so on. I'm going to [153:17] remove exec from here because exec [153:19] function is necessary for this open [153:21] source package to execute certain [153:23] commands. I'm going to click on save. [153:26] Next, if we want to use MySQL database, [153:28] we can search for MySQL here. We can [153:30] create a MySQL database and configure [153:32] our project. But I'm going to stick with [153:34] SQite for now. And I'm going to open [153:37] SSH. We have to enable SSH access. By [153:40] default, it is disabled. So we're going [153:42] to click on enable here and it's going [153:44] to be enabled. [153:46] Okay. After this, we we need to connect [153:48] to the server using SSH. So we can do [153:50] this manually or we can skip connecting [153:52] manually and our open source package [153:54] will be able to connect it. But for [153:56] this, we have to generate public private [153:58] keys. If you don't know what is public [153:59] private keys, I recommend to download [154:01] Putty. Then open putty genen in your [154:05] computer and with just your mouse [154:07] movement you will be able to generate [154:09] public private keys. I have more [154:11] detailed video about the deployment in [154:14] two ways with the manual work and with [154:16] the automated package as well. You can [154:17] check this video in the description or [154:19] in the top right corner of the screen. [154:22] So in this case I'm I have existing key. [154:26] So this is what I'm going to do. I'm [154:27] going to open puty genen. This is puty [154:30] genen and you can click on generate to [154:31] generate new pier or you can load an [154:34] existing one. I have already created the [154:36] one which is saved under my home [154:38] directory. SSH folder. So I'm going to [154:40] click and open this and this is my [154:42] public key. So I'm going to copy this [154:44] entire public key and right here under [154:47] SSH keys click on add SSH key. Okay, my [154:51] laptop. Now I'm going to click on add [154:54] record. [154:56] So finally we have to take this IP [154:58] address and put in ENV file of our [155:01] project which is Hostinger SSH host. [155:04] This is it. Now we're going to grab the [155:06] username and put this here. We're going [155:10] to grab the IP. Put this here. As for [155:13] the test folder, the uh not the test [155:16] folder but the as for the hosting your [155:18] site directory, this is going to be the [155:20] domain name what we gave to our website. [155:23] laravelesting.io. [155:27] Okay. And the final part is to generate [155:29] GitHub API key. I'm going to go into my [155:31] GitHub account. Click on the profile [155:34] settings. [155:36] Then scroll down below developer [155:38] settings personal access tokens fine [155:41] grain tokens. And I'm going to click [155:43] this generate new token. I'm going to [155:45] use two-actor authentication from GitHub [155:47] mobile. Let's give it proper name [155:49] Laravel medium clone. I'm going to give [155:52] the project name to it and this is the [155:54] owner and I'm going to generate this [155:56] only for the selected repository and [155:59] this is going to be medium clone laravel [156:02] medium clone okay perfect and we have to [156:04] assign two permissions one is the [156:06] administration and second is going to be [156:08] secrets administration is necessary to [156:11] generate deployment key inside our [156:14] repository which is uh this laral medium [156:17] clone repository and we need read and [156:19] write permission and secrets is [156:21] necessary to generate deployment secret [156:23] keys in the repository and we also need [156:26] read and write there. Okay, generate. [156:29] Now we're going to copy this deployment [156:31] key and put in our project. Okay, so our [156:35] like the package setup is ready and then [156:39] if we have a look in the package [156:40] documentation, we have a single command [156:42] to do everything. PHP artisan hosting [156:45] your deploy and setup CI/CD or we have [156:48] separate commands for deployment [156:50] separate for to publish the workflow and [156:53] uh separate to set up CI/CD. So I'm [156:55] going to just execute a single command [156:58] from here and let's wait. Now it is [157:02] trying to connect to the server. Okay. [157:05] So in my case it asks me the password [157:06] and that's because I have added in [157:09] hostinger my Windows um Windows public [157:13] key. So I'm actually on Windows right [157:16] now but I also use WSL with Ubuntu and I [157:19] have my project inside WSL Ubuntu and [157:22] this is the reason. So I have not edited [157:24] my Ubuntu's public key right there. I [157:27] have public private keys in both Windows [157:29] and Linux. If I want to add my Linux [157:32] public key there as well, I'm going to [157:34] read this uh ID RSA pub from my home [157:38] directory. Copy this and I can add [157:42] second SSH key as well. Windows WSL [157:48] to Ubuntu. Okay, [157:51] great. Add record. Now let's clear this [157:54] up and execute PHP Artisan Hostinger [157:56] deploy SSH connection successfully. It [157:59] doesn't ask me the password anymore. Now [158:01] it's going to start all these processes. [158:04] It's going to set up SSH keys on the [158:06] server. It is going to connect to our [158:08] repository and add deploy key there. [158:11] First, it's detected that we have [158:13] something in the following folder. [158:14] Laravel testing io. And if we open this [158:18] right now, we're going to see that this [158:20] is the hostinger's default content. Now, [158:23] there is a single PHP file which renders [158:26] the following content. So we have two [158:29] options replace or cancel. I'm going to [158:31] replace this. [158:33] Now it is going to perform the [158:35] deployment of the application. Now the [158:38] application has been deployed and we can [158:40] already open this in the browser and [158:42] we're going to see that our application [158:43] is there. It's working. We can create an [158:46] account. We can login. We can create the [158:48] posts. Everything is going to work. And [158:50] it also set up right now automatic [158:52] deployment. and it published the [158:54] following GitHub workflows hostinger [158:57] deploy file. So as I mentioned the [158:59] application is deployed we don't need to [159:00] do anything but we need to set up [159:02] automatic deployment and use our uh like [159:06] past CI/CD workflows there. Now if we [159:10] have a look inside GitHub workflows [159:12] hostinger-deploy [159:13] yml file this is by default what's [159:16] happening here. So it listens to the [159:18] push on the main branch because right [159:19] now on the main branch and we have three [159:22] jobs and right now inside the deploy it [159:25] um it's going to perform the deploy even [159:27] if it is failure. So we're going to [159:29] remove this failure part [159:32] only deploy will only happen if the [159:34] success is completed. Now inside the [159:36] test it is setting up the PHP executing [159:40] composer install generating keys and [159:42] right here on the following place it is [159:44] running tests. Now let's go into test [159:47] documentation [159:49] into continuous integration which you [159:51] can find in the sidebar and we're going [159:53] to take some of the things like we want [159:55] browser testing and we have to execute [159:58] tests with the following comments. So if [160:00] we scroll down below we have this [160:01] browser testing section as well. So I'm [160:03] going to copy these three steps and I'm [160:07] going to put this before running tests. [160:11] Okay. Okay, so it is setting up the [160:12] Node.js, executing mpm install, [160:14] installing playright. After this, [160:19] we have to execute the following comment [160:24] vendor bean past-ci- [160:26] parallel run. We can call this past [160:30] slash browser test. And I'm going to [160:32] remove this continue and on error. And [160:35] also browser test needs uh mpm uh mpm [160:40] assets to be built. So we are going to [160:42] write here build assets. [160:46] mpm run build. [160:50] Okay. So if everything is done correctly [160:52] then we have to commit and push this [160:54] file. [160:56] Once we push this and open repository [160:58] into actions tab, we're going to see [161:01] that these jobs are running, running [161:04] tests, building assets. And let's wait [161:06] for some time. And just like this, the [161:09] deployment has been completed. All three [161:11] steps are there. And if we refresh our [161:14] website, we are not going to see any [161:15] changes here. Um, but the deployment [161:18] worked and the tests we are run. So we [161:21] can check this run tests section and see [161:24] what happens. build assets run past [161:26] browser tests. Expand this and look at [161:29] this test will run in parallel. All 39 [161:32] tests were passed. All right, my [161:34] friends. That's it for now and I hope [161:36] you learned something new. Please leave [161:38] a like and subscribe. Also, you might [161:40] want to check out the following course [161:41] where I'm going to show you how you can [161:43] build REST APIs using Laravel and deploy [161:45] them on production environment. That API [161:48] uses AI features as well. Thanks for [161:50] watching and I will see you in the next