HARI 5: Testing dan Deployment â
Bootcamp Laravel - Sistem Pengelolaan Dokumen (SiDoku) â
đ Informasi Sesi â
| Item | Keterangan |
|---|---|
| Hari | 5 dari 5 |
| Durasi | 8 Jam (09:00 - 17:00 WIB) |
| Topik Utama | Testing, Optimization, Deployment |
| Project | SiDoku - Finalisasi dan deployment |
đŻ Learning Objectives â
Setelah menyelesaikan sesi ini, peserta akan mampu:
- Menulis dan menjalankan unit test dan feature test
- Mengoptimasi performa aplikasi Laravel
- Mengkonfigurasi environment production
- Melakukan deployment ke server
- Menerapkan best practices keamanan
- Melakukan maintenance dan monitoring
SESI 1: Testing di Laravel â
1.1 Pengenalan Testing â
Laravel menyediakan dukungan testing yang komprehensif dengan PHPUnit.
Jenis Testing:
| Jenis | Lokasi | Fungsi |
|---|---|---|
| Unit Test | tests/Unit/ | Test fungsi/method individual |
| Feature Test | tests/Feature/ | Test fitur lengkap (HTTP, database) |
1.2 Konfigurasi Testing â
Edit phpunit.xml:
<php>
<env name="APP_ENV" value="testing"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_DRIVER" 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>â ď¸ Catatan PHPUnit 11+: Syntax
/** @test */sudah deprecated dan akan dihapus di PHPUnit 12. Gunakan formatpublic function test_nama_method()sebagai gantinya.
1.3 Unit Test â
Buat unit test:
php artisan make:test DokumenUnitTest --unitEdit tests/Unit/DokumenUnitTest.php:
<?php
namespace Tests\Unit;
use App\Models\Dokumen;
use App\Models\Kategori;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class DokumenUnitTest extends TestCase
{
use RefreshDatabase;
public function test_dokumen_can_be_created(): void
{
$user = User::factory()->create();
$kategori = Kategori::factory()->create();
$dokumen = Dokumen::create([
'user_id' => $user->id,
'kategori_id' => $kategori->id,
'judul' => 'Test Dokumen',
'nomor' => 'TEST/001/2025',
'tanggal_dokumen' => now(),
]);
$this->assertDatabaseHas('dokumens', [
'judul' => 'Test Dokumen',
'nomor' => 'TEST/001/2025',
]);
}
public function test_dokumen_belongs_to_kategori(): void
{
$kategori = Kategori::factory()->create(['nama' => 'Surat Keputusan']);
$dokumen = Dokumen::factory()->create(['kategori_id' => $kategori->id]);
$this->assertEquals('Surat Keputusan', $dokumen->kategori->nama);
}
public function test_dokumen_belongs_to_user(): void
{
$user = User::factory()->create(['name' => 'John Doe']);
$dokumen = Dokumen::factory()->create(['user_id' => $user->id]);
$this->assertEquals('John Doe', $dokumen->user->name);
}
public function test_dokumen_status_default_is_draft(): void
{
$dokumen = Dokumen::factory()->create();
// Jika factory tidak mengoverride status
$this->assertContains($dokumen->status, ['draft', 'pending', 'approved', 'rejected']);
}
public function test_scope_status_filters_correctly(): void
{
Dokumen::factory()->count(3)->create(['status' => 'approved']);
Dokumen::factory()->count(2)->create(['status' => 'pending']);
$approved = Dokumen::status('approved')->count();
$pending = Dokumen::status('pending')->count();
$this->assertEquals(3, $approved);
$this->assertEquals(2, $pending);
}
}1.4 Feature Test â
Buat feature test:
php artisan make:test DokumenFeatureTestEdit tests/Feature/DokumenFeatureTest.php:
<?php
namespace Tests\Feature;
use App\Models\Dokumen;
use App\Models\Kategori;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class DokumenFeatureTest extends TestCase
{
use RefreshDatabase;
private User $user;
private Kategori $kategori;
protected function setUp(): void
{
parent::setUp();
$this->user = User::factory()->create();
$this->kategori = Kategori::factory()->create();
}
public function test_guest_cannot_access_dokumen_index(): void
{
$response = $this->get('/dokumen');
$response->assertRedirect('/login');
}
public function test_authenticated_user_can_view_dokumen_index(): void
{
Dokumen::factory()->count(5)->create();
$response = $this->actingAs($this->user)->get('/dokumen');
$response->assertStatus(200);
$response->assertViewIs('dokumen.index');
$response->assertViewHas('dokumens');
}
public function test_user_can_create_dokumen(): void
{
$data = [
'judul' => 'SK Pengangkatan Baru',
'nomor' => 'SK/NEW/2025',
'kategori_id' => $this->kategori->id,
'tanggal_dokumen' => '2025-06-20',
'deskripsi' => 'Deskripsi dokumen test',
];
$response = $this->actingAs($this->user)
->post('/dokumen', $data);
$response->assertRedirect(route('dokumen.index'));
$response->assertSessionHas('success');
$this->assertDatabaseHas('dokumens', [
'judul' => 'SK Pengangkatan Baru',
'nomor' => 'SK/NEW/2025',
]);
}
public function test_dokumen_validation_fails_without_required_fields(): void
{
$response = $this->actingAs($this->user)
->post('/dokumen', []);
$response->assertSessionHasErrors(['judul', 'nomor', 'kategori_id', 'tanggal_dokumen']);
}
public function test_dokumen_nomor_must_be_unique(): void
{
Dokumen::factory()->create(['nomor' => 'SK/001/2025']);
$response = $this->actingAs($this->user)
->post('/dokumen', [
'judul' => 'Dokumen Baru',
'nomor' => 'SK/001/2025', // duplicate
'kategori_id' => $this->kategori->id,
'tanggal_dokumen' => '2025-06-20',
]);
$response->assertSessionHasErrors(['nomor']);
}
public function test_user_can_view_dokumen_detail(): void
{
$dokumen = Dokumen::factory()->create();
$response = $this->actingAs($this->user)
->get("/dokumen/{$dokumen->id}");
$response->assertStatus(200);
$response->assertViewIs('dokumen.show');
$response->assertSee($dokumen->judul);
}
public function test_user_can_update_dokumen(): void
{
$dokumen = Dokumen::factory()->create([
'user_id' => $this->user->id,
]);
$response = $this->actingAs($this->user)
->put("/dokumen/{$dokumen->id}", [
'judul' => 'Judul Updated',
'nomor' => $dokumen->nomor,
'kategori_id' => $this->kategori->id,
'tanggal_dokumen' => '2025-06-21',
]);
$response->assertRedirect(route('dokumen.index'));
$this->assertDatabaseHas('dokumens', [
'id' => $dokumen->id,
'judul' => 'Judul Updated',
]);
}
public function test_user_can_delete_dokumen(): void
{
$dokumen = Dokumen::factory()->create([
'user_id' => $this->user->id,
]);
$response = $this->actingAs($this->user)
->delete("/dokumen/{$dokumen->id}");
$response->assertRedirect(route('dokumen.index'));
$this->assertSoftDeleted('dokumens', ['id' => $dokumen->id]);
}
}1.5 API Test â
php artisan make:test Api/DokumenApiTestEdit tests/Feature/Api/DokumenApiTest.php:
<?php
namespace Tests\Feature\Api;
use App\Models\Dokumen;
use App\Models\Kategori;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
class DokumenApiTest extends TestCase
{
use RefreshDatabase;
private User $user;
private Kategori $kategori;
protected function setUp(): void
{
parent::setUp();
$this->user = User::factory()->create();
$this->kategori = Kategori::factory()->create();
}
public function test_unauthenticated_user_cannot_access_api(): void
{
$response = $this->getJson('/api/dokumen');
$response->assertStatus(401);
}
public function test_authenticated_user_can_list_dokumens(): void
{
Sanctum::actingAs($this->user);
Dokumen::factory()->count(5)->create();
$response = $this->getJson('/api/dokumen');
$response->assertStatus(200)
->assertJsonStructure([
'success',
'message',
'data' => [
'data' => [
'*' => ['id', 'judul', 'nomor', 'status']
]
],
]);
}
public function test_user_can_create_dokumen_via_api(): void
{
Sanctum::actingAs($this->user);
$response = $this->postJson('/api/dokumen', [
'judul' => 'API Created Dokumen',
'nomor' => 'API/001/2025',
'kategori_id' => $this->kategori->id,
'tanggal_dokumen' => '2025-06-20',
]);
$response->assertStatus(201)
->assertJson([
'success' => true,
'data' => [
'judul' => 'API Created Dokumen',
],
]);
}
public function test_api_validation_returns_422(): void
{
Sanctum::actingAs($this->user);
$response = $this->postJson('/api/dokumen', []);
$response->assertStatus(422)
->assertJsonValidationErrors(['judul', 'nomor', 'kategori_id', 'tanggal_dokumen']);
}
public function test_user_can_get_single_dokumen(): void
{
Sanctum::actingAs($this->user);
$dokumen = Dokumen::factory()->create();
$response = $this->getJson("/api/dokumen/{$dokumen->id}");
$response->assertStatus(200)
->assertJson([
'success' => true,
'data' => [
'id' => $dokumen->id,
],
]);
}
}1.6 Menjalankan Test â
# Jalankan semua test
php artisan test
# Jalankan dengan verbose
php artisan test -v
# Jalankan test spesifik
php artisan test --filter=DokumenFeatureTest
# Jalankan method spesifik
php artisan test --filter=test_user_can_create_dokumen
# Dengan coverage (perlu Xdebug/PCOV)
php artisan test --coverageSESI 2: Optimasi Performa â
2.1 Route Caching â
# Cache routes
php artisan route:cache
# Clear cache
php artisan route:clear2.2 Config Caching â
# Cache config
php artisan config:cache
# Clear cache
php artisan config:clear2.3 View Caching â
# Cache compiled views
php artisan view:cache
# Clear cache
php artisan view:clear2.4 Event & Optimization â
# Cache events
php artisan event:cache
# Optimize all
php artisan optimize
# Clear all cache
php artisan optimize:clear2.5 Query Optimization â
Eager Loading:
// [X] N+1 Problem
$dokumens = Dokumen::all();
foreach ($dokumens as $dok) {
echo $dok->kategori->nama; // Query setiap iterasi
}
// [OK] Eager Loading
$dokumens = Dokumen::with('kategori')->get();
foreach ($dokumens as $dok) {
echo $dok->kategori->nama; // Tidak ada query tambahan
}Select Kolom Spesifik:
// [X] Select semua kolom
$users = User::all();
// [OK] Select yang diperlukan
$users = User::select('id', 'name', 'email')->get();Chunk untuk Data Besar:
// [OK] Chunk processing
Dokumen::chunk(100, function ($dokumens) {
foreach ($dokumens as $dokumen) {
// Process
}
});2.6 Database Indexing â
// Tambah index di migration
Schema::table('dokumens', function (Blueprint $table) {
$table->index('status');
$table->index('tanggal_dokumen');
$table->index(['kategori_id', 'status']);
});2.7 Caching Data â
use Illuminate\Support\Facades\Cache;
// Cache query result
$kategoris = Cache::remember('kategoris_aktif', 3600, function () {
return Kategori::aktif()->get();
});
// Cache forever
$settings = Cache::rememberForever('app_settings', function () {
return Setting::all()->pluck('value', 'key');
});
// Clear cache
Cache::forget('kategoris_aktif');
// Tags (Redis/Memcached)
Cache::tags(['dokumen'])->put('list', $dokumens, 3600);
Cache::tags(['dokumen'])->flush();SESI 3: Keamanan â
3.1 Environment Variables â
Jangan commit .env!
# .gitignore
.env
.env.backup
.env.productionGunakan variabel environment:
// Akses config, bukan env langsung
$appName = config('app.name');
$dbHost = config('database.connections.mysql.host');3.2 HTTPS â
Force HTTPS di production:
Edit app/Providers/AppServiceProvider.php:
use Illuminate\Support\Facades\URL;
public function boot(): void
{
if ($this->app->environment('production')) {
URL::forceScheme('https');
}
}3.3 SQL Injection Prevention â
// [OK] Eloquent/Query Builder (otomatis escaped)
Dokumen::where('status', $request->status)->get();
// [OK] Parameter binding
DB::select('SELECT * FROM dokumens WHERE status = ?', [$status]);
// [X] Raw query tanpa binding
DB::select("SELECT * FROM dokumens WHERE status = '$status'"); // BAHAYA!3.4 XSS Prevention â
// [OK] Blade otomatis escape
{{ $dokumen->judul }}
// [X] Unescaped (hati-hati)
{!! $dokumen->deskripsi !!}3.5 Mass Assignment Protection â
// Di Model
protected $fillable = ['judul', 'nomor', 'deskripsi'];
// atau
protected $guarded = ['id', 'created_at', 'updated_at'];3.6 Rate Limiting â
Di routes/api.php:
Route::middleware('throttle:60,1')->group(function () {
Route::apiResource('dokumen', DokumenController::class)
->parameters(['dokumen' => 'dokumen']); // Pastikan parameter route benar
});Custom rate limiter di app/Providers/AppServiceProvider.php:
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
public function boot(): void
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
RateLimiter::for('login', function (Request $request) {
return Limit::perMinute(5)->by($request->ip());
});
}SESI 4: Deployment â
4.1 Checklist Pre-Deployment â
- [ ] Semua test passing
- [ ]
.env.exampleterupdate - [ ] Tidak ada
dd()ataudump()di kode - [ ] Error handling sudah proper
- [ ] Logging sudah dikonfigurasi
- [ ] Assets sudah di-compile (
npm run build)
4.2 Server Requirements â
- PHP >= 8.2
- BCMath PHP Extension
- Ctype PHP Extension
- Fileinfo PHP Extension
- JSON PHP Extension
- Mbstring PHP Extension
- OpenSSL PHP Extension
- PDO PHP Extension
- Tokenizer PHP Extension
- XML PHP Extension
4.3 Deployment Manual â
1. Clone repository:
cd /var/www
git clone https://github.com/username/sidoku.git
cd sidoku2. Install dependencies:
composer install --optimize-autoloader --no-dev
npm install
npm run build3. Setup environment:
cp .env.example .env
php artisan key:generate4. Edit .env untuk production:
APP_ENV=production
APP_DEBUG=false
APP_URL=https://sidoku.example.com
LOG_CHANNEL=daily
LOG_LEVEL=error
DB_CONNECTION=mysql
DB_HOST=localhost
DB_DATABASE=sidoku_prod
DB_USERNAME=sidoku_user
DB_PASSWORD=secure_password_here
CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis5. Jalankan migration dan optimasi:
php artisan migrate --force
php artisan db:seed --force # jika perlu
php artisan storage:link
php artisan optimize6. Set permissions:
sudo chown -R www-data:www-data storage bootstrap/cache
sudo chmod -R 775 storage bootstrap/cache4.4 Nginx Configuration â
server {
listen 80;
server_name sidoku.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name sidoku.example.com;
root /var/www/sidoku/public;
ssl_certificate /etc/letsencrypt/live/sidoku.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/sidoku.example.com/privkey.pem;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
index index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
error_page 404 /index.php;
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
}4.5 Supervisor untuk Queue Worker â
Install supervisor:
sudo apt-get install supervisorBuat config /etc/supervisor/conf.d/sidoku-worker.conf:
[program:sidoku-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/sidoku/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/sidoku/storage/logs/worker.log
stopwaitsecs=3600sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start sidoku-worker:*4.6 Deployment Script â
Buat deploy.sh:
#!/bin/bash
echo "Starting deployment..."
# Pull latest code
git pull origin main
# Install dependencies
composer install --optimize-autoloader --no-dev
# Build assets
npm ci
npm run build
# Clear and cache
php artisan optimize:clear
php artisan migrate --force
php artisan optimize
# Restart queue workers
php artisan queue:restart
echo "Deployment completed!"4.7 CI/CD dengan GitHub Actions â
Buat .github/workflows/deploy.yml:
name: Deploy
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_DATABASE: testing
MYSQL_ROOT_PASSWORD: password
ports:
- 3306:3306
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.2
extensions: mbstring, mysql, zip
coverage: none
- name: Install Composer dependencies
run: composer install --prefer-dist --no-progress
- name: Copy .env
run: cp .env.example .env
- name: Generate key
run: php artisan key:generate
- name: Run tests
env:
DB_CONNECTION: mysql
DB_HOST: 127.0.0.1
DB_DATABASE: testing
DB_USERNAME: root
DB_PASSWORD: password
run: php artisan test
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- name: Deploy to server
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /var/www/sidoku
./deploy.shSESI 5: Monitoring dan Maintenance â
5.1 Logging â
use Illuminate\Support\Facades\Log;
// Level logging
Log::emergency('Sistem down!');
Log::alert('Butuh tindakan segera');
Log::critical('Error kritis');
Log::error('Error terjadi');
Log::warning('Peringatan');
Log::notice('Perlu diperhatikan');
Log::info('Informasi');
Log::debug('Debug info');
// Dengan context
Log::info('Dokumen dibuat', [
'dokumen_id' => $dokumen->id,
'user_id' => auth()->id(),
]);Konfigurasi di .env:
LOG_CHANNEL=daily
LOG_LEVEL=warning5.2 Error Tracking â
Untuk production, gunakan service seperti:
- Sentry
- Bugsnag
- Laravel Telescope (development)
Setup Sentry:
composer require sentry/sentry-laravelSENTRY_LARAVEL_DSN=https://xxxxx@sentry.io/project5.3 Health Check â
Buat endpoint health check:
Route::get('/health', function () {
try {
DB::connection()->getPdo();
Cache::get('health_check');
return response()->json([
'status' => 'healthy',
'timestamp' => now()->toIso8601String(),
]);
} catch (\Exception $e) {
return response()->json([
'status' => 'unhealthy',
'error' => $e->getMessage(),
], 500);
}
});5.4 Backup Database â
Menggunakan spatie/laravel-backup:
composer require spatie/laravel-backup
php artisan vendor:publish --provider="Spatie\Backup\BackupServiceProvider"# Backup
php artisan backup:run
# Backup hanya database
php artisan backup:run --only-db
# Jadwalkan di Console/Kernel.php
$schedule->command('backup:run')->daily()->at('02:00');5.5 Maintenance Mode â
# Aktifkan maintenance mode
php artisan down
# Dengan pesan custom
php artisan down --message="Sedang dalam perbaikan"
# Allow IP tertentu
php artisan down --allow=127.0.0.1 --allow=192.168.1.100
# Dengan secret bypass
php artisan down --secret="bypass-token"
# Akses: https://sidoku.example.com/bypass-token
# Nonaktifkan maintenance
php artisan upPRAKTIKUM FINAL: Deploy SiDoku â
Langkah 1: Finalisasi Kode â
# Jalankan semua test
php artisan test
# Build assets
npm run build
# Commit semua perubahan
git add .
git commit -m "Ready for production"
git push origin mainLangkah 2: Setup Production Environment â
- Siapkan server dengan PHP 8.2, MySQL, Nginx
- Clone repository
- Setup .env production
- Jalankan migration
- Konfigurasi web server
Langkah 3: Monitoring â
- Setup logging
- Konfigurasi error tracking
- Setup backup otomatis
â Checkpoint Hari 5 â
Pastikan Anda sudah bisa:
- Menulis dan menjalankan unit test dan feature test
- Mengoptimasi aplikasi untuk production
- Mengkonfigurasi keamanan aplikasi
- Melakukan deployment ke server
- Setup monitoring dan maintenance
ðŸŽⰠSelamat! â
Anda telah menyelesaikan 5-Day Laravel Bootcamp!
Skill yang Dikuasai: â
- â PHP Fundamentals dan OOP
- â Laravel Installation dan Structure
- â Routing, Controllers, dan Views
- â Blade Templating
- â Database dan Eloquent ORM
- â Form Handling dan Validation
- â Authentication dan Authorization
- â File Upload
- â REST API Development
- â Testing
- â Deployment
Project SiDoku: â
Anda telah membangun Sistem Pengelolaan Dokumen dengan fitur:
- CRUD Dokumen dan Kategori
- Authentication
- File Upload
- REST API
- Responsive UI dengan Tailwind CSS
Next Steps: â
- Eksplorasi Laravel Livewire untuk interaktivitas
- Pelajari Laravel Queues untuk background jobs
- Implementasi Real-time dengan Laravel Echo
- Pelajari Docker untuk containerization
- Eksplorasi Laravel Cloud untuk deployment