18

I am trying to write unit tests for my middleware in Laravel. Does anyone know a tutorial, or have an example of this ?

I have been writing a lot of code, but there must be a better way to test the handle method.

Nealv
  • 6,856
  • 8
  • 58
  • 89

4 Answers4

16

Using Laravel 5.2, I am unit testing my middleware by passing it a request with input and a closure with assertions.

So I have a middleware class GetCommandFromSlack that parses the first word of the text field in my Post (the text from a Slack slash command) into a new field called command, then modifies the text field to not have that first word any more. It has one method with the following signature: public function handle(\Illuminate\Http\Request $request, Closure $next).

My Test case then looks like this:

use App\Http\Middleware\GetCommandFromSlack;
use Illuminate\Http\Request;

class CommandsFromSlackTest extends TestCase
{
  public function testShouldKnowLiftCommand()
  {
    $request = new Illuminate\Http\Request();
    $request->replace([
        'text' => 'lift foo bar baz',
    ]);
    $mw = new \App\Http\Middleware\GetCommandFromSlack;
    $mw->handle($request,function($r) use ($after){
      $this->assertEquals('lift',       $r->input('command'));
      $this->assertEquals('foo bar baz',$r->input('text'));
    });
  }
}

I hope that helps! I'll try to update this if I get more complicated middleware working.

Kirkland
  • 3,257
  • 1
  • 16
  • 17
perkee
  • 161
  • 1
  • 3
4

To actually test the middleware class itself you can do:

public function testHandle()
{

    $user = new User(['email'=>'...','name'=>'...']);
    /**
     * setting is_admin to 1 which means the is Admin middleware should 
     * let him pass, but oc depends on your handle() method
     */
    $user->is_admin = 1;        
    $model = $this->app['config']['auth.model'];
    /**
     * assuming you use Eloquent for your User model
     */
    $userProvider = new \Illuminate\Auth\EloquentUserProvider($this->app['hash'], $model);

    $guard = new \Illuminate\Auth\Guard($userProvider, $this->app['session.store']);
    $guard->setUser($user);

    $request = new \Illuminate\Http\Request();        
    $middleware = new \YourApp\Http\Middleware\AuthenticateAdmin($guard);        

    $result = $middleware->handle($request, function(){ return 'can access';});
    $this->assertEquals('can access',$result);

}
chickenchilli
  • 3,358
  • 2
  • 23
  • 24
  • 1
    In 5.1 probably need to pass $request as the final parameter in the constructor for $guard so need to reorder the initialisations a bit. Also using config('auth.model') and app()->hash are a little more Laravelly :) – markdwhite Nov 24 '16 at 08:44
2

I thinking the best solution is just checking what happened after middleware. For example, the authentication middleware:

<?php namespace App\Http\Middleware;

use Closure;
use Illuminate\Contracts\Auth\Guard;

class Authenticate {

    /**
     * The Guard implementation.
     *
     * @var Guard
     */
    protected $auth;

    /**
     * Create a new filter instance.
     *
     * @param  Guard  $auth
     * @return void
     */
    public function __construct(Guard $auth)
    {
        $this->auth = $auth;
    }

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        if ($this->auth->guest())
        {
            if ($request->ajax())
            {
                return response('Unauthorized.', 401);
            }
            else
            {
                return redirect()->guest('auth/login');
            }
        }

        return $next($request);
    }

}

And my test unit:

<?php

class AuthenticationTest extends TestCase {

    public function testIAmLoggedIn()
    {
        // Login as someone
        $user = new User(['name' => 'Admin']);
        $this->be($user);

        // Call as AJAX request.
        $this->client->setServerParameter('HTTP_X-Requested-With', 'XMLHttpRequest');
        $this->call('get', '/authpage');

        $this->assertEquals(200, $response->getStatusCode());
    }

}

I would do it in that way.

Siper
  • 1,175
  • 1
  • 14
  • 39
  • Though it is mostly fine, but it has fallacy that we are working somewhat indirectly that we are checking behavior rather than class itself. Issue would arise if the behavior is slightly changed then tests will fail. – Sachin Sharma May 17 '16 at 21:45
  • `->be(...)` doesn;t work in laravel 5.1 it should be `->actingAs(...)` – tread Jul 31 '16 at 14:16
  • @SachinSharma I know your comment is over a year old, but I wanted to say I disagree with you. It's not a fallacy, it's an advantage. If you're testing the class directly your test is more coupled to the class under test than it should. If you test behavior, you can *completely* refactor your back-end and still know your test returns green. If you couple the test to the class - you'll have to change/refactor the test itself as well. Cheers. – karni Sep 19 '17 at 15:20
2

I was working on a localization Middleware that sets the app locale based on a URI segment, e.g. http://example.com/ar/foo should set the app local to Arabic. I basically mocked the Request object and tested as normal. Here is my test class:

use Illuminate\Http\Request;
use App\Http\Middleware\Localize;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;

class LocalizeMiddlewareTest extends TestCase
{
    protected $request;
    protected $localize;

    public function setUp()
    {
        parent::setUp();

        config(['locale' => 'en']);
        config(['app.supported_locales' => ['en', 'ar']]);

        $this->request = Mockery::mock(Request::class);

        $this->localize = new Localize;
    }

    /** @test */
    public function it_sets_the_app_locale_from_the_current_uri()
    {
        $this->request->shouldReceive('segment')->once()->andReturn('ar');

        $this->localize->handle($this->request, function () {});

        $this->assertEquals('ar', app()->getLocale());
    }

    /** @test */
    public function it_allows_designating_the_locale_uri_segment()
    {
        $this->request->shouldReceive('segment')->with(2)->once()->andReturn('ar');

        $this->localize->handle($this->request, function () {}, 2);

        $this->assertEquals('ar', app()->getLocale());
    }

    /** @test */
    public function it_throws_an_exception_if_locale_is_unsupported()
    {
        $this->request->shouldReceive('segment')->once()->andReturn('it');
        $this->request->shouldReceive('url')->once()->andReturn('http://example.com/it/foo');

        $this->setExpectedException(
            Exception::class,
            "Locale `it` in URL `http://example.com/it/foo` is not supported."
        );

        $this->localize->handle($this->request, function () {});
    }
}

And here is my Middleware class:

namespace App\Http\Middleware;

use Closure;

class Localize
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  integer  $localeUriSegment
     * @return mixed
     */
    public function handle($request, Closure $next, $localeUriSegment = 1)
    {
        $locale = $request->segment($localeUriSegment);

        if (in_array($locale, config('app.supported_locales')))
        {
            app()->setLocale($locale);
        }
        else
        {
            abort(500, "Locale `{$locale}` in URL `".$request->url().'` is not supported.');
        }

        return $next($request);
    }
}

Hope that helps :)