Bagian 11 dari seri

Pengujian Sistem Kasir Laravel

Diterbitkan 16 September 2026

Pada tahap kesebelas kita akan melakukan pengujian Sistem Kasir Laravel sebelum aplikasi dipindahkan ke server production.

Sistem kasir tidak cukup diuji hanya dengan membuka halaman dan memastikan tidak ada error.

Bagian penting seperti:

login
hak akses
harga produk
stok
penjualan
pembelian
pembatalan transaksi
laporan

harus diuji secara terstruktur.

Pengujian penting karena kesalahan kecil dapat menyebabkan:

stok salah
total transaksi salah
transaksi ganda
kasir membuka halaman admin
pembatalan mengembalikan stok dua kali
laporan menghitung transaksi cancelled

Pada tahap ini kita akan menggunakan dua pendekatan:

automated test
manual test

Automated test menggunakan fitur testing Laravel.

Manual test digunakan untuk memeriksa alur antarmuka, print nota, filter, dan perilaku aplikasi secara keseluruhan.

Hasil yang Akan Dibuat

Pada tahap ini kita akan membuat test untuk:

  • user yang belum login;
  • hak akses admin dan kasir;
  • produk;
  • transaksi penjualan;
  • validasi stok;
  • harga dari database;
  • pengurangan stok;
  • stock movement;
  • pembayaran kurang;
  • pembatalan transaksi;
  • proteksi pembatalan ganda;
  • transaksi pembelian;
  • penambahan stok;
  • pembatalan pembelian;
  • laporan hanya menghitung transaksi paid.

File test utama:

tests/Feature/AuthAccessTest.php
tests/Feature/ProductTest.php
tests/Feature/SaleTest.php
tests/Feature/PurchaseTest.php
tests/Feature/ReportTest.php

Persiapan

Pastikan Tahap 01 sampai Tahap 10 sudah selesai.

Masuk ke folder project:

cd kasir-laravel

Pastikan aplikasi masih berjalan normal.

Sebelum testing, cek:

php artisan about

Kemudian:

php artisan test

Jika project masih menggunakan test bawaan Laravel, hasilnya seharusnya tetap dapat dijalankan.

1. Jangan Menguji Menggunakan Database Production

Automated test tidak boleh menggunakan database production.

Laravel biasanya menggunakan environment:

testing

Kita akan menggunakan:

RefreshDatabase

agar database test dapat disiapkan ulang.

Untuk project sederhana, SQLite in-memory dapat digunakan jika seluruh fitur database yang dipakai kompatibel.

Alternatifnya gunakan database MySQL khusus testing.

2. Konfigurasi Testing dengan SQLite

Buka:

phpunit.xml

Pastikan environment testing mempunyai:

<env name="APP_ENV" value="testing"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>

Contoh bagian environment:

<php>
    <env name="APP_ENV" value="testing"/>
    <env name="APP_MAINTENANCE_DRIVER" value="file"/>
    <env name="BCRYPT_ROUNDS" value="4"/>
    <env name="CACHE_STORE" value="array"/>
    <env name="DB_CONNECTION" value="sqlite"/>
    <env name="DB_DATABASE" value=":memory:"/>
    <env name="MAIL_MAILER" value="array"/>
    <env name="QUEUE_CONNECTION" value="sync"/>
    <env name="SESSION_DRIVER" value="array"/>
</php>

Jika project menggunakan MySQL testing, bagian database dapat disesuaikan.

3. Mengapa Menggunakan RefreshDatabase?

Test class akan menggunakan:

use RefreshDatabase;

Trait ini membantu menyiapkan database untuk setiap test.

Dengan demikian satu test tidak mudah mengganggu test lainnya.

4. Membuat AuthAccessTest

Jalankan:

php artisan make:test AuthAccessTest

File:

tests/Feature/AuthAccessTest.php

Isi:

<?php

namespace Tests\Feature;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class AuthAccessTest extends TestCase
{
    use RefreshDatabase;

    public function test_guest_cannot_open_dashboard(): void
    {
        $response = $this->get('/dashboard');

        $response->assertRedirect('/login');
    }

    public function test_admin_can_open_master_data(): void
    {
        $admin = User::factory()->create([
            'role' => 'admin',
            'is_active' => true,
        ]);

        $response = $this
            ->actingAs($admin)
            ->get('/categories');

        $response->assertOk();
    }

