-1

In my SUT I've got the following function:

class MyClass
{
    public function doSomething(Registration $registration)
    {
        return $registration->attendees->first()->last_name;
    }
}

Registration is a Laravel Eloquent model that has many Attendees.

I can't seem to create a test for this as everything seems to rely on magic methods.

In my test I've got this:

public function testDoSomething()
{
    $this->attendeeMock->last_name = "Bar";
    $this->collection[0] = $this->attendeeMock; // This is a real Eloquent Collection
    $this->registrationMock->attendees = $this->collection;
    $this->assertEquals('Bar', $this->myClass->doSomething($this->registrationMock));
}

The above code throws Call to a member function first() on null.

This seems to be such a simple use case, but on the internet I can't find appropriate answers. Are we supposed to avoid using magic accessors and use getRelationship and getAttribute for everything?

Erfan
  • 2,959
  • 2
  • 17
  • 21
  • You do not mock models or collections... You use [`factories`](https://laravel.com/docs/9.x/database-testing#defining-model-factories), read the documentation so you know more about it, try to apply that and then, if you still have questions, ask again – matiaslauriti Mar 16 '22 at 23:07
  • @matiaslauriti I don't think that's right. That documentation refers specifically to database testing. I don't want to test the database, and my SUT isn't even aware of any persistent storage. When I do follow the documentation and your advice to use Factories instead of mocking models, I get `Error: Call to a member function connection() on null` during my tests. – Erfan Mar 17 '22 at 04:33
  • Then add all the needed information, as you seem to be using a Model without any storage? So how do you use it then? – matiaslauriti Mar 17 '22 at 13:21
  • All the needed information is here. The SUT is simply retrieving the last name, nothing in this example requires a database and if so, that would make it an integration test, which is not my question. I think the Laravel community generally stays away from unit testing, and the common answer to this is "you can't do this, run integration tests instead." – Erfan Mar 19 '22 at 02:55

1 Answers1

0

Zero-IO unit tests in Laravel that includes Eloquent models isn't possible unfortunately, and the community generally recommends integration tests.

Two workarounds are:

  • For more complex applications, use a DTO and repository pattern that act as wrappers for Eloquent models. This way you can separate unit testing logic from Eloquent and your domain logic becomes (unit) testable.
  • Write integration tests instead.

Some good comments are here as well: https://www.reddit.com/r/laravel/comments/qlqqis/do_you_find_yourself_writing_actual_unit_tests/

Eloquent makes zero-IO isolated unit tests pretty much impossible. [...] It's annoying how Laravel decided to redefine "Unit test" to something it isn't.

Erfan
  • 2,959
  • 2
  • 17
  • 21
  • So, you are using Eloquent as a non database, that is not designed for... And you are confusing integration tests with feature tests, features are for your stuff with the framework and everything, but integration tests would be outside the framework, like testing a real API call. Still, Eloquent is designed to be used with a database or data store, it is not a "repository". I am not sure if there is a package that can handle that, but again, Eloquent requires a database or similar, you cannot use it without it... You are trying to use a feature it was not designed for that... that is why – matiaslauriti Mar 19 '22 at 23:28
  • In the context of unit tests, I don't want to use Eloquent at all, I just want to mock it to test my SUT. I know what you're saying though — it's hard to unit test when your domain logic directly uses active record classes, and perhaps that should be abstracted away if unit tests are important. It's an integration test if your test spans multiple "units" (in the Laravel context, if the test uses the database, it's an integration test since there are multiple units/systems involved). – Erfan Mar 21 '22 at 05:03
  • I am very curious why do you need this explicit separation and cannot just use a factory, of course I am missing or not understanding something, but hope you can solve it. If it gets too hard for you, then it means you are either using it very badly (the community should have already solved this type of issues) or you just found a new way/path and it is totally empty and you must forge it for the community. – matiaslauriti Mar 21 '22 at 14:33
  • I suggest you read up on the concept of unit tests and integration tests and what the difference is between them. There's a reason they exist, often times this has to do with TDD, ease of implementation, speed, accuracy of error reporting, needing to put a large amount of data with edge cases through complex situations, and so forth. I've already in my answer outlined two workarounds that are generally accepted by the community. – Erfan Mar 23 '22 at 10:19
  • I do know how to test and mostly everything involving "testing", it is just different in here. You do not have real "unit" tests, you are all the time using the framework behind scenes, so "unit" tests would be just testing `listeners`, `jobs` and maybe, just maybe `commands`, and "feature" tests (or "integration" as you call them in here) would be `Controller requests` for example. Still, you should be able to test anything you want reading the documentation as you have everything explained in there! I am sorry it is very different from what you are used to. – matiaslauriti Mar 23 '22 at 13:55
  • Maybe this will not help you even a bit, but [this is a "super small" introduction to testing in Laravel done by](https://stackoverflow.com/questions/69150653/how-to-feature-test-more-complicated-cases-on-laravel-using-phpunit/69155061#69155061) me in another SO post, so maybe this will give you insights on how we think about tests. – matiaslauriti Mar 23 '22 at 13:59