Test if a view partial is loaded in a phpunit test

Published 5 days ago by Roni

Is there a way to access a segment of a View response object? I'm trying to test if a view partial has been loaded in a less brittle way than just using see.

when I dd the response object I can find the view list

 #finder: Illuminate\View\FileViewFinder {#120
          #files: Illuminate\Filesystem\Filesystem {#121}
          #paths: array:1 [
            0 => "/Users/roni/code/php/laravel/5.7/munroe/resources/views"
          ]
          #views: array:13 [
            "static.new-front" => "/Users/roni/code/php/laravel/5.7/munroe/resources/views/static/new-front.blade.php"
            "segments.front.v2.events.upcomming-conferences" => "/Users/roni/code/php/laravel/5.7/munroe/resources/views/segments/front/v2/events/upcomming-conferences.blade.php"
            "segments.front.v2.weight-management" => "/Users/roni/code/php/laravel/5.7/munroe/resources/views/segments/front/v2/weight-management.blade.php"
            "segments.front.v2.notifications.sale-free-registration" => "/Users/roni/code/php/laravel/5.7/munroe/resources/views/segments/front/v2/notifications/sale-free-registration.php"
            "segments.front.v2.compression-therapy" => "/Users/roni/code/php/laravel/5.7/munroe/resources/views/segments/front/v2/compression-therapy.blade.php"
            "layouts.new-front-with-nav" => "/Users/roni/code/php/laravel/5.7/munroe/resources/views/layouts/new-front-with-nav.blade.php"
            "segments.front.v2.navbar" => "/Users/roni/code/php/laravel/5.7/munroe/resources/views/segments/front/v2/navbar.blade.php"
            "segments.front.v2.reviews.div" => "/Users/roni/code/php/laravel/5.7/munroe/resources/views/segments/front/v2/reviews/div.blade.php"
            "segments.front.v2.newsletter" => "/Users/roni/code/php/laravel/5.7/munroe/resources/views/segments/front/v2/newsletter.blade.php"
            "segments.front.map" => "/Users/roni/code/php/laravel/5.7/munroe/resources/views/segments/front/map.blade.php"
            "segments.front.v2.footer" => "/Users/roni/code/php/laravel/5.7/munroe/resources/views/segments/front/v2/footer.blade.php"
            "segments.scripts.front.v2.navbar" => "/Users/roni/code/php/laravel/5.7/munroe/resources/views/segments/scripts/front/v2/navbar.blade.php"
            "segments.scripts.front.v2.newsletter" => "/Users/roni/code/php/laravel/5.7/munroe/resources/views/segments/scripts/front/v2/newsletter.blade.php"
          ]
          #hints: array:2 [
            "notifications" => array:1 [
              0 => "/Users/roni/code/php/laravel/5.7/munroe/vendor/laravel/framework/src/Illuminate/Notifications/resources/views"
            ]
            "pagination" => array:1 [
              0 => "/Users/roni/code/php/laravel/5.7/munroe/vendor/laravel/framework/src/Illuminate/Pagination/resources/views"
            ]
          ]

but I can't figure out how to isolate that portion of the view object and test against it. Any advice is welcome.

Best Answer (As Selected By Roni)
click

@Roni ah shit, shame on me. But I think I've got a solution now:

Update You know what, here a gist for the trait: https://gist.github.com/kaphert/6845c03472b1d0c52ce5782219363cb5

Usage:

/** @test */
public function should_load_some_view()
{
    $this->expectViewFiles('path.to.view');
    $this->get('http://yoursite.test/some-page');
}

/** @test */
public function should_not_load_some_view()
{
    $this->doesntExpectViewFiles('path.to.view');
    $this->get('http://yoursite.test/some-page');
}

/** @test */
public function should_load_all_this_views()
{
    $this->expectViewFiles('path.to.view', 'path.to.other.view');
    // or: $this->expectViewFiles(['path.to.view', 'path.to.other.view']);
    $this->get('http://yoursite.test/some-page');
}

/** @test */
public function should_not_load_any_of_these_views()
{
    $this->doesntExpectViewFiles('path.to.view', 'path.to.other.view');
    // or: $this->doesntExpectViewFiles(['path.to.view', 'path.to.other.view']);
    $this->get('http://yoursite.test/some-page');
}

Trait file: ViewAssertions.php

