Back to Course |
Testing in Laravel 9 For Beginners: PHPUnit, Pest, TDD

Unit Tests vs Feature Tests: example of currency converter

In this lesson, let's talk about Unit Tests in addition to the Feature Tests. Unit is some internal code of your application, so Unit tests are supposed to test some features and functions in terms of code. It doesn't load the page or the API, doesn't use the full feature, but instead tests that some internal method returns correct data.


For this example, we want to show additional prices for euros in the table.

products table with eur price

To show the price in euros, there is a converter. This is going to be a fake converter. Imagine a CurrencyService class in the App\Services directory. In the service, there is a function to convert the price.

app/Services/CurrencyService.php:

namespace App\Services;
 
class CurrencyService
{
const RATES = [
'usd' => [
'eur' => 0.98
],
];
 
public function convert(float $amount, string $currencyFrom, string $currencyTo): float
{
$rate = self::RATES[$currencyFrom][$currencyTo] ?? 0;
 
return round($amount * $rate, 2);
}
}

In the Product Model, we add an eloquent accessor.

app/Models/Product.php:

use App\Services\CurrencyService;
use Illuminate\Database\Eloquent\Casts\Attribute;
 
class Product extends Model
{
protected $fillable = [
'name',
'price',
];
 
protected function priceEur(): Attribute
{
return Attribute::make(
get: fn() => (new CurrencyService())->convert($this->price, 'usd', 'eur'),
);
}
}

And we show this field in the View.

resources/views/products/index.blade.php:

// ...
<thead>
<tr>
<th class="px-6 py-3 bg-gray-50 text-left">
<span class="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Name</span>
</th>
<th class="px-6 py-3 bg-gray-50 text-left">
<span class="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Price (USD)</span>
</th>
<th class="px-6 py-3 bg-gray-50 text-left">
<span class="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Price (EUR)</span>
</th>
</tr>
</thead>
 
<tbody class="bg-white divide-y divide-gray-200 divide-solid">
@forelse($products as $product)
<tr class="bg-white">
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{{ $product->name }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{{ number_format($product->price, 2) }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{{ $product->price_eur }}
</td>
</tr>
// ...

The overall feature test would check if the page loads successfully with those values and the products. But it would be cool to test if the service converter works well with different currencies and values. Then, it becomes like a separate black box test.

Let's create a new Unit test.

php artisan make:test CurrencyTest --unit

You can delete the example unit test.

In this test, we must call our CurrencyService service. This service will return some value. So, in our test, we can assert that some value is equal to other.

tests/Unit/CurrencyTest.php:

use PHPUnit\Framework\TestCase;
use App\Services\CurrencyService;
 
class CurrencyTest extends TestCase
{
public function test_convert_usd_to_eur_successful()
{
$this->assertEquals(98, (new CurrencyService())->convert(100, 'usd', 'eur'));
}
}

Now, let's run the tests.

first unit test

As you can see, we have convert usd to eur successful under unit tests passed.

From here, it is your decision for what cases to test.

Let's add another unit test. The expected result is 0 this time because the gbp isn't set in the service.

tests/Unit/CurrencyTest.php:

class CurrencyTest extends TestCase
{
public function test_convert_usd_to_eur_successful()
{
$this->assertEquals(98, (new CurrencyService())->convert(100, 'usd', 'eur'));
}
 
public function test_convert_usd_to_gbp_returns_zero()
{
$this->assertEquals(0, (new CurrencyService())->convert(100, 'usd', 'gbp'));
}
}

second unit test


Commit for this lesson