nasirkhan/laravel-starter
Laravel 13 modular starter with separated frontend/backend. Includes auth & authorization, user/role management, admin backend, backups, log viewer, and custom artisan commands (install, update, module builder). Use as a base to build reusable modules.
Created: February 4, 2026
Last Updated: February 4, 2026
This application uses PHPUnit as its primary testing framework with additional testing tools:
tests/Unit/)Test individual classes and methods in isolation.
// Example: tests/Unit/MenuTest.php
public function test_menu_has_correct_table_name(): void
{
$menu = new Menu();
$this->assertEquals('menus', $menu->getTable());
}
tests/Feature/)Test application features with database interactions.
// Example: tests/Feature/Auth/AuthenticationTest.php
public function test_users_can_authenticate(): void
{
$user = User::factory()->create();
$this->post('/login', [
'email' => $user->email,
'password' => 'password',
]);
$this->assertAuthenticated();
}
tests/Browser/)Test full user workflows in a real browser.
// Example: tests/Browser/LoginTest.php
public function test_user_can_login(): void
{
$this->browse(function (Browser $browser) use ($user) {
$browser->visit('/login')
->type('email', $user->email)
->type('password', 'password')
->press('Log in')
->assertPathIs('/dashboard');
});
}
tests/Feature/Api/)Test REST API endpoints with proper authentication and validation.
// Example: tests/Feature/Api/ExampleApiTest.php
public function test_api_requires_authentication(): void
{
$response = $this->getJson('/api/user');
$response->assertStatus(401);
}
# PHPUnit
php artisan test
# Pest
./vendor/bin/pest
# With coverage
php artisan test --coverage
# Parallel execution
php artisan test --parallel
# Unit tests only
php artisan test --testsuite=Unit
# Feature tests only
php artisan test --testsuite=Feature
# Browser tests
php artisan dusk
# Specific browser test
php artisan dusk tests/Browser/LoginTest.php
# By filter
php artisan test --filter=AuthenticationTest
# By method name
php artisan test --filter=test_user_can_login
# Single file
php artisan test tests/Feature/Auth/AuthenticationTest.php
# Automatically determine processes
php artisan test --parallel
# Specify number of processes
php artisan test --parallel --processes=4
# Feature test
php artisan make:test UserTest
# Unit test
php artisan make:test UserTest --unit
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class UserTest extends TestCase
{
use RefreshDatabase;
public function test_user_can_be_created(): void
{
$userData = [
'name' => 'John Doe',
'email' => 'john@example.com',
'password' => bcrypt('password'),
];
$user = User::create($userData);
$this->assertDatabaseHas('users', [
'email' => 'john@example.com',
]);
$this->assertEquals('John Doe', $user->name);
}
}
Dusk is already installed and configured. ChromeDriver is automatically downloaded.
php artisan dusk:make RegistrationTest
<?php
namespace Tests\Browser;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use Tests\Builders\UserBuilder;
class RegistrationTest extends DuskTestCase
{
public function test_user_can_register(): void
{
$this->browse(function (Browser $browser) {
$browser->visit('/register')
->type('name', 'John Doe')
->type('email', 'john@example.com')
->type('password', 'password123')
->type('password_confirmation', 'password123')
->press('Create account')
->waitForLocation('/dashboard', 10)
->assertPathIs('/dashboard')
->assertAuthenticated();
});
}
}
# All browser tests
php artisan dusk
# Specific test
php artisan dusk tests/Browser/LoginTest.php
# With specific browser
php artisan dusk --without-tty
# In headless mode
php artisan dusk --headless
// By ID
$browser->click('#submit-button');
// By class
$browser->type('.email-input', 'test@example.com');
// By name
$browser->select('country', 'US');
// Dusk selector
$browser->click('[@submit-button](https://github.com/submit-button)');
// XPath
$browser->driver->findElement(
WebDriverBy::xpath('//button[[@type](https://github.com/type)="submit"]')
)->click();
// Wait for element
$browser->waitFor('.alert');
// Wait for text
$browser->waitForText('Success');
// Wait for location
$browser->waitForLocation('/dashboard');
// Wait for reload
$browser->waitForReload();
// Custom wait
$browser->waitUsing(10, 100, function () use ($browser) {
return $browser->driver->getCurrentURL() === 'http://example.com/dashboard';
});
<?php
namespace Tests\Feature\Api;
use Tests\TestCase;
use Tests\Builders\UserBuilder;
use Illuminate\Foundation\Testing\RefreshDatabase;
class PostApiTest extends TestCase
{
use RefreshDatabase;
public function test_api_returns_posts_list(): void
{
$user = UserBuilder::make()->create();
$token = $user->createToken('test')->plainTextToken;
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $token,
'Accept' => 'application/json',
])->getJson('/api/posts');
$response
->assertStatus(200)
->assertJsonStructure([
'data' => [
'*' => ['id', 'title', 'content', 'created_at']
],
'meta' => ['current_page', 'total'],
'links' => ['first', 'last', 'prev', 'next'],
]);
}
public function test_api_validates_post_creation(): void
{
$user = UserBuilder::make()->asAdmin()->create();
$token = $user->createToken('test')->plainTextToken;
$response = $this->withHeaders([
'Authorization' => 'Bearer ' . $token,
])->postJson('/api/posts', [
'title' => '', // Empty title should fail
]);
$response
->assertStatus(422)
->assertJsonValidationErrors(['title']);
}
}
application/jsonThe Builder pattern provides a fluent, readable way to create test data.
use Tests\Builders\UserBuilder;
// Simple user
$user = UserBuilder::make()->create();
// Admin user
$admin = UserBuilder::make()
->asAdmin()
->verified()
->create();
// Custom user
$user = UserBuilder::make()
->withName('John Doe')
->withEmail('john@example.com')
->withRole('manager')
->withPermission('edit posts')
->active()
->verified()
->create();
// Multiple users
$users = UserBuilder::make()
->withRole('user')
->count(10);
// Make without persisting
$user = UserBuilder::make()
->withEmail('test@example.com')
->make();
<?php
namespace Tests\Builders;
use App\Models\Post;
class PostBuilder
{
private array $attributes = [];
public static function make(): self
{
return new self();
}
public function withTitle(string $title): self
{
$this->attributes['title'] = $title;
return $this;
}
public function withContent(string $content): self
{
$this->attributes['content'] = $content;
return $this;
}
public function published(): self
{
$this->attributes['published_at'] = now();
$this->attributes['status'] = 1;
return $this;
}
public function draft(): self
{
$this->attributes['published_at'] = null;
$this->attributes['status'] = 0;
return $this;
}
public function create(): Post
{
return Post::factory()->create($this->attributes);
}
public function make(): Post
{
return Post::factory()->make($this->attributes);
}
}
Mutation testing modifies your source code in small ways (mutations) and runs your tests to see if they catch the changes. This tests the quality of your tests themselves.
# Run mutation testing
./vendor/bin/infection
# With specific mutators
./vendor/bin/infection --mutators=[@default](https://github.com/default)
# Show mutations
./vendor/bin/infection --show-mutations
# Generate report
./vendor/bin/infection --logger-html=infection.html
125 mutations were generated:
75 mutants were killed
25 mutants survived
20 mutants were not covered by tests
5 errors were encountered
Metrics:
Mutation Score Indicator (MSI): 60%
Mutation Code Coverage: 84%
Covered Code MSI: 75%
Follow AAA Pattern
// Arrange - Set up test data
$user = User::factory()->create();
// Act - Perform the action
$response = $this->post('/login', [...]);
// Assert - Verify the result
$this->assertAuthenticated();
Use Descriptive Test Names
// Good
test_user_can_login_with_valid_credentials()
// Bad
test_login()
One Assertion Focus Per Test
// Good - focused test
public function test_user_name_is_required(): void
{
$response = $this->post('/users', ['name' => '']);
$response->assertSessionHasErrors('name');
}
Use Database Transactions
use RefreshDatabase; // Rolls back after each test
Avoid Test Interdependencies
describe()# HTML report (opens in browser)
php artisan test --coverage --coverage-html=coverage
# Text report
php artisan test --coverage --coverage-text
# XML report (for CI)
php artisan test --coverage --coverage-clover=coverage.xml
# After generating HTML report
open coverage/index.html
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.3
extensions: mbstring, pdo, pdo_sqlite
coverage: xdebug
- name: Install Dependencies
run: composer install --no-interaction --prefer-dist
- name: Copy Environment
run: cp .env.example .env
- name: Generate Key
run: php artisan key:generate
- name: Run Tests
run: php artisan test --coverage --min=70
- name: Run Pest
run: ./vendor/bin/pest
- name: Run Mutation Tests
run: ./vendor/bin/infection --min-msi=70 --min-covered-msi=80
# Run all tests
php artisan test
# Run with coverage
php artisan test --coverage --min=70
# Run Pest tests
./vendor/bin/pest
# Run browser tests
php artisan dusk
# Run mutation tests
./vendor/bin/infection
# Run specific test
php artisan test --filter=UserTest
# Run in parallel
php artisan test --parallel
# Generate coverage HTML
php artisan test --coverage-html=coverage
RefreshDatabase - Rolls back database after each testDatabaseMigrations - Runs migrations before each testWithFaker - Provides Faker instanceWithoutMiddleware - Disables middlewareWithoutEvents - Disables events// HTTP
$response->assertStatus(200);
$response->assertRedirect('/dashboard');
$response->assertJson(['key' => 'value']);
// Database
$this->assertDatabaseHas('users', ['email' => 'test@example.com']);
$this->assertDatabaseMissing('users', ['email' => 'deleted@example.com']);
// Authentication
$this->assertAuthenticated();
$this->assertGuest();
// Expectations (Pest)
expect($value)->toBe(10);
expect($user)->toBeInstanceOf(User::class);
expect($collection)->toHaveCount(5);
Last Updated: February 4, 2026
How can I help you explore Laravel packages today?