Behat Laravel Domain Testing Inside Out

Posted: 2016-08-10 01:14:58

I will cover the use of FormRequest, Mockery, Behat and more in this article.

The article will show how to test your Domain code, in this case I mean testing how the code will work outside of the Routing and Controller layer of your Application. But what this will lead to is a "lego" like moment of plugging in these parts to your Controller so you know it will work. Of course it will be tested as well during the UI testing.

I will start off with this Behat Feature

Feature: Login Page
  Login page to do authenticated tasks
  As an anonymous user
  So we can protect some personal and administrative parts of the site

  @happy_path @smoke @javascript @profile
  Scenario: A user can login and see their profile
    Given I am on the login page
    And I fill in the login form with my proper username and password
    Then I should be able to see my profile page
    Then if I try to see another persons page I should get rejected

  @smoke @profile
  Scenario: A non logged in user can not get a profile
    Given I am an anonymous user
    And I go to the profile page
    Then I should get redirected with an error message to let me know the problem

What we have is the @happy_path that will be the UI Scenario in this case as well as the Domain. And yes I can have more than one UI Scenario but in this case I just want to deliver something that let's the Product Owner knows her application is working as expected.

What we will start with is Scenario: A user can login and see their profile

In my example there will be a Repository class to manage the Business logic and a FormRequest Class to deal with managing ACL.

Let's take the Context file one step at a time features/bootstrap/LoginPageDomainContext.php

    /**
     * @Given I am on the login page
     */
    public function iAmOnTheLoginPage()
    {
        $this->user = factory(\App\User::class)->create();
    }

I setup the state of the world for this Scenario I do not rely on seed data.

Also at the top of the Class I pull in use \Laracasts\Behat\Context\DatabaseTransactions; and \Laracasts\Behat\Context\Migrator

These come from the Laracast Library on Behat https://laracasts.com/lessons/laravel-5-and-behat-bffs and https://github.com/laracasts/Behat-Laravel-Extension

This next step is written with too much "Web" in mind. I should try and reword it so it fits more inline with the business goals.

    /**
     * @Given I fill in the login form with my proper username and password
     */
    public function iFillInTheLoginFormWithMyProperUsernameAndPassword()
    {
        Auth::login($this->user);
        PHPUnit_Framework_Assert::assertFalse(Auth::guest());
    }

Here is where things get fun

    /**
     * @Then I should be able to see my profile page
     */
    public function iShouldBeAbleToSeeMyProfilePage()
    {
        $user = factory(\App\User::class)->create();
        factory(\App\Profile::class)->create(['user_id' => $user->id]);

        /** @var \App\Http\Requests\ProfileShowRequest $auth */
        $auth = Mockery::mock(\App\Http\Requests\ProfileShowRequest::class)->makePartial();
        $auth->shouldReceive('route')->andReturn($user->url);
        $results = $auth->authorize();

        PHPUnit::assertTrue($results);
    }

And the next part to that

    /**
     * @Then if I try to see another persons page I should get rejected
     */
    public function ifITryToSeeAnotherPersonsPageIShouldGetRejected()
    {
        $user = factory(\App\User::class)->create();

        factory(\App\Profile::class)->create(
            ['favorite_comic_character' => "foo", 'user_id' => $user->id]
        );
        /** @var \App\Http\Requests\ProfileShowRequest $auth */
        $auth = Mockery::mock(\App\Http\Requests\ProfileShowRequest::class)->makePartial();
        $auth->shouldReceive('route')->andReturn($user->url);
        $results = $auth->authorize();

        PHPUnit::assertFalse($results);
    }

Making sure to close my Mockery when done

    /**
     * @afterScenario
     */
    public function cleanUp() {
        Mockery::close();
    }

We are testing the two pieces that matter right now, the Repo and the FormRequest.

All I did was run vendor/bin/behat -slogin_domain --append-snippets --tags=@happy_path and I got to see the one(s) with that tag working.

The Repo

\App\Repositories\ProfileShowPage

<?php
namespace App\Repositories;

use App\Profile;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Facades\Auth;

class ProfileShowPage extends ProfileRepository
{


    public function showProfileForUserFromSlug($slug)
    {
        return \App\User::fromSlug($slug);
    }
}

Why not just use the Controller! Well this is still a simple simple example but honestly if you are just building something for fun or something serious and just getting the feel of an idea go for it. I am doing this cause I want to plug in more to this later since it is part of a larger plan.

Note: Profile::fromSlug() is just a scope and the model works out the rest of the query for me

    public function scopeFromSlug($query, $slug)
    {
        return $query->with('profile')->where('url', $slug)->firstOrFail();
    }

That is it for the repository, now how about this FormRequest at app/Http/Requests/ProfileShowRequest.php?

FormRequest

You can read more about this here on Laravel Docs

<?php

namespace App\Http\Requests;

/**
 * @NOTE THERE ARE SOME CLASSES HERE SEE GITHUB
 */

class ProfileShowRequest extends Request
{

    protected $profile;

    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        if (Auth::guest()) {
            return false;
        }
        /** @var \App\Repositories\ProfileShowPage $profilePage */
        $profilePage = App::make(\App\Repositories\ProfileShowPage::class);

        $user = $profilePage->showProfileForUserFromSlug($this->route('slug'));

        return Auth::user()->id == $user->profile->user_id;
    }

    public function forbiddenResponse()
    {
        return redirect('login')->with('message', "You need to login first");
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            //
        ];
    }
}

The Controller

All right now that step is passing and the pieces are ready to plug in to the Controller!

<?php

namespace App\Http\Controllers;

use App\Http\Requests\ProfileShowRequest;
use App\Repositories\ProfileShowPage;
use Illuminate\Database\Eloquent\ModelNotFoundException;

class ProfileShowController extends Controller
{


    public function getProfileForUserUsingSlug(ProfileShowRequest $request, ProfileShowPage $repository, $slug)
    {
        try {
            $profile = $repository->showProfileForUserFromSlug($slug);

            return view('profile.show', compact('profile'));
        } catch (ModelNotFoundException $e) {
            return redirect()->route('home')->with('message', "Could not find your profile :(");
        } catch (\Exception $e) {
            return redirect()->route('home')->with('message', "Error getting profile :(");
        }
    }
}

This controller has one Job app/Http/Controllers/ProfileShowController.php Showing the profile.

In with that job it has the ACL taken care of before it even has to think about what it needs to do.

After that it asks the repo to take care of the request and it deals with the response or inevitable exception!

So right away I can go to my ui test and make it so you can see the code here

Follow Up

All code for this can be seen at https://github.com/alnutile/recipes this is a foundation repo for a new book coming out in 2 months with many day to day Laravel Recipes!