    public function test_cashier_cannot_open_admin_master_data(): void
    {
        $cashier = User::factory()->create([
            'role' => 'cashier',
            'is_active' => true,
        ]);

        $response = $this
            ->actingAs($cashier)
            ->get('/categories');

        $response->assertForbidden();
    }

    public function test_cashier_can_open_sales_page(): void
    {
        $cashier = User::factory()->create([
            'role' => 'cashier',
            'is_active' => true,
        ]);

        $response = $this
            ->actingAs($cashier)
            ->get('/sales');

        $response->assertOk();
    }
}

5. Jika Middleware Role Menggunakan Redirect

Pada Tahap 01 middleware role dapat saja dibuat dengan:

403

atau redirect.

Jika implementasi middleware menggunakan redirect, sesuaikan assertion.

Contoh:

$response->assertRedirect('/dashboard');

Gunakan assertion sesuai implementasi project.

6. Factory User dan Kolom Role

Pastikan factory User tidak bermasalah dengan kolom:

role
is_active

Kita menimpa nilainya pada test:

User::factory()->create([
    'role' => 'admin',
    'is_active' => true,
]);

7. Membuat ProductTest

Jalankan:

php artisan make:test ProductTest

Isi:

<?php

namespace Tests\Feature;

use App\Models\Category;
use App\Models\Product;
use App\Models\Unit;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class ProductTest extends TestCase
{
    use RefreshDatabase;

    private function admin(): User
    {
        return User::factory()->create([
            'role' => 'admin',
            'is_active' => true,
        ]);
    }

    public function test_admin_can_create_product(): void
    {
        $admin = $this->admin();

        $category = Category::create([
            'name' => 'Minuman',
        ]);

        $unit = Unit::create([
            'name' => 'Botol',
            'symbol' => 'btl',
        ]);

        $response = $this
            ->actingAs($admin)
            ->post('/products', [
                'category_id' => $category->id,
                'unit_id' => $unit->id,
                'sku' => 'brg-100',
                'name' => 'Produk Test',
                'purchase_price' => 5000,
                'selling_price' => 7000,
                'minimum_stock' => 5,
                'is_active' => 1,
            ]);

        $response->assertRedirect('/products');

        $this->assertDatabaseHas('products', [
            'sku' => 'BRG-100',
            'name' => 'Produk Test',
            'stock' => 0,
        ]);
    }

    public function test_product_stock_cannot_be_set_from_create_request(): void
    {
        $admin = $this->admin();

        $response = $this
            ->actingAs($admin)
            ->post('/products', [
                'sku' => 'BRG-101',
                'name' => 'Produk Stock Test',
                'purchase_price' => 1000,
                'selling_price' => 2000,
                'minimum_stock' => 1,
                'stock' => 999,
                'is_active' => 1,
            ]);

        $response->assertRedirect('/products');

        $this->assertDatabaseHas('products', [
            'sku' => 'BRG-101',
            'stock' => 0,
        ]);
    }

    public function test_duplicate_sku_is_rejected(): void
    {
        $admin = $this->admin();

        Product::create([
            'sku' => 'BRG-001',
            'name' => 'Produk Lama',
            'purchase_price' => 1000,
            'selling_price' => 2000,
            'stock' => 0,
            'minimum_stock' => 0,
            'is_active' => true,
        ]);

        $response = $this
            ->actingAs($admin)
            ->post('/products', [
                'sku' => 'BRG-001',
                'name' => 'Produk Baru',
                'purchase_price' => 1000,
                'selling_price' => 2500,
                'minimum_stock' => 0,
                'is_active' => 1,
            ]);

        $response->assertSessionHasErrors('sku');
    }
}

8. Test Penting Produk

Test kedua memastikan request seperti:

stock=999

tidak dapat memanipulasi saldo produk.

Controller tetap membuat:

stock = 0

9. Membuat SaleTest

Jalankan:

php artisan make:test SaleTest

Isi:

<?php

namespace Tests\Feature;