/**
 * Laravel + PHPUnit assert that blade files are being loaded. 
 *
 * Trait AssertView
 */
trait ViewAssertions
{
    protected $__loadedViews;

    protected function captureLoadedViews()
    {
        if (!isset($this->__loadedViews)) {
            $this->__loadedViews = [];
            $this->app['events']->listen('composing:*', function ($view, $data = []) {
                if ($data) {
                    $view = $data[0]; // For Laravel >= 5.4
                }
                $this->__loadedViews[] = $view->getName();
            }
            );
        }
    }

    /**
     * Assert that all of the given views are loaded.
     *  - expectViewFiles('path.to.view')
     *  - expectViewFiles(['path.to.view', 'path.to.other.view'])
     *  - expectViewFiles('path.to.view', 'path.to.other.view')
     *
     * @param string|array $paths
     */
    public function expectViewFiles($paths)
    {
        $paths = is_array($paths) ? $paths : func_get_args();

        $this->captureLoadedViews();

        $this->beforeApplicationDestroyed(function () use ($paths) {
            $this->assertEmpty(
                $viewsLoaded = array_diff($paths, $this->__loadedViews),
                'These expected view files were not loaded: [' . implode(', ', $viewsLoaded) . ']'
            );
        });
    }

    /**
     * Assert that none of the given views are loaded.
     *  - doesntExpectViewFiles('path.to.view')
     *  - doesntExpectViewFiles(['path.to.view', 'path.to.other.view'])
     *  - doesntExpectViewFiles('path.to.view', 'path.to.other.view')
     *
     * @param string|array $paths
     */
    public function doesntExpectViewFiles($paths)
    {
        $paths = is_array($paths) ? $paths : func_get_args();

        $this->captureLoadedViews();

        $this->beforeApplicationDestroyed(function () use ($paths) {
            $this->assertEmpty(
                $viewsLoaded = array_intersect($this->__loadedViews, $paths),
                'These unexpected view files were loaded: ['.implode(', ', $viewsLoaded).']'
            );
        });
    }
}
click
click
5 days ago (74,420 XP)

If you lookup the class you mention here you see that it has a find() method where you can search for the view name. I think you can use that method.

See: https://github.com/laravel/framework/blob/9f313ce9bb5ad49a06ae78d33fbdd1c92a0e21f6/src/Illuminate/View/FileViewFinder.php#L63-L80

Tray2
Tray2
5 days ago (102,250 XP)

What I usually do it that I have something unique in the html that a look for.

Like looking for the tag <footer>

$response = $this->get('/whatever');
$response->assertSee('<footer>');
Roni
Roni
5 days ago (64,150 XP)

@Tray2, I use that when I'm the designer, and the developer, but sometimes I've noticed during design updates where I'm not always on point that trying to semantically test on html is breaking, even if the code is fine. But the unit test lost what it was looking for. However, normally no one will change the blade or blade partial names. so I thought this would be a nicer solution.

@click thanks for the pointer, trying it now!

Roni
Roni
5 days ago (64,150 XP)

@click, thats actually just a buried part of the response object, basically I can find it, I just dont know how to find it :)

meaning for example in a unit test I'm not sure where to hook into it for example


$this->assertContains('some.blade.partial', $response->original->whatGoesHere());

I've been source diving for a couple of hours going through illuminate\Foundation\Response Object and it's symfony http-foundation set. But I can't figure out how to call it.

click
click
5 days ago (74,420 XP)

@Roni I've did some testing and this seems to work:

$response = $this->get('https://myapp.local/login');
$this->assertTrue($response->original->getFactory()->exists('auth.login'));

Maybe you could create your own assertion methods to make it easier to read:

    public function assertViewLoaded($response, $path)
    {
        $this->assertTrue(
            $response->original->getFactory()->exists($path),
            'Blade view file `'. $path .'` not loaded'
        );
    }

    public function assertViewNotLoaded($response, $path)
    {
        $this->assertFalse(
            $response->original->getFactory()->exists($path),
            'Blade view file `'. $path .'` is loaded'
        );
    }

Above code would look like:

$response = $this->get('https://myapp.local/login');
$this->assertViewLoaded('auth.login');

note: it does fail if the response is a redirect, 404 not found etc. If you want to catch that you should add extra code to see if the original is available before you ask for the getFactory() otherwise you get an Error : Call to a member function getFactory() on null exception

