How to feature test more complicated cases on Laravel using PHPUnit

I’m using Laravel for my project and I’m new to unit/feature testing so I was wondering what is the best way to approach more complicated feature cases when writing tests?

Let’s take this test example:

  // tests/Feature/UserConnectionsTest.php
  public function testSucceedIfConnectAuthorised()
  {
    $connection = factory(Connection::class)->make([
      'sender_id'     => 1,
      'receiver_id'   => 2,
      'accepted'      => false,
      'connection_id' => 5,
    ]);

    $user = factory(User::class)->make([
      'id' => 1,
    ]);

    $response = $this->actingAs($user)->post(
      '/app/connection-request/accept',
      [
        'accept'     => true,
        'request_id' => $connection->id,
      ]
    );

    $response->assertLocation('/')->assertStatus(200);
  }

So we’ve got this situation where we have some connection system between two users. There is a Connection entry in the DB created by one of the users. Now to make it a successful connection the second user has to approve it. The problem is within the UserController accepting this through connectionRequest:
  // app/Http/Controllers/Frontend/UserController.php
  public function connectionRequest(Request $request)
  {
    // we check if the user isn't trying to accept the connection
    // that he initiated himself
    $connection = $this->repository->GetConnectionById($request->get('request_id'));
    $receiver_id = $connection->receiver_id;
    $current_user_id = auth()->user()->id;

    if ($receiver_id !== $current_user_id) {
      abort(403);
    }

    [...]
  }


  // app/Http/Repositories/Frontend/UserRepository.php 
  public function GetConnectionById($id)
  {
    return Connection::where('id', $id)->first();
  }

So we’ve got this fake (factory created) connection in the test function and then we unfortunately are using its fake id to run a check within the real DB among real connections, which is not what we want 🙁

Researching I found an idea of creating interfaces so then we can provide a different method bodies depending if we’re testing or not. Like here for GetConnectionById() making it easy to fake answers to for the testing case. And that seems OK, but:

  • for one it looks like a kind of overhead, besides writing tests I have to make the “real” code more complicated itself for the sole purpose of testing.
  • and second thing, I read all that Laravel documentation has to say about testing, and there is no one place where they mention using of interfaces, so that makes me wonder too if that’s the only way and the best way of solving this problem.

Answers:

Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.

Method 1

I will try to help you, when someone start with testing it is not easy at all, specially if you don’t have a strong framework (or even a framework at all).

So, let me try help you:

  • It is really important to differentiate Unit testing vs Feature testing. You are correctly using Feature test, because you want to test business logic instead of a class directly.
  • When you test, my personal recommendation is always create a second DB to only use with tests. It must be completely empty all the time.
    • So, for you to achieve this, you have to define the correct environment variables in phpunit.xml, so you don’t have to do magic for this to work when you only run tests.
    • Also, use RefreshDatabase trait. So, each time you run a test, it is going to delete everything, migrate your tables again and run the test.
  • You should always create what you need to have as mandatory for your test to run. For example, if you are testing if a user can cancel an order he/she created, you only need to have a product, a user and an invoice associated with the product and user. You do not need to have notifications created or anything not related to this. You must have what you expect to have in the real case scenario, but nothing extra, so you can truly test that it fully works with the minimum stuff.
  • You can run seeders if your setup is “big”, so you should be using setup method.
  • Remember to NEVER mock core code, like request or controllers or anything similar. If you are mocking any of these, you are doing something wrong. (You will learn this with experience, once you truly know how to test).
  • When you write tests names, remember to never use if and must and similar wording, instead use when and should. For example, your test testSucceedIfConnectAuthorised should be named testShouldSucceedWhenConnectAuthorised.
  • This tip is super personal: do not use RepositoryPattern in Laravel, it is an anti-pattern. It is not the worst thing to use, but I recommend having a Service class (do not confuse with a Service Provider, the class I mean is a normal class, it is still called Service) to achieve what you want. But still, you can google about this and Laravel and you will see everyone discourages this pattern in Laravel.
  • One last tip, Connection::where('id', $id)->first() is exactly the same as Connection::find($id).
  • I forgot to add that, you should always hardcode your URLs (like you did in your test) because if you rely on route('url.name') and the name matches but the real URL is /api/asdasdasd, you will never test that the URL is the one you want. So congrats there ! A lot of people do not do this and that is wrong.

So, to help you in your case, I will assume you have a clear database (database without tables, RefreshDatabase trait will handle this for you).

I would have your first test as this:

public function testShouldSucceedWhenConnectAuthorised()
{
    /**
     * I have no idea how your relations are, but I hope
     * you get the main idea with this. Just create what
     * you should expect to have when you have this
     * test case
     */
    $connection = factory(Connection::class)->create([
        'sender_id' => factory(Sender::class)->create()->id,
        'receiver_id' => factory(Reciever::class)->create()->id,
        'accepted' => false,
        'connection_id' => factory(Connection::class)->create()->id,
    ]);

    $response = $this->actingAs(factory(User::class)->create())
        ->post(
            '/app/connection-request/accept',
            [
                'accept' => true,
                'request_id' => $connection->id
            ]
        );

    $response->assertLocation('/')
        ->assertOk();
}

Then, you should not change anything except phpunit.xml environment variables pointing to your testing DB (locally) and it should work without you changing anything in your code.


All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x