rhand's avatar
Level 6

test for registration display and if display test validation

I would like to check if our registration has been activated and if not display this in the test. In the case it is not active the form is not shown and a test will be shown. So for the test I had

<?php

namespace Tests\Feature\Http\Controllers\Auth;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;

class RegisterControllerTest extends TestCase
{
    /**
     * Check for active registration
     *
     * @return void
     */
    /** @test */
    
    public function registration_not_active()
    {
        $response = $this->get(route('register'));
        $response->assertStatus(500);
        $this->assertTrue(true);
    }

    public function registration_active() 
    {
    
        $response = $this->get(route('register'));
        $response->assertStatus(200);
    
        $response->assertViewIs('auth.register');
    }

}

but it only runs the display test showing the registration has not been activated even if that is not true. How come?

When I test with showing exceptions I see

⨯ registration not active

  ---

  • Tests\Feature\Http\Controllers\Auth\RegisterControllerTest > registration not active
   Illuminate\View\ViewException 

  SQLSTATE[HY000]: General error: 1 no such table: settings (SQL: select * from "settings" where "feature" = register limit 1) (View: /Users/me/code/site.com/resources/views/auth/register.blade.php)

  at vendor/laravel/framework/src/Illuminate/Database/Connection.php:759
    755▕         // If an exception occurs when attempting to run a query, we'll format the error
    756▕         // message to include the bindings with SQL, which will make this exception a
    757▕         // lot more helpful to the developer instead of just the database's errors.
    758▕         catch (Exception $e) {
  ➜ 759▕             throw new QueryException(
    760▕                 $query, $this->prepareBindings($bindings), $e
    761▕             );
    762▕         }
    763▕     }

      +14 vendor frames 
  15  app/Models/Setting.php:16
      Illuminate\Database\Eloquent\Builder::first()

  16  storage/framework/views/84de924690fd199ca5ce25f0291c19efdc00c175.php:5
      App\Models\Setting::checkActive("register")


  Tests:  1 failed, 5 passed
  Time:   0.16s

But how can I then test for no form display without testing for database entries made to activate or not activate registration?

0 likes
14 replies
Sinnbeck's avatar

You are missing this on the second test

/** @test */

But just to understand. You want one test to be dependent on the other?

1 like
Sinnbeck's avatar

Also the error shows that you are missing a settings table. Do you have

use RefreshDatabase; 
1 like
rhand's avatar
Level 6

@Sinnbeck True. And when I added that both methods failed inside this registerController test:

• Tests\Feature\Http\Controllers\Auth\RegisterControllerTest > registration not active
   Illuminate\View\ViewException 

  SQLSTATE[HY000]: General error: 1 no such table: settings (SQL: select * from "settings" where "feature" = register limit 1) (View: /Users/me/code/site.com/resources/views/auth/register.blade.php)

  at vendor/laravel/framework/src/Illuminate/Database/Connection.php:759
    755▕         // If an exception occurs when attempting to run a query, we'll format the error
    756▕         // message to include the bindings with SQL, which will make this exception a
    757▕         // lot more helpful to the developer instead of just the database's errors.
    758▕         catch (Exception $e) {
  ➜ 759▕             throw new QueryException(
    760▕                 $query, $this->prepareBindings($bindings), $e
    761▕             );
    762▕         }
    763▕     }

      +14 vendor frames 
  15  app/Models/Setting.php:16
      Illuminate\Database\Eloquent\Builder::first()

  16  storage/framework/views/84de924690fd199ca5ce25f0291c19efdc00c175.php:5
      App\Models\Setting::checkActive("register")

  • Tests\Feature\Http\Controllers\Auth\RegisterControllerTest > registration active
  Expected response status code [200] but received 500.
  
  The following exception occurred during the request:
  
  PDOException: SQLSTATE[HY000]: General error: 1 no such table: settings in /Users/me/code/site.com/vendor/laravel/framework/src/Illuminate/Database/Connection.php:413
  Stack trace:
  #0 /Users/me/code/site.com/vendor/laravel/framework/src/Illuminate/Database/Connection.php(413): PDO->prepare('select * from "...')
....

And the no such table: settings is rather odd because the tablet does exist and model

<?php

namespace App\Models;

use App\Models;
use Illuminate\Database\Eloquent\Model;

class Setting extends Model
{
    protected $fillable = [
        'feature', 'active',
    ];

    public static function checkActive($key)
    {
        return  self::where('feature', $key)->first()['active'] ?? true;
    }
}

too

rhand's avatar
Level 6

@Sinnbeck I have use Illuminate\Foundation\Testing\RefreshDatabase;:

<?php

