Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

longestdrive's avatar

Testing Laravel Form Request with custom rules

I'm trying to get my head around testing (again) and in particular test a form request with a custom rule. I'm using Laravel 5.5.

My rule needs a variable from the request to compare the field value under validation.

Whilst I can get this to work in the app I can't create the test correctly and pass the values.

Here's the custom rule (stripped down to essential info):

 class TicketNumberIsNotUsed implements Rule
{
    
    public function __construct($reservation_id)
    {
        $this->reservation_id = $reservation_id;
    }



    public function passes($attribute, $value)
    {
        $ticket = TicketAudit::where('ticketnumber', '=', $value)->first();

        if($ticket) {
            return (is_null($ticket->reservation_id) || $ticket->reservation_id == $this->reservation_id ? true : false);
        }
        return false;
    }

  
    public function message()
    {
        return 'This ticket has already been used for another booking.';
    }
}

Here's the form request which uses the custom rule:

class CreateGreenFeeSaleRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return Auth::user()->hasRole('sales');
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'play_date'=>'required',
            'am_tee'=>'required',
            'howBooked'=>'required',
            'ticket'=>[
                'required',
                'exists:ticket_audits,ticketnumber' ,
                new TicketNumberIsNotUsed($this->reservation_id)
            ] ,
            'pay_method'=>'required',
            'customer_id'=>'required:exists:customers,id'

        ];
    }

    public function messages()
    {
        return [
            'ticket.required' => 'A valid ticket is required',
            'ticket.exists'  => 'This ticket does not exist',
        ];
    }

}

and here is my test:

class CreateGreenFeeTest extends TestCase
{
    protected function setUp()
    {
        parent::setUp();

    }

    private function createAttributes()
    {
        $faker = \Faker\Factory::create();

        $attributes = [
            'play_date' => $faker->date('d/m/Y'),
            'reservation_id' => null,
            'am_tee' => $faker->time('H:m a'),
            'howBooked' => 'drop in',
            'ticket' => 905,
            'pay_method' => 'cash',
            'customer_id' => 1532
        ];

        return $attributes;


    }

    public function test_form_validation_passes()
    {
        $attributes = $this->createAttributes();
        $request = new \App\Http\Requests\CreateGreenFeeSaleRequest();
        $rules = $request->rules();
        $validator = Validator::make($attributes, $rules);#
        $passes = $validator->passes();
        $this->assertEquals(true, $passes);
    }

    public function test_form_validation_fails_unused_ticket()
    {
        $attributes = $this->createAttributes();
//        make change to a used ticket
        $attributes['ticket']=967;
        $request = new \App\Http\Requests\CreateGreenFeeSaleRequest();
        $rules = $request->rules();
        $validator = Validator::make($attributes, $rules);#
        $fails = $validator->fails();
        $this->assertEquals(true, $fails);
    }

    public function test_form_validation_passes_used_ticket_this_reservation()
    {
        $attributes = $this->createAttributes();
//        make change to a used ticket
        $attributes['ticket']=967;
        $attributes['reservation_id']=1554;
        $request = new \App\Http\Requests\CreateGreenFeeSaleRequest();
        $rules = $request->rules();
        $validator = Validator::make($attributes, $rules);#
        var_dump($validator->errors());
        $passes = $validator->passes();
        $this->assertEquals(true, $passes);
    }
}

So:

  1. How do I pass the reservation_id to the custom rule from the test
  2. How should I correctly mock that an authorised user is logged in?

I am struggling with tests despite hours reading and trying to learn - it doesnt stick until I can apply it - I cant apply it till I get it!! (catch 22) So any help appreciated especially on how to construct the tests properly

Thank you

0 likes
6 replies
shez1983's avatar
shez1983
Best Answer
Level 23

i am not sure about your db schema BUT it seems to me are you are doing is looking to make sure reservation_id is not filled.. in which case you could try using

https://laravel.com/docs/5.5/validation#rule-unique

unique validation helper, it does similar to what you are doing..

also this is not how I usually test if validator is working.. i just hit my route or controller function with 'valid' data and make sure it gives 201 and then another test where i give invalid data and make sure i get 422.. (you can drill down to look for exact error messages)

Regarding your question: you are creating

'reservation_id' => null, so cant you change it before calling validate like you do ticket array item?

1 like
longestdrive's avatar

Hi - the rule checks whether this ticket has already been allocated to a reservation by reservation_id and if it is is the reservation this reservation.

If it is the same reservation it should pass and if not it should fail

If the reservation_id for this ticket is null then it means it's not been allocated so passes.

So I think unique would work I don't think it takes into account that it could be used for this record.

Reservations are in one table, tickets are in another - tickets contain the reservation_id

I'll look at your approach (do you have an example test you could show me?) - I think I assumed with the test to try an isolate to the method (in this case the rule) but then I don't yet understand what to test and how to test

Thank you

shez1983's avatar

well you must be using this FOrmRequest in a controller? right something like

route::post('route',  'Controlller@controller');
...
function controller(FromRequest $request).....

so just test that function and pass in data so

function test_......()  {    
$this->post('route', $someValidData)->assertStatus(200);
}

now pass in some invalidData to and do assertStatus(422) ..

invalidData = array('ticket' => 1, ... ); pretty much like you have in your current test

1 like
longestdrive's avatar

Yes, you're right in controller I have:

public function store(CreateGreenFeeSaleRequest $request)
    {
// create the records needed

return redirect()->route('greenfees.index');
}

I've now updated the test as suggested - with valid attributes:

public function test_form_validation_passes_used_ticket_this_reservation()
    {
        $attributes = $this->createAttributes(['ticket'=>967, 'reservation_id'=>1554]);

        $this->post(route('greenfees.store'), $attributes)->assertStatus(200);
}

The test though fails - I'm getting a response code of 302

Question - by doing it this way I assume the records get created in the store method and the rest of the method is applied? So is this test testing too much in one go? ie if there's something wrong in the store method this will affect the test so I don't know if it's the store logic or the request logic that has the problem?

Sorry for my lack of understanding and thanks for the help

shez1983's avatar

well in your test you will test for:

  1. validation by giving it different values (one that pass, one that fails) 2.when it passes you will assert that DB has that record by doing seeInDB() or something
  2. or if it fails notSeeinDB();

regarding your problem, 302 indicates a redirect, are you using an endpoint/JSON or doing it over admin panel? (if admin panel then you shouldnt have got 302, but 422..)

maybe you are testing too much but as long as you have your assertion, you are fine..

1 like
longestdrive's avatar

Thanks - yes I am testing too much and will change these tests.

I think the redirect occurs at the end of the store method in the controller - it's redirecting to an index page so that makes sense

Please or to participate in this conversation.