Roni's avatar
Level 33

Test if a view partial is loaded in a phpunit test

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.

0 likes
10 replies
Tray2's avatar

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>');
1 like
Roni's avatar
Level 33

@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!

1 like
Roni's avatar
Level 33

@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's avatar

@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

1 like
Roni's avatar
Level 33

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's avatar
Level 33

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's avatar
click
Best Answer
Level 35

@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).']'
            );
        });
    }
}
3 likes
Roni's avatar
Level 33

@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.

Please or to participate in this conversation.