use App\Models\Product;
use App\Models\Sale;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class SaleTest extends TestCase
{
    use RefreshDatabase;

    private function cashier(): User
    {
        return User::factory()->create([
            'role' => 'cashier',
            'is_active' => true,
        ]);
    }

    private function admin(): User
    {
        return User::factory()->create([
            'role' => 'admin',
            'is_active' => true,
        ]);
    }

    private function product(
        int $stock = 10,
        float $price = 5000
    ): Product {
        return Product::create([
            'sku' => 'BRG-' . uniqid(),
            'name' => 'Produk Test',
            'purchase_price' => 3000,
            'selling_price' => $price,
            'stock' => $stock,
            'minimum_stock' => 2,
            'is_active' => true,
        ]);
    }

    public function test_sale_reduces_stock(): void
    {
        $cashier = $this->cashier();
        $product = $this->product(stock: 10);

        $response = $this
            ->actingAs($cashier)
            ->post('/sales', [
                'items' => [
                    [
                        'product_id' => $product->id,
                        'quantity' => 2,
                    ],
                ],
                'discount' => 0,
                'paid_amount' => 10000,
            ]);

        $response->assertRedirect();

        $this->assertDatabaseHas('products', [
            'id' => $product->id,
            'stock' => 8,
        ]);

        $this->assertDatabaseHas('stock_movements', [
            'product_id' => $product->id,
            'type' => 'sale',
            'quantity_out' => 2,
        ]);
    }

    public function test_server_uses_database_price(): void
    {
        $cashier = $this->cashier();

        $product = $this->product(
            stock: 10,
            price: 5000
        );

        $this
            ->actingAs($cashier)
            ->post('/sales', [
                'items' => [
                    [
                        'product_id' => $product->id,
                        'quantity' => 2,
                        'price' => 1,
                        'subtotal' => 2,
                    ],
                ],
                'grand_total' => 2,
                'discount' => 0,
                'paid_amount' => 10000,
            ]);

        $sale = Sale::latest('id')->first();

        $this->assertNotNull($sale);

        $this->assertEquals(
            10000,
            (float) $sale->grand_total
        );

        $detail = $sale->details()->first();

        $this->assertEquals(
            5000,
            (float) $detail->price
        );
    }

    public function test_sale_fails_when_stock_is_insufficient(): void
    {
        $cashier = $this->cashier();
        $product = $this->product(stock: 1);

        $response = $this
            ->actingAs($cashier)
            ->post('/sales', [
                'items' => [
                    [
                        'product_id' => $product->id,
                        'quantity' => 2,
                    ],
                ],
                'discount' => 0,
                'paid_amount' => 10000,
            ]);

        $response->assertSessionHasErrors('items');

        $this->assertDatabaseCount('sales', 0);

        $this->assertDatabaseHas('products', [
            'id' => $product->id,
            'stock' => 1,
        ]);
    }

    public function test_sale_fails_when_payment_is_less_than_total(): void
    {
        $cashier = $this->cashier();
        $product = $this->product(
            stock: 10,
            price: 5000
        );

        $response = $this
            ->actingAs($cashier)
            ->post('/sales', [
                'items' => [
                    [
                        'product_id' => $product->id,
                        'quantity' => 2,
                    ],
                ],
                'discount' => 0,
                'paid_amount' => 5000,
            ]);

        $response->assertSessionHasErrors('paid_amount');

        $this->assertDatabaseCount('sales', 0);

        $this->assertDatabaseHas('products', [
            'id' => $product->id,
            'stock' => 10,
        ]);
    }

    public function test_cancelled_sale_restores_stock_once(): void
    {
        $cashier = $this->cashier();
        $admin = $this->admin();
        $product = $this->product(stock: 10);

        $this
            ->actingAs($cashier)
            ->post('/sales', [
                'items' => [
                    [
                        'product_id' => $product->id,
                        'quantity' => 2,
                    ],
                ],
                'discount' => 0,
                'paid_amount' => 10000,
            ]);

        $sale = Sale::latest('id')->first();

        $this->assertDatabaseHas('products', [
            'id' => $product->id,
            'stock' => 8,
        ]);

        $this
            ->actingAs($admin)
            ->post("/sales/{$sale->id}/cancel");

        $this->assertDatabaseHas('products', [
            'id' => $product->id,
            'stock' => 10,
        ]);

        $this->assertDatabaseHas('sales', [
            'id' => $sale->id,
            'status' => 'cancelled',
        ]);

        $secondResponse = $this
            ->actingAs($admin)
            ->post("/sales/{$sale->id}/cancel");

        $secondResponse->assertSessionHasErrors('cancel');

        $this->assertDatabaseHas('products', [
            'id' => $product->id,
            'stock' => 10,
        ]);

        $this->assertDatabaseCount(
            'stock_movements',
            2
        );
    }
}

10. Mengapa Test Harga Sangat Penting?