namespace Tests\Feature\Http\Controllers\Auth;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;

class RegisterControllerTest extends TestCase
{
    /**
     * Check for active registration
     *
     * @return void
     */
    /** @test */
    
    public function registration_not_active()
    {
        $this->withoutExceptionHandling();
        $response = $this->get(route('register'));
        $response->assertStatus(500);
        $this->assertTrue(true);
    }

    /** @test */ 
    public function registration_active() 
    {
    
        $response = $this->get(route('register'));
        $response->assertStatus(200);
    
        $response->assertViewIs('auth.register');
    }

}

but perhaps I need to add a factory as well..?

rhand's avatar
Level 6

I am using this phpunit.xml

<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
>
    <testsuites>
        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>
        <testsuite name="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>
    </testsuites>
    <coverage processUncoveredFiles="true">
        <include>
            <directory suffix=".php">./app</directory>
        </include>
    </coverage>
    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="BCRYPT_ROUNDS" value="4"/>
        <env name="CACHE_DRIVER" value="array"/>
        <env name="DB_CONNECTION" value="sqlite"/>
        <env name="DB_DATABASE" value=":memory:"/>
        <env name="MAIL_MAILER" value="array"/>
        <env name="QUEUE_CONNECTION" value="sync"/>
        <env name="SESSION_DRIVER" value="array"/>
        <env name="TELESCOPE_ENABLED" value="false"/>
    </php>
</phpunit>

So assume that would work with database refresh..? Not using separate .env, only one for Dusk env.dusk.local.example besides standard .env and .env.xample

rhand's avatar
Level 6

When I used use RefreshDatabase inside the class

<?php

namespace Tests\Feature\Http\Controllers\Auth;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;

class RegisterControllerTest extends TestCase
{
    use RefreshDatabase;
    /**
     * Check for active registration
     *
     * @return void
     */
    /** @test */
    
    public function registration_not_active()
    {
        $this->withoutExceptionHandling();
        $response = $this->get(route('register'));
        $response->assertStatus(500);
        $this->assertTrue(true);
    }

    /** @test */ 
    public function registration_active() 
    {
    
        $response = $this->get(route('register'));
        $response->assertStatus(200);
    
        $response->assertViewIs('auth.register');
    }

}

I got further

• Tests\Feature\Http\Controllers\Auth\RegisterControllerTest > registration not active
   Illuminate\Database\QueryException 

  SQLSTATE[42S21]: Column already exists: 1060 Duplicate column name 'version' (SQL: alter table `mobilelandscape_grid_modules` add `version` int unsigned null)

  at vendor/laravel/framework/src/Illuminate/Database/Connection.php:759
    755▕         // If an exception occurs when attempting to run a query, we'll format the error
    756▕         // message to include the bindings with SQL, which will make this exception a
    757▕         // lot more helpful to the developer instead of just the database's errors.
    758▕         catch (Exception $e) {
  ➜ 759▕             throw new QueryException(
    760▕                 $query, $this->prepareBindings($bindings), $e
    761▕             );
    762▕         }
    763▕     }

      +8 vendor frames 
  9   database/migrations/2021_05_11_115221_add_version_to_backup.php:17
      Illuminate\Database\Schema\Builder::table("mobilelandscape_grid_modules", Object(Closure))

  • Tests\Feature\Http\Controllers\Auth\RegisterControllerTest > registration active
   Illuminate\Database\QueryException 

  SQLSTATE[42S21]: Column already exists: 1060 Duplicate column name 'version' (SQL: alter table `mobilelandscape_grid_modules` add `version` int unsigned null)

  at vendor/laravel/framework/src/Illuminate/Database/Connection.php:759
    755▕         // If an exception occurs when attempting to run a query, we'll format the error
    756▕         // message to include the bindings with SQL, which will make this exception a
    757▕         // lot more helpful to the developer instead of just the database's errors.
    758▕         catch (Exception $e) {
  ➜ 759▕             throw new QueryException(
    760▕                 $query, $this->prepareBindings($bindings), $e
    761▕             );
    762▕         }
    763▕     }

Mainly complaining about

SQLSTATE[42S21]: Column already exists: 1060 Duplicate column name 'version' (SQL: alter table `mobilelandscape_grid_modules` add `version` int unsigned null)

which I assume is related to a run migration for the test using sqlite in memory. Will need to see how I can remedy that again. I wonder if the schema directory with base built up is skipped and that that is causing issues, but how to avoid that?

Reading some on https://github.com/laravel/framework/issues/36294 and https://github.com/laravel/framework/issues/36523

rhand's avatar
Level 6

For local testing I added a database called test. I also updated database.php using

