1

I am trying to create a test for a feature I've written.

The logic is quite simple:

From the api.php I am calling the store method:

Route::group(['prefix' => '/study/{study}/bookmark_list'], function () {
           ...
            Route::post('/{bookmarkList}/bookmark', 'BookmarkController@store');
           ...
        });

thus I am injecting the study and the bookmark list.

My controller passes down the parameters

 public function store(Study $study, BookmarkList $bookmarkList)
    {
        return $this->serve(CreateBookmarkFeature::class);
    }

And I am using them in the Feature accordingly

'bookmark_list_id' => $request->bookmarkList->id,

class CreateBookmarkFeature extends Feature
{
    public function handle(CreateBookmarkRequest $request)
    {

        //Call the appropriate job
        $bookmark = $this->run(CreateBookmarkJob::class, [
            'bookmark_list_id' => $request->bookmarkList->id,
            'item_id'          => $request->input('item_id'),
            'type'             => $request->input('type'),
            'latest_update'    => $request->input('latest_update'),
            'notes'            => $request->input('notes')
        ]);

        //Return
        return $this->run(RespondWithJsonJob::class, [
            'data' => [
                'bookmark' => $bookmark
            ]
        ]);
    }
}

I am also using a custom request (CreateBookmarkRequest) which practically verifies if the user is authorised and imposes some rules on the input.

class CreateBookmarkRequest extends JsonRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return $this->getAuthorizedUser()->canAccessStudy($this->study->id);
    }


    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            "item_id"       => ["integer", "required"],
            "type"          => [Rule::in(BookmarkType::getValues()), "required"],
            "latest_update" => ['date_format:Y-m-d H:i:s', 'nullable'],
            "text"          => ["string", "nullable"]
        ];
    }
}

Now, here comes the problem. I want to write a test for the feature that tests that the correct response is being returned (it would be good to verify the CreateBookmarkJob is called but not that important). The problem is that although I can mock the request, along with the input() method, I cannot mock the injected bookmarkList.

The rest of the functions are mocked properly and work as expected.

My test:

class CreateBookmarkFeatureTest extends TestCase
{
    use WithoutMiddleware;
    use DatabaseMigrations;

    public function setUp(): void
    {
        parent::setUp();

        // seed the database
        $this->seed();
    }

    public function test_createbookmarkfeature()
    {
        //GIVEN
        $mockRequest = $this->mock(CreateBookmarkRequest::class);
        $mockRequest->shouldReceive('authorize')->once()->andReturnTrue();
        $mockRequest->shouldReceive('rules')->once()->andReturnTrue();
        $mockRequest->shouldReceive('input')->once()->with('item_id')->andReturn(1);
        $mockRequest->shouldReceive('input')->once()->with('type')->andReturn("ADVOCATE");
        $mockRequest->shouldReceive('input')->once()->with('latest_update')->andReturn(Carbon::now());
        $mockRequest->shouldReceive('input')->once()->with('notes')->andReturn("acs");
        $mockRequest->shouldReceive('bookmark_list->id')->once()->andReturn(1);
        

        //WHEN
        $response = $this->postJson('/api/recruitment_toolkit/study/1/bookmark_list/1/bookmark', [
            "type"=> "ADVOCATE",
            "item_id"=> "12",
            "text"=> "My first bookmark"
        ]);
        
        //THEN
        $this->assertEquals("foo", $response['data'], "das");
    }

One potential solution that I though would be to not mock the request, but this way I cannot find a way to mock the "returnAuthorisedUser" in the request.

Any ideas on how to mock the injected model would be appreciated, or otherwise any idea on how to properly test the feature in case I am approaching it wrong.

It is worth mentioning that I have separate unit tests for each of the jobs (CreateBookmarkJob and RespondWithJSONJob).

Thanks in advance

vasilaou
  • 35
  • 5
  • My answer is lacking some detail because it's not really clear what your controller is doing or what it is returning (no idea what `Feature` class is, or why you're doing simple object creation with jobs) but hopefully it gets you on the right track. – miken32 Jan 06 '22 at 18:49
  • As @miken32 answer, you can also look at mine that is completely detailed (general topics) in another post but will help you a lot to understand testing better. It is also a addition to miken32 answer. [How to correctly test](https://stackoverflow.com/questions/69150653/how-to-feature-test-more-complicated-cases-on-laravel-using-phpunit/69155061#69155061) – matiaslauriti Jan 06 '22 at 22:26

2 Answers2

3

A feature test, by definition, will be imitating an end-user action. There's no need to mock the request class, you just make the request as a user would.

Assuming a Study with ID 1 and a BookmarkList with ID 1 have been created by your seeder, Laravel will inject appropriate dependencies via route model binding. If not, you should use a factory method to create models and then substitute the appropriate ID in the URL.

<?php

namespace Tests\Feature;

use Tests\TestCase;

class CreateBookmarkFeatureTest extends TestCase
{
    use WithoutMiddleware;
    use DatabaseMigrations;

    public function setUp(): void
    {
        parent::setUp();
        $this->seed();
    }

    public function TestCreateBookmarkFeature()
    {
        $url = '/api/recruitment_toolkit/study/1/bookmark_list/1/bookmark';
        $data = [
            "type"=> "ADVOCATE",
            "item_id"=> "12",
            "text"=> "My first bookmark"
        ];
        $this->postJson($url, $data)
            ->assertStatus(200)
            ->assertJsonPath("some.path", "some expected value");
    }
}
miken32
  • 42,008
  • 16
  • 111
  • 154
0

I agree with @miken32's response - that a feature should indeed imitate a user interaction - however the dependency injection via route model binding still did not work.

After spending some hours on it, I realised that the reason for it is that

use WithoutMiddleware;

disables all middleware, even the one responsible for route model binding, therefore the object models were not injected in the request.

The actual solution for this is that (for laravel >=7) we can define the middleware we want to disable, in this case:

$this->withoutMiddleware(\App\Http\Middleware\Authenticate::class);

Then we just use

$user = User::where('id',1)->first(); $this->actingAs($user);

And everything else works as expected.

DISCLAIMER: I am not implying that miken32's response was incorrect; it was definitely in the right direction - just adding this as a small detail.

vasilaou
  • 35
  • 5