Reputation: 31
I am a complete novice in testing and I wanted to know if someone could guide me on what I'm doing wrong. I want to test the following function in my php laravel project:
public static function getFlatSupplierAddress($supplier_address_id): string
{
$supplierAddress = Address::findOrFail($supplier_address_id);
return $supplierAddress->street . " " .
$supplierAddress->number . " " .
$supplierAddress->apartment . ", " .
($supplierAddress->city ? $supplierAddress->city->name : '') . ", " .
($supplierAddress->province ? $supplierAddress->province->name : '') . ", " .
($supplierAddress->country ? $supplierAddress->country->name : '');
}
This is my test code:
it('returns the formatted supplier address', function () {
$addressMock = Mockery::mock(Address::class);
$addressMock->shouldReceive('findOrFail')
->with(1)
->andReturnSelf(); // Retorna el mismo mock
$addressMock->shouldReceive('getAttribute')
->with('street')
->andReturn('Calle');
$addressMock->shouldReceive('getAttribute')
->with('number')
->andReturn('456');
$addressMock->shouldReceive('getAttribute')
->with('apartment')
->andReturn('Apt 789');
$addressMock->shouldReceive('city')
->andReturn((object)['name' => 'Retiro']);
$addressMock->shouldReceive('province')
->andReturn((object)['name' => 'Buenos Aires']);
$addressMock->shouldReceive('country')
->andReturn((object)['name' => 'Argentina']);
$expectedAddress = 'Calle 456 , Retiro, Buenos Aires, Argentina';
$formattedAddress = AddressService::getFlatSupplierAddress(1);
// Assert
expect($formattedAddress)->toBe($expectedAddress);
Of course I have the problem that I am not using well the Mock since the function returns me a string of an address that it found in the database and it is comparing it with the Mock that I have created.
Upvotes: 1
Views: 1151
Reputation: 15869
You don't need to mock anything in order to test that
it('returns the formatted supplier address', function () {
// Arrange
$address = Address::factory()
->for(City::factory())
->for(Province::factory())
->for(Country::factory())
->create();
// Act
$formatted = getFlatSupplierAddress($address->id);
// Assert
$expected = "{$address->street} {$address->number} {$address->apartment} {$address->city?->name}, {$address->province?->name}, {$address->country?->name}";
$this->assertEquals($formatted, $expected);
// or expect($formatted)->toBe($expected);
});
Edit a somewhat more in depth explanation
As I commented, findOrFail
is not actually a method found in Address
. Laravel does this a lot. It forwards calls to other classes under the hood by using __call
or __callStatic
.
When you do Address::findOrFail(1)
, it actually calls the underlying Illuminate\Database\Eloquent\Model
's static __callStatic('findOrFail', [1])
.
... which in turn just does (new static)->findOrFail(1)
... which in turns calls the Model
's __call('findOrFail' [1])
... which in turn eventually calls the Model
's newQuery()
and returns an Illuminate\Database\Eloquent\Builder
... which finally calls findOrFail(1)
.
And that just covers findOrFail
. Whenever you call a Model
's attributes ($address->street
, $address->city->name
, etc), it also forwards the call to other methods via __call
.
If you really wanted to write this test with mocks and make sure it didn't touch the database, I suppose you could write something like this:
use App\Models\{Address, Country, City, Province};
use Illuminate\Database\Eloquent\Builder;
it('returns the formatted supplier address', function () {
// Arrange
$address = Address::factory()->make();
$address->setRelation('city', City::factory()->make()); // Have to set the relations like this
$address->setRelation('province', Province::factory()->make()); // because the Factory's for method will persist them
$address->setRelation('country', Country::factory()->make()); // into the database otherwise
$address->id = 'fake-id'; // value doesn't really matter.
// Mock
$builderMock = Mockery::mock(Builder::class);
$builderMock->shouldReceive('findOrFail')->with($address->id)->andReturn($address);
$addressMock = Mockery::mock(Address::class)->makePartial(); // we still need the class's original behavior for some methods.
$addressMock->shouldReceive('newQuery')->andReturn($builderMock);
// Act
$formatted = getFlatSupplierAddress($address->id);
// Assert
$expected = "{$address->street} {$address->number} {$address->apartment} {$address->city?->name}, {$address->province?->name}, {$address->country?->name}";
$this->assertEquals($formatted, $expected);
// or expect($formatted)->toBe($expected);
});
As an aside, if you want to test every combiantion of having or not having a City
, Province
and Country
in your Address
, that's 8 separate cases. You could take advantage of bitwise operations to avoid having to write the same test 8 times.
it('returns the formatted supplier address', function ($i) {
$address = Address::factory()->create();
if ($i & 0b001) $address->setRelation('city', City::factory()->make());
if ($i & 0b010) $address->setRelation('province', Province::factory()->make());
if ($i & 0b100) $address->setRelation('country', Country::factory()->make());
})->with(range(0b000, 0b111))
Or, if you don't care about writing to the testing database
it('returns the formatted supplier address', function ($i) {
$factory = Address::factory();
if ($i & 0b001) $factory = $factory->for(City::class);
if ($i & 0b010) $factory = $factory->for(Province::class);
if ($i & 0b100) $factory = $factory->for(Country::class);
$address = $factory->create();
})->with(range(0b000, 0b111))
Upvotes: 0