Behat Laravel Domain Testing Inside Out
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!
comments powered by Disqus