Creating a project template using Laravel, Vue 3, and Tailwind - Part 2

Nolan Nordlund

Nolan Nordlund

Today we are going to take our simple project template and bring it to the next level by adding Laravel Breeze for simple authentication and set up our TDD workflow using PHPUnit and Jest. If you missed the last post, I'd suggest checking it out. It'll only take a few minutes for you to catch up but if you can also download the source code on Github.

Installing Breeze

Just as it was when installing the Laravel framework, adding Breeze is incredibly simple. Just a few commands will add everything that we need.

composer require laravel/breeze --dev
php artisan breeze:install
npm install
npm run dev

The breeze:install command will add a ton of files to your project structure. In the resources folder, you'll see several views added that are specific to authentication. These are things like login, register, or password reset views. You'll have a new components folder also. These are Laravel Components which are blade templates but with some additional horsepower that work together to make them more programmatic in nature. We won't go into detail on them here so if you want to learn more, take a look at the docs. We have a layouts folder for our navigation as well as layouts for guests and authenticated users. Finally, dashboard.blade.php will be added to the root level of our views folder. This is the view that is returned when a user successfully login into the application. There are corresponding classes in app\View\Components.

Several controllers have been added to app\Http\Controllers\Auth as well as a LoginRequest in app\Http\Requests\Auth. We have new routes added via routes/auth.php and in routes/web.php. Our RouteServiceProvider has also been updated to redirect users to /dashboard once they authenticate. There are also tests for our authentication process but we'll get to those in a minute. Before we get to testing, we'll need to visit some files that Breeze overwrites and merge the new changes with our previous versions of the files.

In our package.json, you'll see that some of the versions of packages that we already had installed might have changed. These aren't too important so I'll leave it up to you if you want to revert to our previous versions or not. What is important is that a few new packages have been added. @tailwindcss/forms, alpinejs, and postcss-import are all needed by the Breeze starter kit. Making use of these new packages requires changes to app.js, webpack.mix.js, and tailwind.config.js.

// resources/js/app.js
require("./bootstrap");
require('alpinejs');

import { createApp, h } from "vue";
...
// webpack.mix.js
...
mix.js('resources/js/app.js', 'public/js').vue();
mix.postCss('resources/css/app.css', 'public/css', [
        require('postcss-import'),
        require('tailwindcss'),
        require('autoprefixer'),
    ]);
// tailwind.config.js
const defaultTheme = require('tailwindcss/defaultTheme');

module.exports = {
    purge: ['./storage/framework/views/*.php', './resources/views/**/*.blade.php'],
    darkMode: false, // or 'media' or 'class'
    theme: {
        extend: {
            fontFamily: {
                sans: ['Nunito', ...defaultTheme.fontFamily.sans],
            },
        },
    },
    variants: {
        extend: {
            opacity: ['disabled'],
        },
    },
    plugins: [require('@tailwindcss/forms')],
};

The changes to the first two files are pretty simple. We are just adding alpinejs to our main javascript file and including postcss-import and autoprefixer in our CSS build process. The tailwind.config.js changes are a little bigger although the only one that is truly necessary is adding the official tailwind forms plugin to our configuration. The last change to mention is in resources/css/app.css. Breeze updates this file to use an alternate syntax for including the tailwind base styles. I like the brevity of the @tailwind syntax, but either option works.

Let's add links for the login and register pages to our welcome view.

// resources/views/welcome.blade.php
...
<body>
    @if (Route::has('login'))
    <div class="hidden fixed top-0 right-0 px-6 py-4 sm:block">
        @auth
        <a href="{{ url('/dashboard') }}" class="text-sm text-gray-700 underline">Dashboard</a>
        @else
        <a href="{{ route('login') }}" class="text-sm text-gray-700 underline">Login</a>

        @if (Route::has('register'))
        <a href="{{ route('register') }}" class="ml-4 text-sm text-gray-700 underline">Register</a>
        @endif
        @endauth
    </div>
    @endif

    <div id="app" class="flex items-center bg-gray-100 min-h-screen">
        <home />
    </div>
...