Test:

server_uses_database_price

mencoba mengirim:

price = 1
subtotal = 2
grand_total = 2

Server harus mengabaikan nilai tersebut.

Jika database price:

5000

dan quantity:

2

grand total tetap:

10000

11. Menguji Rollback Secara Tidak Langsung

Pada test stok tidak cukup:

sales count = 0
stock tetap

Ini membuktikan transaksi gagal tanpa meninggalkan header transaksi.

Test pembayaran kurang juga memastikan perubahan tidak tersimpan.

12. Membuat PurchaseTest

Jalankan:

php artisan make:test PurchaseTest

Isi:

<?php

namespace Tests\Feature;

use App\Models\Product;
use App\Models\Purchase;
use App\Models\Supplier;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class PurchaseTest extends TestCase
{
    use RefreshDatabase;

    private function admin(): User
    {
        return User::factory()->create([
            'role' => 'admin',
            'is_active' => true,
        ]);
    }

    private function product(int $stock = 5): Product
    {
        return Product::create([
            'sku' => 'BRG-' . uniqid(),
            'name' => 'Produk Test',
            'purchase_price' => 2000,
            'selling_price' => 4000,
            'stock' => $stock,
            'minimum_stock' => 2,
            'is_active' => true,
        ]);
    }

    public function test_purchase_adds_stock(): void
    {
        $admin = $this->admin();
        $product = $this->product(stock: 5);

        $supplier = Supplier::create([
            'name' => 'Supplier Test',
        ]);

        $response = $this
            ->actingAs($admin)
            ->post('/purchases', [
                'supplier_id' => $supplier->id,
                'purchase_date' => now()
                    ->format('Y-m-d H:i:s'),
                'items' => [
                    [
                        'product_id' => $product->id,
                        'quantity' => 10,
                        'price' => 2500,
                    ],
                ],
            ]);

        $response->assertRedirect();

        $this->assertDatabaseHas('products', [
            'id' => $product->id,
            'stock' => 15,
        ]);

        $this->assertDatabaseHas('stock_movements', [
            'product_id' => $product->id,
            'type' => 'purchase',
            'quantity_in' => 10,
        ]);
    }

    public function test_purchase_updates_latest_purchase_price(): void
    {
        $admin = $this->admin();
        $product = $this->product();

        $this
            ->actingAs($admin)
            ->post('/purchases', [
                'purchase_date' => now()
                    ->format('Y-m-d H:i:s'),
                'items' => [
                    [
                        'product_id' => $product->id,
                        'quantity' => 2,
                        'price' => 2750,
                    ],
                ],
            ]);

        $this->assertDatabaseHas('products', [
            'id' => $product->id,
            'purchase_price' => 2750,
        ]);
    }

    public function test_cancel_purchase_reduces_stock(): void
    {
        $admin = $this->admin();
        $product = $this->product(stock: 5);

        $this
            ->actingAs($admin)
            ->post('/purchases', [
                'purchase_date' => now()
                    ->format('Y-m-d H:i:s'),
                'items' => [
                    [
                        'product_id' => $product->id,
                        'quantity' => 10,
                        'price' => 2500,
                    ],
                ],
            ]);

        $purchase = Purchase::latest('id')->first();

        $this->assertDatabaseHas('products', [
            'id' => $product->id,
            'stock' => 15,
        ]);

        $this
            ->actingAs($admin)
            ->post(
                "/purchases/{$purchase->id}/cancel"
            );

        $this->assertDatabaseHas('products', [
            'id' => $product->id,
            'stock' => 5,
        ]);

        $this->assertDatabaseHas('purchases', [
            'id' => $purchase->id,
            'status' => 'cancelled',
        ]);
    }

    public function test_purchase_cancel_is_rejected_if_stock_is_not_enough(): void
    {
        $admin = $this->admin();
        $product = $this->product(stock: 0);

        $this
            ->actingAs($admin)
            ->post('/purchases', [
                'purchase_date' => now()
                    ->format('Y-m-d H:i:s'),
                'items' => [
                    [
                        'product_id' => $product->id,
                        'quantity' => 10,
                        'price' => 2500,
                    ],
                ],
            ]);

        $purchase = Purchase::latest('id')->first();

        $product->update([
            'stock' => 3,
        ]);

        $response = $this
            ->actingAs($admin)
            ->post(
                "/purchases/{$purchase->id}/cancel"
            );

        $response->assertSessionHasErrors('cancel');

        $this->assertDatabaseHas('purchases', [
            'id' => $purchase->id,
            'status' => 'received',
        ]);
    }
}

