hmmehead's avatar

flow for test validation

Hello Guy's! What would be the best way to test a validation request? For example. If a class has 4 attributes, all required, and one of them with a minimum of letters, and another being of type email.

It would be wise to test all possibilities, such as a test for filled fields or not, or if the attribute has the number of letters or just make sure that an error message is displayed, without worrying about the attribute itself.

In the API I'm taking care of, I did a test for every possible situation, but some tests will get really big

0 likes
4 replies
alanholmes's avatar
Level 35

For validation tests, I use @dataProvider and write a single test that uses it (which will be ran once for each data row)

/**
    * @test
    *
    * @dataProvider provideValidationData
    */
public function returns_json_validation_errors_when_validation_fails($field, $value)
{
    // any setup required

    $this->postJson(route('URL'), $this->validParams([$field => $value])))
        ->assertStatus(422)
        ->assertJsonValidationErrors($field);

    // I would then also check that data wasn't either inserted or updated
}

public function provideValidationData()
{
    return [
         'name_required' => ['field' => 'name', 'value' => ''],
         'email_required' => ['field' => 'email', 'value' => ''],
         'email_is_email' => ['field' => 'email', 'value' => 'not-an-email'],
    ];
}

// Just a method to provide default valid data, so we are only testing the single field
protected function validParams($overrides = [])
{
    return array_merge([
            'name' => 'John Smith',
            'email' => '[email protected]',
    ], $overrides);
}

Tray2's avatar

I usually start with the happy path and make that pass then I add the validations one by one, A test for a table with authors would look something like

<?php

namespace Tests\Unit\Validation;

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

class AuthorValidationTest extends TestCase
{
    use RefreshDatabase;

    /**
    * @test
    */
    public function a_valid_author_can_be_stored()
    {
        $this->withoutExceptionHandling();
        
        $this->signIn();

        $author = factory(Author::class)->make([
            'first_name' => 'Robert',
            'last_name' => 'Jordan',
        ]);

        $response = $this->post('/authors', $author->toArray());
        $this->assertEquals(1, Author::count());
    }

    /**
    * @test
    */
    public function author_first_name_is_required()
    {
        $this->signIn();

        $author = factory(Author::class)->make([
            'first_name' => null,
            'last_name' => 'Jordan',
        ]);

        $this->post('/authors', $author->toArray())->assertSessionHasErrors('first_name');
        $this->assertEquals(0, Author::count());
    }

    /**
    * @test
    */
    public function author_last_name_is_required()
    {
        $this->signIn();

        $author = factory(Author::class)->make([
            'first_name' => 'Robert',
            'last_name' => null,
        ]);

        $this->post('/authors', $author->toArray())->assertSessionHasErrors('last_name');
        $this->assertEquals(0, Author::count());
    }

    /**
    * @test
    */
    public function authors_name_must_be_unique_to_stare_an_author()
    {
        $this->signIn();

        factory(Author::class)->create([
            'first_name' => 'Robert',
            'last_name' => 'Jordan'
        ]);

        $author = factory(Author::class)->make([
            'first_name' => 'Robert',
            'last_name' => 'Jordan'
        ]);

        $this->post('authors', $author->toArray())->assertSessionHasErrors(['first_name' => 'Author name not unique']);

        $this->assertEquals(1, Author::count());
    }
hmmehead's avatar

I usually do this kind of test when my class is small, but I'm having cases where I have a class with many attributes and their criteria.

I don't know if this is the best way, or if it really is necessary to go through all these steps to better understand the possible scenarios.

Talinon's avatar

@hmmehead

I will explicitly write a test (or multiple tests) for each validation rule. A major benefit of providing a detailed description in the test function name is clear feedback on any failure.

Your test shouldn't get too lengthy. I usually keep each test as basic as possible and put any of the repetitive initialization in the setUp() method.

For example, for a length_required field, I might write several tests such as:


    /** @test */
    public function an_entry_requires_a_length_required()
    {

        $this->post('/endpoint', [])
            ->assertSessionHasErrors('length_required');

    }

    /** @test */
    public function the_length_required_must_be_an_integer()
    {

        $this->post('/endpost', ['length_required' => 'a'])
            ->assertSessionHasErrors('length_required');

    }


    /** @test */
    public function the_length_required_value_must_be_at_least_one()
    {

        $this->post('/', ['length_required' => 0])
            ->assertSessionHasErrors('length_required');

    }


At the end, I'll usually have a test that will provide all the parameters, something like:


    /** @test */
    public function an_entry_may_be_persisted_to_the_database_after_successful_validation()
    {

    $data = $this->passingData();

        $this->post('/endpoint', $data);

        $this->assertDatabaseHas('entries', $data);

    }


private function passingData()
{

    return [
        'length_required' => 1,
        ...
        ...
    ]

}

Or, if possible, instead of the passingData() function, I might just make() a model from a factory and cast it to an array to pass to the post request.

    /** @test */
    public function an_entry_may_be_persisted_to_the_database_after_successful_validation()
    {

        $entry = make(Entry::class);

        $this->post('/endpoint', $entry->toArray());

        $this->assertDatabaseHas('entries', $entry->toArray());

    }



Please or to participate in this conversation.