You should see the links in the top right of our welcome view. We'll want to register a new user but first, we need to create a database for our application. Once it is set up, run php artisan migrate and then we can register our user. After registering you should be redirected to the dashboard which was created by Breeze. Our authentication system is in place!

Configuring PHPUnit

Laravel has some great tools which extend the functionality of PHPUnit making TDD an excellent fit for any Laravel project. We'll just make a couple of extra config changes before we move onto the javascript portion of our testing setup. In our phpunit.xml file, let's uncomment the 2 entries within the PHP section so our tests will use sqlite in memory when testing.

// phpunit.xml
...
    <php>
        <server name="APP_ENV" value="testing"/>
        <server name="BCRYPT_ROUNDS" value="4"/>
        <server name="CACHE_DRIVER" value="array"/>
        <server name="DB_CONNECTION" value="sqlite"/>
        <server name="DB_DATABASE" value=":memory:"/>
        <server name="MAIL_MAILER" value="array"/>
        <server name="QUEUE_CONNECTION" value="sync"/>
        <server name="SESSION_DRIVER" value="array"/>
        <server name="TELESCOPE_ENABLED" value="false"/>
    </php>
...

You will be able to run the tests using vendor/bin/phpunit but Laravel has an awesome feature called parallel testing which greatly decreases the testing time of large test suites by running tests on multiple cores. There might not be a significant improvement at this point but we'll add it regardless. Running php artisan test --parallel will prompt you to install brianium/paratest. Once it is installed, the same command will run the test suite for us.

php artisan test --parallel

Testing with Jest

Javascript testing via Jest

The last goal we want to accomplish in this version of our project template is having automated testing capabilities for our frontend. There are lots of ways to do this in the javascript ecosystem. We will use Jest as our main testing tool and Vue Test Utils to allow testing of our Vue single file components (SFCs).

We can add all the packages we need in one go by running the following command.

npm install --save-dev jest vue-jest@~5.0.0-alpha babel-jest @babel/core @babel/preset-env @vue/test-utils@next

We need some extra configuration for Jest and Babel to get these packages working together to provide us a usable testing environment. You can either add new config files specific to each package in your root directory (jest.config.js and babel.config.js) or just add the relevant sections to package.json.

// package.json
...
"jest": {
        "testRegex": "tests/components/.*.test.js$",
        "moduleFileExtensions": [
            "js",
            "vue"
        ],
        "moduleNameMapper": {
            "^@/(.*)$": "<rootDir>/resources/js/$1"
        },
        "transform": {
            "^.+\\.js$": "<rootDir>/node_modules/babel-jest",
            ".*\\.vue$": "<rootDir>/node_modules/vue-jest"
        }
    },
    "babel": {
        "presets": [
            [
                "@babel/preset-env",
                {
                    "targets": {
                        "node": "current"
                    }
                }
            ]
        ]
    }

At the very beginning of our Jest configuration, we have an entry for testRegex. This is the pattern that Jest will use to find our test files. I am going to add a new folder called components inside of the tests folder. Now Jest should find any files in this folder that end with test.js. Another common standard to use is spec.js. Let's add a simple test for our Home component.

// tests/components/Home.test.js
import Home from '../../resources/js/components/Home.vue';
import { mount } from '@vue/test-utils'

test('displays message', () => {
  const wrapper = mount(Home);

  expect(wrapper.text()).toContain('Home component')
});

The last step is to add scripts to run Jest against our test suite.

// package.json
...
"scripts": {
        "dev": "npm run development",
        "development": "mix",
        "watch": "mix watch",
        "watch-poll": "mix watch -- --watch-options-poll=1000",
        "hot": "mix watch --hot",
        "prod": "npm run production",
        "production": "mix --production",
        "test": "jest",
        "testing": "jest --watch"
    },
...

Let's see it in action.

npm run test

Testing with Jest

Conclusion and next steps

While the basic version of our project template is helpful to simplify the start-up process of the most basic experiments, this version with authentication and a working test driven development workflow is much more practical for a real project. Therefore, we are saving much more time getting up and running with a template like this one. You can find the working template on Github once again. Next time we are going to set up a single page application template for projects that need more interactivity with the user.