13. Catatan Test Manipulasi Stock pada Purchase

Pada test terakhir kita mengubah:

$product->update([
    'stock' => 3,
]);

secara langsung hanya untuk mensimulasikan kondisi bahwa stok sudah keluar setelah pembelian.

Pada aplikasi normal perubahan tersebut dilakukan oleh penjualan.

Dalam production jangan menggunakan cara langsung tersebut.

14. Membuat ReportTest

Jalankan:

php artisan make:test ReportTest

Isi:

<?php

namespace Tests\Feature;

use App\Models\Sale;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class ReportTest extends TestCase
{
    use RefreshDatabase;

    public function test_report_revenue_only_counts_paid_sales(): void
    {
        $admin = User::factory()->create([
            'role' => 'admin',
            'is_active' => true,
        ]);

        Sale::create([
            'invoice_number' => 'INV-PAID',
            'user_id' => $admin->id,
            'sale_date' => now(),
            'subtotal' => 100000,
            'discount' => 0,
            'grand_total' => 100000,
            'paid_amount' => 100000,
            'change_amount' => 0,
            'status' => 'paid',
        ]);

        Sale::create([
            'invoice_number' => 'INV-CANCELLED',
            'user_id' => $admin->id,
            'sale_date' => now(),
            'subtotal' => 50000,
            'discount' => 0,
            'grand_total' => 50000,
            'paid_amount' => 50000,
            'change_amount' => 0,
            'status' => 'cancelled',
        ]);

        $response = $this
            ->actingAs($admin)
            ->get('/reports/sales?status=');

        $response->assertOk();

        $response->assertViewHas(
            'totalRevenue',
            function ($value) {
                return (float) $value === 100000.0;
            }
        );
    }
}

15. Menjalankan Semua Test

Jalankan:

php artisan test

Laravel menjalankan seluruh test.

Hasil ideal:

PASS

untuk semua test.

16. Menjalankan Satu File Test

Contoh:

php artisan test tests/Feature/SaleTest.php

17. Menjalankan Test Berdasarkan Nama

Contoh:

php artisan test --filter=sale_reduces_stock

Ini berguna ketika sedang memperbaiki satu fitur.

18. Jika Test Gagal

Baca bagian:

FAIL

dan error yang diberikan.

Jangan hanya mengubah assertion agar test lolos.

Pastikan behavior aplikasi memang benar.

19. Manual Test Login

Uji:

[ ] login admin berhasil
[ ] login kasir berhasil
[ ] password salah ditolak
[ ] user tidak aktif ditolak
[ ] logout berhasil

20. Manual Test Role

Admin:

[ ] dapat membuka master data
[ ] dapat membuka produk
[ ] dapat membuka pembelian
[ ] dapat membuka stok
[ ] dapat membuka laporan
[ ] dapat membatalkan transaksi

Kasir:

[ ] dapat membuka dashboard
[ ] dapat membuka penjualan
[ ] dapat membuat penjualan
[ ] tidak dapat membuka master data admin
[ ] tidak dapat membuka pembelian
[ ] tidak dapat membuka adjustment stok
[ ] tidak dapat membuka laporan admin
[ ] tidak dapat cancel transaksi

21. Manual Test CRUD Master

Kategori:

[ ] create
[ ] edit
[ ] search
[ ] unique
[ ] delete aman

Satuan:

[ ] create
[ ] edit
[ ] search
[ ] delete aman

Pelanggan:

[ ] create
[ ] edit
[ ] search
[ ] delete

Supplier:

[ ] create
[ ] edit
[ ] search
[ ] delete

22. Manual Test Produk

Uji:

[ ] SKU unique
[ ] SKU uppercase
[ ] kategori valid
[ ] satuan valid
[ ] harga negatif ditolak
[ ] stock awal 0
[ ] stock tidak dapat diedit
[ ] produk dapat nonaktif
[ ] produk dengan histori tidak dapat dihapus

23. Manual Test Penjualan

Uji transaksi normal:

[ ] produk tampil
[ ] pencarian bekerja
[ ] cart bekerja
[ ] qty bekerja
[ ] subtotal tampil
[ ] diskon bekerja
[ ] pembayaran bekerja
[ ] kembalian benar
[ ] sale tersimpan
[ ] detail tersimpan
[ ] stock berkurang
[ ] movement dibuat