'test' => [
            'driver' => 'mysql',
            'url' => env('DATABASE_URL'),
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '3306'),
            'database' => env('DB_DATABASE', 'test'),
            'username' => env('DB_USERNAME', 'root'),
            'password' => env('DB_PASSWORD', ''),
            'unix_socket' => env('DB_SOCKET', ''),
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
            'prefix' => '',
            'prefix_indexes' => true,
            'strict' => false,
            'engine' => null,
            'options' => extension_loaded('pdo_mysql') ? array_filter([
                PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
            ]) : [],
        ],

Then I ran php artisan schema:dump --database=test to create a new database dump of the latest just for the testing database.

Then I updated phpunit.xml:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
>
    <testsuites>
        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>
        <testsuite name="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>
    </testsuites>
    <coverage processUncoveredFiles="true">
        <include>
            <directory suffix=".php">./app</directory>
        </include>
    </coverage>
    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="BCRYPT_ROUNDS" value="4"/>
        <env name="CACHE_DRIVER" value="array"/>
        <env name="DB_CONNECTION" value="test"/>
        <env name="DB_DATABASE" value="test"/>
        <env name="MAIL_MAILER" value="array"/>
        <env name="QUEUE_CONNECTION" value="sync"/>
        <env name="SESSION_DRIVER" value="array"/>
        <env name="TELESCOPE_ENABLED" value="false"/>
    </php>
</phpunit>

Testing now started to work better too

php artisan test 

   PASS  Tests\Unit\JsonObjectTest
  ✓ get exists property
  ✓ get undefined property

   PASS  Tests\Feature\Http\Controllers\Auth\LoginControllerTest
  ✓ login displays the login form
  ✓ user cannot view a login form when authenticated
  ✓ login displays validation errors

   FAIL  Tests\Feature\Http\Controllers\Auth\RegisterControllerTest
  ⨯ registration not active
  ✓ registration active

  ---

  • Tests\Feature\Http\Controllers\Auth\RegisterControllerTest > registration not active
  Expected response status code [500] but received 200.
  Failed asserting that 500 is identical to 200.

  at tests/Feature/Http/Controllers/Auth/RegisterControllerTest.php:23
     19▕     public function registration_not_active()
     20▕     {
     21▕         $this->withoutExceptionHandling();
     22▕         $response = $this->get(route('register'));
  ➜  23▕         $response->assertStatus(500);
     24▕         $this->assertTrue(true);
     25▕     }
     26▕ 
     27▕     /** @test */ 


  Tests:  1 failed, 6 passed
  Time:   13.41s

Seem I just need to now improve upon the test when registration is not active to allow for check for active, if not show 500, if it is show 200 and form.

Would prefer sqlite however and memory based as it should be faster, but then I get stuk with a check for table missing I do not get with a dedicated test mysql database

    SQLSTATE[HY000]: General error: 1 no such table: settings (SQL: select * from "settings" where "feature" = register limit 1)
  
  Failed asserting that 200 is identical to 500.

  at tests/Feature/Http/Controllers/Auth/RegisterControllerTest.php:32
     28▕     public function registration_active() 
     29▕     {
     30▕     
     31▕         $response = $this->get(route('register'));
  ➜  32▕         $response->assertStatus(200);
     33▕     
     34▕         $response->assertViewIs('auth.register');
     35▕     }

though the table is part of the mysql schema dump.

MohamedTammam's avatar

@rhand First you need to add

use RefreshDatabase;

As you did to clear the database and run migration after every test.

Second, you need to seed your database with necessary data for each test. https://laravel.com/docs/9.x/database-testing#running-seeders

Third, you shouldn't test for 500 error. That shouldn't be a case that you expect. It means you there's in error in your code and testing it doesn't mean it's safe now.

How you have 2 tests that test the same request and asserting against 2 different results? Of course one of them will not work.

rhand's avatar
Level 6
<?php

namespace Tests\Feature\Http\Controllers\Auth;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;

class RegisterControllerTest extends TestCase
{
    use RefreshDatabase;
    /**
     * Check for active registration
     *
     * @return void
     */
    /** @test */
    
    public function registration_request_works()
    {
        $response = $this->get(route('register'));
        // $this->withoutExceptionHandling();

        if (! \App\Models\Setting::checkActive('register')) {
            $response->assertStatus(500);
        }

        else{
        $response->assertStatus(200);
        $response->assertViewIs('auth.register');
        }
    }

    /** @test */
    public function register_displays_validation_errors()
    {
        // php artisan config:clear
        $response = $this->post('/register', []);

        $response->assertStatus(302);
        $response->assertSessionHasErrors('email');
    }

}