Roni
Roni
5 days ago (64,150 XP)

A+ for effort! You are awesome! However I can't make this work, in your case it's the main view not the view partial that you are checking. like using something from @include('some.partial');

For the main view an easier method is assertViewIs('auth.login'); It's a pre-baked assertion in laravel 5.7 and probably earlier, as the name is available as the last key in the view response.

The thing thats killing me is all the other blade segments or partials are stored there too. I just can't touch them because I can't find an accessor or a way to intercept the data.

I was hopeful on getFactory()->getFinder() they are all stored right there in $views, but there is no way to sneak them out.

Roni
Roni
5 days ago (64,150 XP)

Actully the exists has another sneaky caveat, it will give you a false positive, it's only chekcing for view existance, not restricting it to your open views :(

click
click
5 days ago (74,420 XP)

@Roni ah shit, shame on me. But I think I've got a solution now:

Update You know what, here a gist for the trait: https://gist.github.com/kaphert/6845c03472b1d0c52ce5782219363cb5

Usage:

/** @test */
public function should_load_some_view()
{
    $this->expectViewFiles('path.to.view');
    $this->get('http://yoursite.test/some-page');
}

/** @test */
public function should_not_load_some_view()
{
    $this->doesntExpectViewFiles('path.to.view');
    $this->get('http://yoursite.test/some-page');
}

/** @test */
public function should_load_all_this_views()
{
    $this->expectViewFiles('path.to.view', 'path.to.other.view');
    // or: $this->expectViewFiles(['path.to.view', 'path.to.other.view']);
    $this->get('http://yoursite.test/some-page');
}

/** @test */
public function should_not_load_any_of_these_views()
{
    $this->doesntExpectViewFiles('path.to.view', 'path.to.other.view');
    // or: $this->doesntExpectViewFiles(['path.to.view', 'path.to.other.view']);
    $this->get('http://yoursite.test/some-page');
}

Trait file: ViewAssertions.php

/**
 * Laravel + PHPUnit assert that blade files are being loaded. 
 *
 * Trait AssertView
 */
trait ViewAssertions
{
    protected $__loadedViews;

    protected function captureLoadedViews()
    {
        if (!isset($this->__loadedViews)) {
            $this->__loadedViews = [];
            $this->app['events']->listen('composing:*', function ($view, $data = []) {
                if ($data) {
                    $view = $data[0]; // For Laravel >= 5.4
                }
                $this->__loadedViews[] = $view->getName();
            }
            );
        }
    }

    /**
     * Assert that all of the given views are loaded.
     *  - expectViewFiles('path.to.view')
     *  - expectViewFiles(['path.to.view', 'path.to.other.view'])
     *  - expectViewFiles('path.to.view', 'path.to.other.view')
     *
     * @param string|array $paths
     */
    public function expectViewFiles($paths)
    {
        $paths = is_array($paths) ? $paths : func_get_args();

        $this->captureLoadedViews();

        $this->beforeApplicationDestroyed(function () use ($paths) {
            $this->assertEmpty(
                $viewsLoaded = array_diff($paths, $this->__loadedViews),
                'These expected view files were not loaded: [' . implode(', ', $viewsLoaded) . ']'
            );
        });
    }

    /**
     * Assert that none of the given views are loaded.
     *  - doesntExpectViewFiles('path.to.view')
     *  - doesntExpectViewFiles(['path.to.view', 'path.to.other.view'])
     *  - doesntExpectViewFiles('path.to.view', 'path.to.other.view')
     *
     * @param string|array $paths
     */
    public function doesntExpectViewFiles($paths)
    {
        $paths = is_array($paths) ? $paths : func_get_args();

        $this->captureLoadedViews();

        $this->beforeApplicationDestroyed(function () use ($paths) {
            $this->assertEmpty(
                $viewsLoaded = array_intersect($this->__loadedViews, $paths),
                'These unexpected view files were loaded: ['.implode(', ', $viewsLoaded).']'
            );
        });
    }
}
Roni
Roni
5 days ago (64,150 XP)

@click BOOM! Love it! I think this will play a major role in speeding up rapid my development, but the major benefit is going to really shine in change update request and task delegation! Just having the confidence to know your view is being hit, while you can easily test the data sets passed to the view separately.

click
click
5 days ago (74,420 XP)

Great! ;-)

Please sign in or create an account to participate in this conversation.