1

I made the following controller:


namespace App\Http\Controllers;

use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller as BaseController;

use App\Models\Record;

class SpyController extends BaseController
{
    public function add(Request $request)
    {
        $model = new Record();
        $model->value = $request->get('value');

        return new JsonResponse(['message'=>'Insert Success','record'=>$model],201);
        
    }   
}

And I want to test that upon a successful insertion, a value will be inserted:


namespace Tests\Controller;

use Illuminate\Testing\Fluent\AssertableJson;
use Laravel\Sanctum\Sanctum;

use App\Models\User;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\DatabaseTestCase;


class SpyRoutesTest extends TestCase
{
   public function addSuccess()
   {
      DB::fake()
      $response = $this->put('/record',['value'=>'lalalala']);
      
      $this->assertStatus(201);
   }
}

But in my case, how I can mock the actual Insertion of the record? Will the fake() just assume a successfull insertion? Also, how I can mock an error in the database for example a connection timeout as well?

Dimitrios Desyllas
  • 9,082
  • 15
  • 74
  • 164
  • 1
    Curious, why do you need to mock this? If you're using `RefreshDatabase`, you database will be wiped either between tests, or when tests are finished (I think it's between, but I don't recall 100%). If you set up a `.env.testing`, that points at a testing database, you can run all of your tests, insert actual data and not have to worry about mocking anything. Someone else might know how to do this mocking, but it's not something I've ever done. – Tim Lewis Jul 13 '23 at 18:21
  • 1
    @TimLewis 100% wiped between test's methods, so each test method has the same "pristine" level – matiaslauriti Jul 13 '23 at 18:26
  • 1
    @matiaslauriti That's what I thought, but didn't want to speak out of turn. Thanks for confirming! Sidenote, I knew you'd answer this; nice to see your expertise on testing Laravel solutions come into play here – Tim Lewis Jul 13 '23 at 18:27
  • 1
    @TimLewis thank you so much, I really like answering Laravel testing questions, as sometimes they are very hard or long to explain and answer, but they also make me think and learn new stuff! I would wish more questions on ([tag:phpunit] or [tag:testing]) + [tag:laravel] were asked – matiaslauriti Jul 13 '23 at 18:50
  • 1
    @matiaslauriti It's a funny (and maybe a little sad) fact that the lack of those kinds of questions is likely due to people not doing testing at all (which was my first 5+ years of experience with Laravel; all coding, no testing of any kind). It's a skill I'm actively trying to learn, and I typically do learn something new whenever one of these questions is asked (and usually answered by you), so keep up the great work, for both of our sakes – Tim Lewis Jul 13 '23 at 18:56
  • 1
    @TimLewis thank you so much, I really appreciate your words! I want to create a sort of video tutorials/course about basic testing and more (but related to testing), I will do something about it in the near future ;) – matiaslauriti Jul 13 '23 at 19:15
  • @TimLewis I want to emulate database behaviour insterd od using actual DB. The reason why is that because in the future I want to parallelize the test execution and Database is a bottleneck. Mocking offers me an easyness upon testing. – Dimitrios Desyllas Jul 14 '23 at 08:40

1 Answers1

2

Your solution is easy: you do not mock the database AT ALL. I have replied your exact question before (I have more answers related to testing in my profile).

You have to literally test the insertion, so have a fake database (another database name on the same connection (or not) but only used for testing) and insert the data, but use RefreshDatabase so it gets deleted.

Your test should be like this:

use Illuminate\Foundation\Testing\RefreshDatabase;

class SpyRoutesTest extends TestCase
{
    use RefreshDatabase;

    public function test_record_should_be_added_with_expected_value()
    {
        $this->freezeTime();

        $expectedValue = 'nice string to set in the value property';

        $response = $this->put('/record', ['value' => $expectedValue])
            ->assertCreated();

        $this->assertDatabaseCount('records', 1);

        $this->assertDatabaseHas(
            'records',
            ['value' => $expectedValue]
        );

        $response->assertJson([
            'message' => 'Insert Success',
            'record' => [
                'id' => 1,
                'value' => $expectedValue,
                'created_at' => now()->toDateTimeString(),
                'updated_at' => now()->toDateTimeString(),
            ]
        ]);
    }
}

As you can see, I am doing stuff in this order:

  1. Freezing time, so when you use now() or today() or any carbon helper, it will always return the same exact time. This is useful when you want to test if it returns the right time and format.
  2. Setting a common value to expect back.
  3. Doing the call to the desired URL, with known data.
  4. Asserting it returned the status 201.
  5. Checking that a single record was added (before running the test, we knew that we had 0, so no need to check if it was empty before doing the HTTP call)
  6. Checking that the added record has the expected data (you only use the value property, so I am only looking for that)
  7. Finally, check that the returned JSON has the expected format and exactly the expected data back. Here you may need to modify the returning stuff in the test if you have other columns, or you are not returning, for example, the time (in that case you can remove the freeze).
    • I did do this last check at the end, because I was testing something else before, but I removed it (not added it to this example) so I left it at the end, you can definitely move this check after the ->assertCreated() in a chain call too.

More info:

matiaslauriti
  • 7,065
  • 4
  • 31
  • 43
  • But in case that my database malfunctions for example dat6abase is down how I can ensure that in that case api will return error 500? – Dimitrios Desyllas Jul 13 '23 at 19:30
  • @DimitriosDesyllas that is a different thing, not what you asked, in that case, you could mock the stuff, but that is not what you asked – matiaslauriti Jul 13 '23 at 19:37