now works with phpunit.xml:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
>
    <testsuites>
        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>
        <testsuite name="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>
    </testsuites>
    <coverage processUncoveredFiles="true">
        <include>
            <directory suffix=".php">./app</directory>
        </include>
    </coverage>
    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="BCRYPT_ROUNDS" value="4"/>
        <env name="CACHE_DRIVER" value="array"/>
        <env name="DB_CONNECTION" value="mysql"/>
        <env name="DB_DATABASE" value="test"/>
        <env name="MAIL_MAILER" value="array"/>
        <env name="QUEUE_CONNECTION" value="sync"/>
        <env name="SESSION_DRIVER" value="array"/>
        <env name="TELESCOPE_ENABLED" value="false"/>
    </php>
</phpunit>

I just wonder why the test is so slow (14 seconds) and if there is no better way to do this.

rhand's avatar
Level 6

When I used the same phpunit.xml setup as a default Laravel installation

<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
>
    <testsuites>
        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>
        <testsuite name="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>
    </testsuites>
    <coverage processUncoveredFiles="true">
        <include>
            <directory suffix=".php">./app</directory>
        </include>
    </coverage>
    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="BCRYPT_ROUNDS" value="4"/>
        <env name="CACHE_DRIVER" value="array"/>
        <!-- <env name="DB_CONNECTION" value="test"/> -->
        <!-- <env name="DB_DATABASE" value="test"/> -->
        <env name="MAIL_MAILER" value="array"/>
        <env name="QUEUE_CONNECTION" value="sync"/>
        <env name="SESSION_DRIVER" value="array"/>
        <env name="TELESCOPE_ENABLED" value="false"/>
    </php>
</phpunit>

and I did not use use RefreshDatabase the test ran in 0.15 seconds

php artisan test                                

   PASS  Tests\Unit\JsonObjectTest
  ✓ get exists property
  ✓ get undefined property

   PASS  Tests\Feature\Http\Controllers\Auth\LoginControllerTest
  ✓ login displays the login form
  ✓ user cannot view a login form when authenticated
  ✓ login displays validation errors

   PASS  Tests\Feature\Http\Controllers\Auth\RegisterControllerTest
  ✓ registration request works
  ✓ register displays validation errors

  Tests:  7 passed
  Time:   0.15s

Wonder still if the RegisterControllerTest

<?php

namespace Tests\Feature\Http\Controllers\Auth;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;

class RegisterControllerTest extends TestCase
{
    // use RefreshDatabase;
    /**
     * Check for active registration
     *
     * @return void
     */
    /** @test */
    
    public function registration_request_works()
    {
        $response = $this->get(route('register'));
        $this->withoutExceptionHandling();

        if (! \App\Models\Setting::checkActive('register')) {
            $response->assertStatus(500);
        }

        else{
        $response->assertStatus(200);
        $response->assertViewIs('auth.register');
        }
    }

    /** @test */
    public function register_displays_validation_errors()
    {
        // php artisan config:clear
        $response = $this->post('/register', []);

        $response->assertStatus(302);
        $response->assertSessionHasErrors('email');
    }

}

is any good

rhand's avatar
Level 6

When I use mysql connection and run tests without use RefreshDatabase all tests are also done quickly. In like 0.17 seconds. I just thought the trait would be more efficient somehow. If we need 16 seconds for a refresh that is quite a bit.

rhand's avatar
Level 6

@MohamedTammam Thanks for the feedback. Did add use RefreshDatabase after and that does help though it slows down the whole test a lot. I am going to look into seeders as well to have the basics. Normally on setups I do run some seeders. Just need to see how to run those for tests. Perhaps just run the seeders before starting testing.

As for not testing for error 500. That might be good to not do. But if registration is turned off I do not seen the form. Only text it is not possible. Perhaps I need to test for text in view?

MohamedTammam's avatar
Level 51

@rhand If the registration isn't enabled you need to check for 404 Not Found, 401 unauthorized, 403 Forbidden or any other code the describe the response, But not 50*. https://developer.mozilla.org/en-US/docs/Web/HTTP/Status

For the slow performance, check Lazy Database Refreshing

When more thing, if you have a common code that you need to run on every every test (like seeders), You can it that to setup method. In your test class, or if it's for every test in all test classes, you can add that setup method in your TestCase parent class. https://phpunit.readthedocs.io/en/9.5/fixtures.html

1 like
rhand's avatar
Level 6

@MohamedTammam Thank you very much for all these tips. Will work on things more this week. Clearly loads to learn. But your insight is helping me a lot. Thanks again!

1 like

Please or to participate in this conversation.