Di dashboard, angka yang salah lebih berbahaya daripada fitur yang hilang — pemilik toko membelanjakan stok berdasarkan tren yang ia lihat. Bagian penutup ini membuktikan seluruh angka proyek lewat feature test
yang menuntut nilai eksak, bukan sekadar status 200. Kunci yang membuat hal itu mungkin sudah kita pasang sejak Bagian 2: seeder deterministik. Karena setiap pengisian ulang database menghasilkan 498 baris yang sama, assertSame(3038000, ...) adalah assertion yang sah, bukan angka ajaib.
Strategi Pengujian #
Empat file test membagi tugas berdasarkan lapisan yang dijaganya:
| File test | Yang dibuktikan |
|---|---|
ExampleTest | halaman utama mengalihkan ke dashboard |
DashboardMetricTest | lima metode service menghasilkan angka eksak dari data seeder |
DashboardFilterTest | filter rentang membatasi agregasi, validasi menolak input salah |
DashboardPageTest | halaman merender muatan @json, lebar batang, kartu, dan pagination |
Seluruh test memakai trait RefreshDatabase dan mengisi ulang seeder di setUp(), jadi setiap test berdiri di atas data 498 baris yang identik. Nama test ditulis dalam Bahasa Indonesia berprefiks test_ sehingga daftar kegagalan pun terbaca seperti laporan perilaku aplikasi, dan durasi totalnya di bawah satu detik berkat SQLite dalam memori.
Tiga Pembuktian Kunci #
Pembuktian pertama menuntut tren dua belas bulan sampai ke rupiah terakhir:
$trend = $this->dashboard->monthlyTrend(
Carbon::parse('2026-01-01'),
Carbon::parse('2026-12-31'),
);
$this->assertCount(12, $trend);
$this->assertSame(
[3038000, 2954000, 3275000, 3222500, 3435500, 3401500,
3404500, 3630000, 3359000, 3822000, 3840000, 3924000],
$trend->pluck('revenue')->all(),
);
$this->assertSame(
[36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47],
$trend->pluck('orders')->all(),
);Pembuktian kedua menyasar halaman: muatan @json yang dikirim server dicocokkan secara utuh, character-per-character:
$payload = json_encode([
'labels' => ['Jan 2026', 'Feb 2026', 'Mar 2026', 'Apr 2026', 'Mei 2026', 'Jun 2026',
'Jul 2026', 'Agt 2026', 'Sep 2026', 'Okt 2026', 'Nov 2026', 'Des 2026'],
'revenue' => [3038000, 2954000, 3275000, 3222500, 3435500, 3401500,
3404500, 3630000, 3359000, 3822000, 3840000, 3924000],
'orders' => [36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47],
], 15);
$response->assertSee('const trend = ' . $payload . ';', false);Angka 15 adalah konstanta flag yang dipakai directive @json (JSON_HEX_TAG dan kawan-kawan), jadi json_encode(..., 15) di test menghasilkan teks yang persis sama dengan yang dirender Blade. Pembuktian ketiga adalah sandaran kejujuran seluruh rangkaian — agregasi SQL diperiksa ulang dengan cara paling bodoh: memuat semua baris lalu menjumlahkan satu per satu di PHP. Test agregasi sql sama dengan penjumlahan per baris membandingkan kedua hasil, dan test penjumlahan dua semester sama dengan setahun memastikan 19326500 + 21979500 tetap 41306000 — sifat tambah yang di atas kertas sepele, tetapi hanya terbukti bila tidak ada baris yang lolos atau terhitung dua kali di batas bulan.
Sisanya menunggu lapisan presentasi. Test pagination menghitung jumlah tag <tr> mentah — 17 berarti lima produk teratas plus kepala tabelnya (6) ditambah sepuluh baris penjualan plus kepalanya (11) — angka sederhana yang akan berubah begitu paginate(10) bocor. Test validasi memakai assertSessionHasErrors() dengan pesan persis milik kita, dan test pangsa kategori menuntut lebar width: 58% sampai kodenya warnanya, persis seperti janji Bagian 4 bahwa angka dan tampilan tidak pernah berpisah.
Hasil Eksekusi Test #
Seluruh rangkaian dijalankan dengan satu perintah:
php artisan testOutput yang Dihasilkan:
PASS Tests\Unit\ExampleTest
✓ that true is true
PASS Tests\Feature\DashboardFilterTest
✓ filter rentang membatasi agregasi ke periode terpilih 0.15s
✓ penjumlahan dua semester sama dengan setahun 0.02s
✓ halaman menerima filter rentang tanggal 0.04s
✓ format tanggal tidak sah ditolak 0.02s
✓ tanggal akhir mendahului awal ditolak 0.02s
✓ rentang tanpa penjualan menampilkan nol 0.02s
PASS Tests\Feature\DashboardMetricTest
✓ ringkasan hari menghitung pendapatan dan pesanan tepat 0.02s
✓ ringkasan bulan menghitung total pesanan dan rata rata 0.02s
✓ tren bulanan setahun menghasilkan dua belas titik 0.02s
✓ tren bulanan mengisi bulan tanpa penjualan dengan nol 0.02s
✓ produk teratas diurutkan menurun berdasar pendapatan 0.02s
✓ pangsa kategori berjumlah seratus persen 0.02s
✓ agregasi sql sama dengan penjumlahan per baris 0.03s
PASS Tests\Feature\DashboardPageTest
✓ dashboard tampil tanpa login 0.03s
✓ muatan grafik garis terender penuh dari server 0.03s
✓ batang kategori menampilkan lebar persentase 0.03s
✓ kartu ringkasan menampilkan angka bulan acuan 0.03s
✓ tabel penjualan terpaginasi sepuluh baris per halaman 0.03s
✓ paginasi menjaga query filter 0.03s
PASS Tests\Feature\ExampleTest
✓ halaman utama mengalihkan ke dashboard 0.01s
Tests: 21 passed (74 assertions)
Duration: 0.68sDua puluh satu test dengan 74 assertion selesai dalam waktu kurang dari satu detik. Arah pengembangan berikutnya terbuka: penyegaran otomatis lewat polling fetch(), ekspor CSV untuk akuntan, perbandingan tahun-ke-tahun, atau autentikasi bila dashboard mulai memuat data sensitif — dan setiap arah itu kini punya jaring berupa angka pasti yang tinggal dituntut ulang.
Dengan dashboard ini, perjalanan enam proyek Course Laravel kita tuntas: dari blog pertama yang menyimpan artikel, inventaris yang menjaga stok, REST API yang melayani aplikasi lain, keuangan pribadi yang menghitung rupiah, aplikasi pemesanan yang mengunci transaksi, sampai dashboard yang membaca semuanya kembali menjadi keputusan. Anda kini membawa bekal yang lengkap — routing, Eloquent, validasi, Blade, testing — dan bukan sekadar hafalan sintaks, melainkan kebiasaan menuntut bukti untuk setiap angka yang tampil di layar. Lanjutkan perjalanan Anda di bagian Sumber Belajar : dokumentasi resmi untuk pendalaman, referensi Artisan dan Blade untuk pekerjaan sehari-hari, serta roadmap belajar untuk menentukan langkah berikutnya. Selamat membangun — semoga aplikasi pertama Anda melayani banyak pengguna, dan semoga test suite-nya selalu hijau.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
app/Http/Controllers/Controller.php
Unduh berkas<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}
app/Http/Controllers/DashboardController.php
Unduh berkas<?php
namespace App\Http\Controllers;
use App\Services\DashboardService;
use Carbon\Carbon;
use Illuminate\Contracts\View\View;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
class DashboardController extends Controller
{
public function __construct(private readonly DashboardService $dashboard)
{
}
public function index(Request $request): View
{
$validated = $request->validate([
'from' => ['nullable', 'date_format:Y-m-d'],
'to' => ['nullable', 'date_format:Y-m-d'],
]);
$from = isset($validated['from']) ? Carbon::parse($validated['from']) : null;
$to = isset($validated['to']) ? Carbon::parse($validated['to']) : null;
if ($from !== null && $to !== null && $to->lt($from)) {
throw ValidationException::withMessages([
'to' => 'Tanggal akhir harus sama dengan atau setelah tanggal awal.',
]);
}
$reference = $to ?? Carbon::today();
$from ??= $reference->copy()->subMonths(11)->startOfMonth();
$trend = $this->dashboard->monthlyTrend($from, $reference);
return view('dashboard.index', [
'reference' => $reference,
'from' => $from,
'to' => $reference,
'today' => $this->dashboard->todaySummary($reference),
'month' => $this->dashboard->monthSummary($reference),
'trend' => $trend,
'chart' => [
'labels' => $trend->pluck('label')->all(),
'revenue' => $trend->pluck('revenue')->all(),
'orders' => $trend->pluck('orders')->all(),
],
'topProducts' => $this->dashboard->topProducts($from, $reference),
'categoryShare' => $this->dashboard->categoryShare($from, $reference),
'sales' => $this->dashboard->salesQuery($from, $reference)
->paginate(10)
->withQueryString(),
]);
}
}
app/Models/Category.php
Unduh berkas<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Category extends Model
{
protected $fillable = [
'name',
'color',
];
public function products(): HasMany
{
return $this->hasMany(Product::class);
}
}
app/Models/Product.php
Unduh berkas<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Product extends Model
{
protected $fillable = [
'category_id',
'name',
'price',
];
protected function casts(): array
{
return [
'price' => 'integer',
];
}
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
public function sales(): HasMany
{
return $this->hasMany(Sale::class);
}
}
app/Models/Sale.php
Unduh berkas<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Sale extends Model
{
protected $fillable = [
'product_id',
'quantity',
'price',
'sale_date',
];
protected function casts(): array
{
return [
'quantity' => 'integer',
'price' => 'integer',
'sale_date' => 'date:Y-m-d',
];
}
public function product(): BelongsTo
{
return $this->belongsTo(Product::class);
}
}
app/Services/DashboardService.php
Unduh berkas<?php
namespace App\Services;
use App\Models\Sale;
use Carbon\Carbon;
use Carbon\CarbonInterface;
use Illuminate\Support\Collection;
class DashboardService
{
public function __construct()
{
Carbon::setLocale('id');
}
/**
* Pendapatan dan jumlah pesanan pada satu hari.
*
* @return array{revenue: int, orders: int}
*/
public function todaySummary(CarbonInterface $day): array
{
$row = Sale::query()
->where('sale_date', $day->toDateString())
->selectRaw('COALESCE(SUM(quantity * price), 0) AS revenue')
->selectRaw('COUNT(*) AS orders')
->first();
return [
'revenue' => (int) $row->revenue,
'orders' => (int) $row->orders,
];
}
/**
* Pendapatan, jumlah pesanan, dan rata-rata nilai pesanan pada
* bulan yang memuat tanggal acuan.
*
* @return array{revenue: int, orders: int, avg_order_value: int}
*/
public function monthSummary(CarbonInterface $day): array
{
$row = Sale::query()
->whereBetween('sale_date', [
$day->copy()->startOfMonth()->toDateString(),
$day->copy()->endOfMonth()->toDateString(),
])
->selectRaw('COALESCE(SUM(quantity * price), 0) AS revenue')
->selectRaw('COUNT(*) AS orders')
->first();
$revenue = (int) $row->revenue;
$orders = (int) $row->orders;
return [
'revenue' => $revenue,
'orders' => $orders,
'avg_order_value' => $orders > 0
? (int) round($revenue / $orders)
: 0,
];
}
/**
* Pendapatan dan jumlah pesanan per bulan di dalam rentang,
* termasuk bulan tanpa penjualan (diisi nol).
*
* @return Collection<int, array{month: string, label: string, revenue: int, orders: int}>
*/
public function monthlyTrend(CarbonInterface $from, CarbonInterface $to): Collection
{
$rows = Sale::query()
->whereBetween('sale_date', [$from->toDateString(), $to->toDateString()])
->groupBy('bulan')
->selectRaw('substr(sale_date, 1, 7) AS bulan')
->selectRaw('SUM(quantity * price) AS revenue')
->selectRaw('COUNT(*) AS orders')
->orderBy('bulan')
->get()
->keyBy('bulan');
$trend = new Collection();
$cursor = $from->copy()->startOfMonth();
$last = $to->copy()->startOfMonth();
while ($cursor->lessThanOrEqualTo($last)) {
$key = $cursor->format('Y-m');
$row = $rows->get($key);
$trend->push([
'month' => $key,
'label' => $cursor->translatedFormat('M Y'),
'revenue' => (int) ($row->revenue ?? 0),
'orders' => (int) ($row->orders ?? 0),
]);
$cursor->addMonth();
}
return $trend;
}
/**
* Produk dengan pendapatan terbesar di dalam rentang.
*
* @return Collection<int, array{id: int, name: string, qty: int, revenue: int}>
*/
public function topProducts(CarbonInterface $from, CarbonInterface $to, int $limit = 5): Collection
{
return Sale::query()
->join('products', 'products.id', '=', 'sales.product_id')
->whereBetween('sale_date', [$from->toDateString(), $to->toDateString()])
->groupBy('products.id', 'products.name')
->selectRaw('products.id AS id')
->selectRaw('products.name AS name')
->selectRaw('SUM(sales.quantity) AS qty')
->selectRaw('SUM(sales.quantity * sales.price) AS revenue')
->orderByDesc('revenue')
->limit($limit)
->get()
->map(fn (object $row) => [
'id' => (int) $row->id,
'name' => $row->name,
'qty' => (int) $row->qty,
'revenue' => (int) $row->revenue,
]);
}
/**
* Pendapatan per kategori di dalam rentang beserta persentase
* dari total — angka yang menjadi lebar batang di dashboard.
*
* @return Collection<int, array{name: string, color: string, revenue: int, percent: int}>
*/
public function categoryShare(CarbonInterface $from, CarbonInterface $to): Collection
{
$rows = Sale::query()
->join('products', 'products.id', '=', 'sales.product_id')
->join('categories', 'categories.id', '=', 'products.category_id')
->whereBetween('sale_date', [$from->toDateString(), $to->toDateString()])
->groupBy('categories.id', 'categories.name', 'categories.color')
->selectRaw('categories.name AS name')
->selectRaw('categories.color AS color')
->selectRaw('SUM(sales.quantity * sales.price) AS revenue')
->orderByDesc('revenue')
->get();
$total = $rows->reduce(fn (int $carry, object $row) => $carry + (int) $row->revenue, 0);
return $rows->map(fn (object $row) => [
'name' => $row->name,
'color' => $row->color,
'revenue' => (int) $row->revenue,
'percent' => $total > 0
? (int) round((int) $row->revenue / $total * 100)
: 0,
]);
}
/**
* Query dasar tabel penjualan: sudah terurut, terfilter rentang,
* dan membawa nama produk serta kategori untuk ditampilkan.
*/
public function salesQuery(CarbonInterface $from, CarbonInterface $to)
{
return Sale::query()
->join('products', 'products.id', '=', 'sales.product_id')
->join('categories', 'categories.id', '=', 'products.category_id')
->whereBetween('sale_date', [$from->toDateString(), $to->toDateString()])
->orderByDesc('sale_date')
->orderByDesc('sales.id')
->select('sales.*')
->selectRaw('products.name AS product_name')
->selectRaw('categories.name AS category_name');
}
}
app/Support/Rupiah.php
Unduh berkas<?php
namespace App\Support;
class Rupiah
{
public static function format(int $value): string
{
return 'Rp ' . number_format($value, 0, ',', '.');
}
}
database/migrations/2026_10_14_000010_create_categories_table.php
Unduh berkas<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->string('color', 7);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('categories');
}
};
database/migrations/2026_10_14_000020_create_products_table.php
Unduh berkas<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->foreignId('category_id')
->constrained()
->cascadeOnDelete();
$table->string('name');
$table->unsignedInteger('price');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('products');
}
};
database/migrations/2026_10_14_000030_create_sales_table.php
Unduh berkas<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('sales', function (Blueprint $table) {
$table->id();
$table->foreignId('product_id')
->constrained()
->restrictOnDelete();
$table->unsignedInteger('quantity');
$table->unsignedInteger('price');
$table->date('sale_date');
$table->timestamps();
$table->index('sale_date');
});
}
public function down(): void
{
Schema::dropIfExists('sales');
}
};
database/seeders/DatabaseSeeder.php
Unduh berkas<?php
namespace Database\Seeders;
use App\Models\Category;
use App\Models\Product;
use App\Models\Sale;
use Carbon\Carbon;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class DatabaseSeeder extends Seeder
{
/**
* Data penjualan 2026 dibangkitkan murni lewat rumus aritmetika —
* tanpa rand() dan tanpa now() — sehingga angkanya selalu sama
* setiap kali seeder dijalankan dan bisa dituntut tepat oleh test.
*/
public function run(): void
{
$categoryIds = collect([
['name' => 'Alat Tulis', 'color' => '#2563eb'],
['name' => 'Buku', 'color' => '#16a34a'],
['name' => 'Kertas & Catatan', 'color' => '#f97316'],
])->mapWithKeys(function (array $row): array {
$category = Category::create($row);
return [$category->name => $category->id];
});
$products = collect([
['Alat Tulis', 'Pulpen Gel Hitam 0,5', 15000],
['Alat Tulis', 'Pensil Kayu HB isi 12', 28000],
['Alat Tulis', 'Penggaris Besi 30 cm', 22000],
['Buku', 'Buku Tulis Sivi 58 halaman', 6500],
['Buku', 'Buku Agenda Hard Cover', 89000],
['Buku', 'Novel Perjalanan Pulang', 98000],
['Kertas & Catatan', 'Kertas HVS A4 80g 1 rim', 55000],
['Kertas & Catatan', 'Sticky Notes Warna 3x3', 18000],
])->map(function (array $row) use ($categoryIds): Product {
return Product::create([
'category_id' => $categoryIds[$row[0]],
'name' => $row[1],
'price' => $row[2],
]);
})->values();
$sales = [];
$total = 0;
foreach (range(1, 12) as $month) {
$monthStart = Carbon::create(2026, $month, 1);
$daysInMonth = $monthStart->daysInMonth;
$count = 35 + $month;
foreach (range(0, $count - 1) as $j) {
$day = ($j * 269 + $month * 101) % $daysInMonth + 1;
$product = $products[($j * 5 + $month) % $products->count()];
$quantity = 1 + (($j + $month) % 3);
$sales[] = [
'product_id' => $product->id,
'quantity' => $quantity,
'price' => $product->price,
'sale_date' => $monthStart->copy()->day($day)->toDateString(),
'created_at' => now()->toDateTimeString(),
'updated_at' => now()->toDateTimeString(),
];
$total++;
}
}
foreach (array_chunk($sales, 100) as $chunk) {
DB::table('sales')->insert($chunk);
}
$this->command?->info('Kategori terisi: ' . $categoryIds->count());
$this->command?->info('Produk terisi: ' . $products->count());
$this->command?->info('Penjualan terisi: ' . $total);
}
}
routes/web.php
Unduh berkas<?php
use App\Http\Controllers\DashboardController;
use Illuminate\Support\Facades\Route;
Route::get('/', fn () => redirect()->route('dashboard'))->name('home');
Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
tests/Feature/DashboardFilterTest.php
Unduh berkas<?php
namespace Tests\Feature;
use App\Services\DashboardService;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class DashboardFilterTest extends TestCase
{
use RefreshDatabase;
private DashboardService $dashboard;
protected function setUp(): void
{
parent::setUp();
$this->seed();
$this->dashboard = app(DashboardService::class);
}
public function test_filter_rentang_membatasi_agregasi_ke_periode_terpilih(): void
{
$maret = $this->dashboard->categoryShare(
Carbon::parse('2026-03-01'),
Carbon::parse('2026-03-31'),
);
$this->assertSame(['Buku', 'Kertas & Catatan', 'Alat Tulis'], $maret->pluck('name')->all());
$this->assertSame([2122000, 602000, 551000], $maret->pluck('revenue')->all());
$this->assertSame([65, 18, 17], $maret->pluck('percent')->all());
}
public function test_penjumlahan_dua_semester_sama_dengan_setahun(): void
{
$semester1 = $this->dashboard->monthlyTrend(
Carbon::parse('2026-01-01'),
Carbon::parse('2026-06-30'),
);
$semester2 = $this->dashboard->monthlyTrend(
Carbon::parse('2026-07-01'),
Carbon::parse('2026-12-31'),
);
$this->assertSame(19326500, $semester1->sum('revenue'));
$this->assertSame(21979500, $semester2->sum('revenue'));
$this->assertSame(
$semester1->sum('revenue') + $semester2->sum('revenue'),
41306000,
);
}
public function test_halaman_menerima_filter_rentang_tanggal(): void
{
$response = $this->get('/dashboard?from=2026-03-01&to=2026-03-31');
$response->assertOk();
$response->assertSee('per 31 Maret 2026');
$response->assertSee('Rp 3.275.000');
$response->assertSee('38 pesanan');
}
public function test_format_tanggal_tidak_sah_ditolak(): void
{
$response = $this->get('/dashboard?from=31-06-2026');
$response->assertStatus(302);
$response->assertSessionHasErrors('from');
}
public function test_tanggal_akhir_mendahului_awal_ditolak(): void
{
$response = $this->get('/dashboard?from=2026-05-31&to=2026-05-01');
$response->assertStatus(302);
$response->assertSessionHasErrors(['to' => 'Tanggal akhir harus sama dengan atau setelah tanggal awal.']);
}
public function test_rentang_tanpa_penjualan_menampilkan_nol(): void
{
$response = $this->get('/dashboard?from=2025-01-01&to=2025-12-31');
$response->assertOk();
$response->assertSee('Rp 0');
$response->assertSee('Belum ada penjualan pada rentang ini.');
$response->assertSee('"revenue":[0,0,0,0,0,0,0,0,0,0,0,0]', false);
}
}
tests/Feature/DashboardMetricTest.php
Unduh berkas<?php
namespace Tests\Feature;
use App\Models\Sale;
use App\Services\DashboardService;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class DashboardMetricTest extends TestCase
{
use RefreshDatabase;
private DashboardService $dashboard;
protected function setUp(): void
{
parent::setUp();
$this->seed();
$this->dashboard = app(DashboardService::class);
}
public function test_ringkasan_hari_menghitung_pendapatan_dan_pesanan_tepat(): void
{
$juli = $this->dashboard->todaySummary(Carbon::parse('2026-07-15'));
$tahunBaru = $this->dashboard->todaySummary(Carbon::parse('2026-01-01'));
$this->assertSame(['revenue' => 15000, 'orders' => 1], $juli);
$this->assertSame(['revenue' => 267000, 'orders' => 1], $tahunBaru);
}
public function test_ringkasan_bulan_menghitung_total_pesanan_dan_rata_rata(): void
{
$januari = $this->dashboard->monthSummary(Carbon::parse('2026-01-15'));
$desember = $this->dashboard->monthSummary(Carbon::parse('2026-12-15'));
$this->assertSame(3038000, $januari['revenue']);
$this->assertSame(36, $januari['orders']);
$this->assertSame(84389, $januari['avg_order_value']);
$this->assertSame(3924000, $desember['revenue']);
$this->assertSame(47, $desember['orders']);
$this->assertSame(83489, $desember['avg_order_value']);
}
public function test_tren_bulanan_setahun_menghasilkan_dua_belas_titik(): void
{
$trend = $this->dashboard->monthlyTrend(
Carbon::parse('2026-01-01'),
Carbon::parse('2026-12-31'),
);
$this->assertCount(12, $trend);
$this->assertSame(
[3038000, 2954000, 3275000, 3222500, 3435500, 3401500,
3404500, 3630000, 3359000, 3822000, 3840000, 3924000],
$trend->pluck('revenue')->all(),
);
$this->assertSame(
[36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47],
$trend->pluck('orders')->all(),
);
$this->assertSame('Des 2026', $trend->last()['label']);
}
public function test_tren_bulanan_mengisi_bulan_tanpa_penjualan_dengan_nol(): void
{
$trend = $this->dashboard->monthlyTrend(
Carbon::parse('2025-11-01'),
Carbon::parse('2026-02-28'),
);
$this->assertSame(0, $trend[0]['revenue']);
$this->assertSame(0, $trend[1]['revenue']);
$this->assertSame(3038000, $trend[2]['revenue']);
$this->assertSame(2954000, $trend[3]['revenue']);
}
public function test_produk_teratas_diurutkan_menurun_berdasar_pendapatan(): void
{
$top = $this->dashboard->topProducts(
Carbon::parse('2026-01-01'),
Carbon::parse('2026-12-31'),
);
$this->assertSame(
['Novel Perjalanan Pulang', 'Buku Agenda Hard Cover', 'Kertas HVS A4 80g 1 rim',
'Pensil Kayu HB isi 12', 'Penggaris Besi 30 cm'],
$top->pluck('name')->all(),
);
$this->assertSame(
[12054000, 11214000, 6930000, 3444000, 2772000],
$top->pluck('revenue')->all(),
);
}
public function test_pangsa_kategori_berjumlah_seratus_persen(): void
{
$share = $this->dashboard->categoryShare(
Carbon::parse('2026-01-01'),
Carbon::parse('2026-12-31'),
);
$this->assertSame(['Buku', 'Kertas & Catatan', 'Alat Tulis'], $share->pluck('name')->all());
$this->assertSame([24074000, 9126000, 8106000], $share->pluck('revenue')->all());
$this->assertSame([58, 22, 20], $share->pluck('percent')->all());
$this->assertSame(100, $share->sum('percent'));
}
public function test_agregasi_sql_sama_dengan_penjumlahan_per_baris(): void
{
$revenue = 0;
$orders = 0;
foreach (Sale::all() as $sale) {
$revenue += $sale->quantity * $sale->price;
$orders++;
}
$tahun = $this->dashboard->monthlyTrend(
Carbon::parse('2026-01-01'),
Carbon::parse('2026-12-31'),
);
$this->assertSame($revenue, $tahun->sum('revenue'));
$this->assertSame($orders, $tahun->sum('orders'));
$this->assertSame(498, $orders);
}
}
tests/Feature/DashboardPageTest.php
Unduh berkas<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class DashboardPageTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->seed();
}
public function test_dashboard_tampil_tanpa_login(): void
{
$response = $this->get('/dashboard');
$response->assertOk();
$response->assertSee('Dashboard Penjualan');
$response->assertSee('Tren Bulanan');
$response->assertSee('Pangsa Kategori');
$response->assertSee('Riwayat Penjualan');
}
public function test_muatan_grafik_garis_terender_penuh_dari_server(): void
{
$response = $this->get('/dashboard?from=2026-01-01&to=2026-12-31');
$payload = json_encode([
'labels' => ['Jan 2026', 'Feb 2026', 'Mar 2026', 'Apr 2026', 'Mei 2026', 'Jun 2026',
'Jul 2026', 'Agt 2026', 'Sep 2026', 'Okt 2026', 'Nov 2026', 'Des 2026'],
'revenue' => [3038000, 2954000, 3275000, 3222500, 3435500, 3401500,
3404500, 3630000, 3359000, 3822000, 3840000, 3924000],
'orders' => [36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47],
], 15);
$response->assertOk();
$response->assertSee('const trend = ' . $payload . ';', false);
}
public function test_batang_kategori_menampilkan_lebar_persentase(): void
{
$response = $this->get('/dashboard?from=2026-01-01&to=2026-12-31');
$response->assertOk();
$response->assertSee('width: 58%; background: #16a34a;', false);
$response->assertSee('width: 22%; background: #f97316;', false);
$response->assertSee('width: 20%; background: #2563eb;', false);
$response->assertSee('Rp 24.074.000 (58%)');
}
public function test_kartu_ringkasan_menampilkan_angka_bulan_acuan(): void
{
$response = $this->get('/dashboard?from=2026-01-01&to=2026-12-31');
$response->assertOk();
$response->assertSee('per 31 Desember 2026');
$response->assertSee('Pendapatan Bulan Desember');
$response->assertSee('Rp 3.924.000');
$response->assertSee('47 pesanan');
$response->assertSee('Rp 83.489');
}
public function test_tabel_penjualan_terpaginasi_sepuluh_baris_per_halaman(): void
{
$response = $this->get('/dashboard?from=2026-01-01&to=2026-12-31');
$response->assertOk();
// 17 baris = 6 dari tabel produk teratas (kepala + 5) + 11 dari
// tabel penjualan (kepala + 10 baris terpaginasi).
$this->assertSame(17, substr_count($response->getContent(), '<tr>'));
$response->assertSee('Menampilkan 1–10', false);
$response->assertSee('dari 498 penjualan');
}
public function test_paginasi_menjaga_query_filter(): void
{
$response = $this->get('/dashboard?from=2026-01-01&to=2026-12-31&page=2');
$response->assertOk();
$response->assertSee('Menampilkan 11–20', false);
$response->assertSee('from=2026-01-01&to=2026-12-31&page=3', false);
$response->assertSee('from=2026-01-01&to=2026-12-31&page=1', false);
}
}
tests/Feature/ExampleTest.php
Unduh berkas<?php
namespace Tests\Feature;
use Tests\TestCase;
class ExampleTest extends TestCase
{
public function test_halaman_utama_mengalihkan_ke_dashboard(): void
{
$this->get('/')->assertRedirect(route('dashboard'));
}
}