24. Manual Test Penjualan Negatif

Uji:

[ ] cart kosong ditolak
[ ] quantity 0 ditolak
[ ] quantity melebihi stock ditolak
[ ] produk inactive ditolak
[ ] pembayaran kurang ditolak
[ ] diskon > subtotal ditolak
[ ] harga browser tidak dipercaya

25. Manual Test Cancel Sale

Uji:

[ ] admin dapat cancel
[ ] kasir tidak dapat cancel
[ ] status berubah cancelled
[ ] stock kembali
[ ] movement reversal dibuat
[ ] cancel kedua ditolak

26. Manual Test Pembelian

Uji:

[ ] supplier dapat dipilih
[ ] item multi produk
[ ] quantity valid
[ ] harga beli valid
[ ] total benar
[ ] purchase tersimpan
[ ] detail tersimpan
[ ] stock bertambah
[ ] purchase_price diperbarui
[ ] movement dibuat

27. Manual Test Cancel Purchase

Uji:

[ ] cancel normal bekerja
[ ] stock dikurangi kembali
[ ] movement reversal dibuat
[ ] cancel kedua ditolak
[ ] cancel ditolak jika stock tidak cukup

28. Manual Test Kartu Stok

Uji:

[ ] initial tampil
[ ] sale tampil
[ ] sale_cancelled tampil
[ ] purchase tampil
[ ] purchase_cancelled tampil
[ ] adjustment tampil
[ ] saldo berjalan benar
[ ] filter type benar
[ ] filter tanggal benar

29. Manual Test Adjustment

Uji:

[ ] stock naik
[ ] stock turun
[ ] movement dibuat
[ ] notes wajib
[ ] nilai sama ditolak
[ ] negatif ditolak

30. Manual Test Dashboard

Uji:

[ ] omzet paid saja
[ ] cancelled tidak dihitung
[ ] jumlah transaksi benar
[ ] produk aktif benar
[ ] stok habis benar
[ ] low stock benar
[ ] best seller benar

31. Manual Test Laporan

Uji:

[ ] default awal bulan
[ ] tanggal valid
[ ] tanggal salah ditolak
[ ] filter paid
[ ] filter cancelled
[ ] filter semua
[ ] filter kasir
[ ] search invoice
[ ] pagination
[ ] omzet tidak menghitung cancelled

32. Manual Test Nota

Uji:

[ ] 58mm
[ ] 80mm
[ ] data toko
[ ] invoice
[ ] kasir
[ ] pelanggan
[ ] item
[ ] harga historis
[ ] total
[ ] bayar
[ ] kembalian
[ ] cancelled diberi tanda
[ ] toolbar tidak tercetak

33. Menguji Konsistensi Stock

Pilih satu produk.

Hitung:

$movementBalance =
    $product->stockMovements()->sum('quantity_in')
    -
    $product->stockMovements()->sum('quantity_out');

Bandingkan:

$product->stock;

Harus sama.

34. Membuat Pemeriksaan Cepat via Tinker

Jalankan:

php artisan tinker

Kemudian:

use App\Models\Product;

Cek produk yang tidak konsisten:

Product::all()->filter(function ($product) {
    $movementBalance =
        $product->stockMovements()->sum('quantity_in')
        -
        $product->stockMovements()->sum('quantity_out');

    return (int) $product->stock
        !==
        (int) $movementBalance;
});

Hasil ideal:

collection kosong

35. Menguji Status Production Sebelum Deploy

Pastikan tidak ada:

dd()
dump()
ray()

yang tertinggal pada controller.

Cari secara manual di project.

36. Cek Route

Jalankan:

php artisan route:list

Pastikan tidak ada route admin yang lupa menggunakan middleware role.

37. Cek Migration

Jalankan:

php artisan migrate:status

Semua migration harus:

Ran

38. Cek Config

Jalankan:

php artisan config:clear
php artisan cache:clear
php artisan view:clear
php artisan route:clear

Kemudian test ulang aplikasi.

39. Jangan Mengandalkan Testing Otomatis Saja

Automated test sangat membantu untuk logic.

Namun beberapa hal tetap harus diuji manual:

layout
mobile responsive
printer thermal
browser print
menu
flash message
JavaScript cart
UX

40. Browser yang Perlu Diuji

Minimal coba:

Chrome
Edge

Jika pengguna target menggunakan browser lain, lakukan pengujian tambahan.

41. Uji Resolusi Desktop

Karena POS umumnya digunakan desktop, uji minimal pada:

1366 × 768
1920 × 1080

Pastikan keranjang masih mudah digunakan.

42. Uji Mobile Secukupnya

Walaupun kasir biasanya desktop, halaman admin sebaiknya tetap dapat dibuka dari mobile.

Periksa:

table responsive
form
navigation
button

43. Uji Printer Sebelum Production

Jika aplikasi akan benar-benar dipakai kasir, jangan hanya mengandalkan print preview.

Cetak ke printer thermal nyata.

Periksa:

lebar
margin
font
potongan kertas
kecepatan

44. Uji Data Besar Sederhana

Tambahkan banyak data produk dan transaksi.

Pastikan:

pagination tetap bekerja
dashboard tetap wajar
laporan tidak terlalu lambat

Untuk aplikasi skala kecil, query tutorial ini masih cukup sederhana.

45. Uji Backup Database

Sebelum production, lakukan satu simulasi:

backup database
restore database

Pastikan backup benar-benar dapat digunakan.

Backup yang tidak pernah diuji restore belum tentu aman.

46. Checklist Akhir Sebelum Deploy

Pastikan:

[ ] login aman
[ ] role benar
[ ] CRUD bekerja
[ ] produk bekerja
[ ] penjualan bekerja
[ ] pembelian bekerja
[ ] stock konsisten
[ ] cancel aman
[ ] kartu stok benar
[ ] adjustment benar
[ ] dashboard benar
[ ] laporan benar
[ ] nota benar
[ ] automated test lulus
[ ] manual test selesai
[ ] tidak ada debug code
[ ] backup siap

47. Kesalahan Umum: Test Hanya Happy Path

Jangan hanya menguji transaksi berhasil.

Uji juga:

stok tidak cukup
harga dimanipulasi
pembayaran kurang
produk inactive
cancel dua kali
hak akses salah

48. Kesalahan Umum: Menggunakan Database Asli

Jangan menjalankan test otomatis pada database production.

Gunakan database testing.

49. Kesalahan Umum: Test Lulus tetapi UI Belum Diuji

Automated test tidak menggantikan pengecekan UI dan printer.

50. Kesalahan Umum: Tidak Menguji Cancel

Fitur cancel sangat berisiko karena dapat mengubah stok kembali.

Test pembatalan wajib.

51. Kesalahan Umum: Tidak Menguji Manipulasi Harga

Pastikan server tidak mempercayai nilai:

price
subtotal
grand_total

dari request penjualan.

52. Kesalahan Umum: Laporan Menghitung Cancelled

Test khusus laporan perlu memastikan omzet paid saja.

53. Pengembangan Testing Selanjutnya

Test dapat dikembangkan menjadi:

unit test
browser test
parallel test
CI/CD
GitHub Actions
coverage
performance test
security test

Untuk tutorial dasar, feature test di atas sudah memberikan fondasi yang baik.

54. Checkpoint Hasil

Setelah Tahap 11:

Login                         : selesai
Role admin/kasir              : selesai
Database                      : selesai
Model dan relasi              : selesai
Seeder                        : selesai
CRUD master                   : selesai
CRUD produk                   : selesai
Penjualan                     : selesai
Pembelian                     : selesai
Kartu stok                    : selesai
Adjustment                    : selesai
Dashboard                     : selesai
Laporan                       : selesai
Nota                          : selesai
Feature test                  : selesai
Manual test                   : selesai
Deploy                        : tahap terakhir

55. Ringkasan

Pada tahap kesebelas kita telah menambahkan pengujian untuk bagian terpenting sistem kasir.

Automated test mencakup:

auth
role
produk
penjualan
stok
harga server-side
pembayaran
cancel sale
pembelian
cancel purchase
laporan

Manual test melengkapi bagian yang sulit diverifikasi hanya dengan feature test:

JavaScript POS
layout
responsive
print thermal
filter
UX

Sebelum deploy, pastikan:

php artisan test

memberikan hasil yang sesuai dan checklist manual sudah selesai.

Tahap terakhir akan membahas deployment sistem kasir Laravel ke hosting/server production.

Slug lama pengujian-dan-deploy akan tetap digunakan agar URL artikel lama tidak berubah.

Lanjut: Deploy Sistem Kasir Laravel