Laravel Feature and Unit Tests: A Practical Guide with PHPUnit
Write comprehensive tests for your Laravel applications.
Laravel Tinker is a powerful REPL (Read-Eval-Print Loop) tool that allows you to interact with your entire Laravel application from the command line. Think of it as a interactive playground where you can test code, debug issues, and manipulate your database without creating temporary routes or controllers.
Why Tinker is a Game-Changer:
# Start Tinker session
php artisan tinker
# Start Tinker with specific environment
php artisan tinker --env=local
# Start Tinker in production (be careful!)
php artisan tinker --env=production
# Exit Tinker
>>> exit
>>> quit
>>> Ctrl + D
$ php artisan tinker
Psy Shell v0.11.8 (PHP 8.1.0 — cli) by Justin Hileman
>>>
>>> 2 + 2
= 4
>>> $name = "TeachyLeaf"
= "TeachyLeaf"
>>> echo "Hello, " . $name
Hello, null
= null
>>> "Hello, " . $name
= "Hello, TeachyLeaf"
>>> collect([1, 2, 3])->map(fn($n) => $n * 2)
= Illuminate\Support\Collection {#4682
all: [
2,
4,
6,
],
}
>>> app()->environment()
= "local"
>>> config('app.name')
= "Laravel"
>>> now()
= Illuminate\Support\Carbon @1694567890 {#4685
date: 2024-09-13 10:18:10.0 UTC (+00:00),
}
>>> storage_path('app/public')
= "/path/to/your/project/storage/app/public"
>>> url('/posts')
= "http://localhost:8000/posts"
>>> route('posts.index')
= "http://localhost:8000/posts"
Creating Records
>>> use App\Models\User;
// Create a user
>>> $user = User::create([
... 'name' => 'John Doe',
... 'email' => 'john@teachyleaf.com',
... 'password' => bcrypt('password123')
... ]);
= App\Models\User {#4689
name: "John Doe",
email: "john@teachyleaf.com",
updated_at: "2024-09-13 10:20:45",
created_at: "2024-09-13 10:20:45",
id: 1,
}
// Alternative creation method
>>> $user = new User;
>>> $user->name = 'Jane Smith';
>>> $user->email = 'jane@teachyleaf.com';
>>> $user->password = bcrypt('password123');
>>> $user->save();
= true
Reading Records
// Get all users
>>> User::all()
= Illuminate\Database\Eloquent\Collection {#4691
all: [
App\Models\User {#4692
id: 1,
name: "John Doe",
email: "john@teachyleaf.com",
email_verified_at: null,
#password: "$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi",
#remember_token: "aBcDeFgHiJkL",
created_at: "2024-09-13 10:20:45",
updated_at: "2024-09-13 10:20:45",
},
],
}
// Find specific user
>>> User::find(1)
= App\Models\User {#4693
id: 1,
name: "John Doe",
email: "john@teachyleaf.com",
...
}
// Find or fail
>>> User::findOrFail(999)
Illuminate\Database\Eloquent\ModelNotFoundException with message 'No query results for model [App\Models\User] 999'
// Get first user
>>> User::first()
= App\Models\User {#4694
id: 1,
name: "John Doe",
...
}
// Count records
>>> User::count()
= 1
Updating Records
// Update a user
>>> $user = User::find(1)
>>> $user->name = 'John Updated'
>>> $user->save()
= true
// Mass update
>>> User::where('id', 1)->update(['name' => 'John Mass Updated'])
= 1
// Increment values
>>> User::where('id', 1)->increment('login_count')
= 1
Deleting Records
// Delete a user
>>> $user = User::find(1)
>>> $user->delete()
= true
// Force delete (if using soft deletes)
>>> $user->forceDelete()
= true
// Mass delete
>>> User::where('email', 'like', '%test%')->delete()
= 0
>>> use App\Models\Post;
// Multiple conditions
>>> Post::where('is_published', true)
... ->where('views', '>', 100)
... ->get()
// OR conditions
>>> Post::where('is_published', true)
... ->orWhere('user_id', 1)
... ->get()
// Date queries
>>> Post::whereDate('created_at', '2024-09-13')->get()
>>> Post::whereBetween('created_at', ['2024-09-01', '2024-09-30'])->get()
>>> Post::whereYear('created_at', 2024)->get()
// Relationship queries
>>> Post::whereHas('comments', function($query) {
... $query->where('is_approved', true);
... })->get()
// Load relationships to avoid N+1 queries
>>> $posts = Post::with('user', 'tags', 'comments')->get()
// Check loaded relationships
>>> $posts->first()->user
>>> $posts->first()->tags
>>> $posts->first()->comments
>>> $user = User::first()
>>> $user->posts
= Illuminate\Database\Eloquent\Collection {#4701
all: [],
}
>>> $user->posts()->create([
... 'title' => 'My First Post',
... 'content' => 'This is the content of my first post.',
... 'is_published' => true
... ])
= App\Models\Post {#4702
title: "My First Post",
content: "This is the content of my first post.",
is_published: true,
user_id: 1,
updated_at: "2024-09-13 10:25:30",
created_at: "2024-09-13 10:25:30",
id: 1,
}
>>> $post = Post::first()
>>> $post->user
= App\Models\User {#4703
id: 1,
name: "John Updated",
email: "john@teachyleaf.com",
...
}
>>> use Illuminate\Support\Facades\Mail;
>>> use App\Mail\WelcomeEmail;
// Test email without actually sending
>>> Mail::fake()
// Check if email would be sent
>>> Mail::assertNothingSent()
// Test email sending
>>> $user = User::first()
>>> Mail::to($user)->send(new WelcomeEmail($user))
>>> Mail::assertSent(WelcomeEmail::class)
>>> use Illuminate\Support\Facades\Storage;
// Fake storage for testing
>>> Storage::fake('local')
// Test file operations
>>> Storage::put('test.txt', 'Hello Tinker!')
= true
>>> Storage::get('test.txt')
= "Hello Tinker!"
>>> Storage::exists('test.txt')
= true
>>> use Illuminate\Support\Facades\Queue;
>>> use App\Jobs\ProcessPodcast;
// Fake queue for testing
>>> Queue::fake()
// Dispatch a job
>>> ProcessPodcast::dispatch($podcast)
// Assert job was pushed
>>> Queue::assertPushed(ProcessPodcast::class)
>>> $users = User::all()
>>> $users->pluck('name')
= Illuminate\Support\Collection {#4710
all: [
"John Updated",
"Jane Smith",
],
}
>>> $users->where('id', 1)->first()
>>> $users->sortBy('created_at')
>>> $users->filter(fn($user) => strlen($user->name) > 5)
// Transform collection
>>> $users->map(fn($user) => [
... 'id' => $user->id,
... 'name' => strtoupper($user->name),
... 'email' => $user->email
... ])
>>> $numbers = collect([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
>>> $numbers->sum()
= 55
>>> $numbers->avg()
= 5.5
>>> $numbers->chunk(3)
>>> $numbers->groupBy(fn($n) => $n % 2 == 0 ? 'even' : 'odd')
// Working with key-value pairs
>>> $data = collect(['name' => 'John', 'age' => 30, 'city' => 'New York'])
>>> $data->only(['name', 'age'])
>>> $data->except('city')
>>> use App\Services\UserService;
>>> $userService = app(UserService::class)
>>> $user = $userService->createUser([
... 'name' => 'Test User',
... 'email' => 'test@teachyleaf.com',
... 'password' => 'secret'
... ])
>>> use Illuminate\Http\Request;
>>> use App\Http\Requests\StorePostRequest;
// Test validation rules
>>> $request = new Request([
... 'title' => 'Test Post',
... 'content' => 'This is content'
... ])
>>> $formRequest = StorePostRequest::createFrom($request)
>>> $formRequest->setContainer(app())
>>> $formRequest->validateResolved()
>>> use Illuminate\Support\Facades\Auth;
// Login a user
>>> $user = User::first()
>>> Auth::login($user)
>>> Auth::check()
= true
>>> Auth::user()
= App\Models\User {#4715
id: 1,
name: "John Updated",
...
}
// Logout
>>> Auth::logout()
>>> Auth::check()
= false
>>> $result = function() {
... $users = User::all();
... return $users->map(function($user) {
... return [
... 'id' => $user->id,
... 'name' => $user->name,
... 'email' => $user->email
... ];
... });
... }
>>> $result()
// Create and test a custom function
>>> function calculateReadingTime($content) {
... $wordCount = str_word_count(strip_tags($content));
... return ceil($wordCount / 200);
... }
>>> calculateReadingTime("This is a sample blog post content.")
= 1
// Test a class method
>>> class Calculator {
... public function add($a, $b) {
... return $a + $b;
... }
... }
>>> $calc = new Calculator
>>> $calc->add(5, 3)
= 8
// Create multiple test records quickly
>>> foreach(range(1, 10) as $i) {
... User::create([
... 'name' => "Test User $i",
... 'email' => "user$i@teachyleaf.com",
... 'password' => bcrypt('password')
... ]);
... }
// Let's say you have an issue with a specific user's posts
>>> $user = User::where('email', 'problem@example.com')->first()
>>> $user->posts->load('comments', 'tags')
>>> $user->posts->each(function($post) {
... echo "Post: {$post->title}\n";
... echo "Comments: {$post->comments->count()}\n";
... echo "Tags: {$post->tags->pluck('name')->implode(', ')}\n";
... echo "---\n";
... })
>>> use Illuminate\Http\Request;
>>> use App\Http\Controllers\Api\PostController;
>>> $controller = app(PostController::class)
>>> $request = Request::create('/api/posts', 'GET')
>>> $response = $controller->index($request)
>>> $response->getData()
// Clean up test data
>>> User::where('email', 'like', '%test%')->delete()
// Update multiple records
>>> Post::where('is_published', false)
... ->where('created_at', '<', now()->subMonth())
... ->update(['status' => 'archived'])
// Check database size/statistics
>>> User::count()
>>> Post::count()
>>> Comment::count()
>>> Post::groupBy('is_published')->selectRaw('is_published, count(*) as count')->get()
>>> alias User = App\Models\User
>>> alias Post = App\Models\Post
>>> User::first()
Create a .psysh.php file in your project root:
<?php
// .psysh.php
return [
'aliases' => [
'User' => App\Models\User::class,
'Post' => App\Models\Post::class,
'Comment' => App\Models\Comment::class,
],
'defaultIncludes' => [
__DIR__ . '/vendor/autoload.php',
],
];
// Set up application context
>>> config(['app.name' => 'TeachyLeaf Tinker Test'])
>>> app()->bind('current_user', fn() => User::first())
// Instead of just echoing, use these techniques:
>>> $user = User::with('posts.tags')->first()
>>> dump($user->toArray()) // See all attributes
>>> $user->getRelations() // See loaded relationships
>>> get_class_methods($user) // See available methods
// Test a new feature idea quickly
>>> $idea = "What if we add a featured posts section?"
>>> $featuredPosts = Post::where('is_featured', true)
... ->with('user')
... ->orderBy('featured_at', 'desc')
... ->limit(5)
... ->get()
>>> $featuredPosts->count()
// Test a data migration before running it
>>> $usersToUpdate = User::whereNull('timezone')->get()
>>> $usersToUpdate->count()
>>> $usersToUpdate->each(function($user) {
... $user->timezone = 'UTC';
... // Don't save - just testing
... })
// Test query performance
>>> $start = microtime(true)
>>> $posts = Post::with(['user', 'comments', 'tags'])->get()
>>> $end = microtime(true)
>>> echo "Query took: " . ($end - $start) . " seconds"
# Never run these in production without caution:
php artisan tinker --env=production
# Better approach for production:
# 1. Create a specific testing route
# 2. Use database transactions
# 3. Have backups ready
// Always use transactions for destructive operations
>>> DB::beginTransaction();
>>> try {
... // Your risky operations here
... DB::commit();
... } catch (Exception $e) {
... DB::rollback();
... echo "Error: " . $e->getMessage();
... }
Tinker is a REPL tool that allows interactive interaction with Laravel applications. It's useful for testing code snippets, debugging, database queries, and learning Laravel features without creating full application structures.
You can load models with their relationships using with(), then access the related data directly. For example: $user = User::with('posts.comments')->first() then $user->posts->first()->comments.
They're identical - both return all records. all() is a static method that calls get() internally. The choice is mostly stylistic preference.
Use Mail::fake() to mock the mail system, then use Mail::assertSent() to verify emails would be sent without actually delivering them.
Always backup your database first, use database transactions for destructive operations, and avoid running Tinker on production unless absolutely necessary. Consider using specific testing routes instead.
You can use exit, quit, or Ctrl + D to exit a Tinker session.
# Basic Tinker
php artisan tinker
php artisan tinker --execute="echo 'Hello'"
# Common Tinker Operations
>>> User::all() # Get all users
>>> User::find(1) # Find user by ID
>>> User::where('active', true)->get() # Conditional query
>>> $user->posts # Access relationship
>>> Post::with('user')->get() # Eager loading
>>> DB::table('users')->count() # Raw query
>>> exit # Exit Tinker
// Solution: Import the class
>>> use App\Models\User
>>> User::first()
// Solution: Use fillable attributes or forceFill
>>> $user = new User
>>> $user->forceFill([...])->save()
# Solution: Clear compiled classes
php artisan clear-compiled
composer dump-autoload
Now you're equipped with the power of Laravel Tinker to test, debug, and explore your application with incredible speed and efficiency! In our next post, we'll dive into Laravel Eloquent Relationships: Demystifying One-to-Many and Many-to-One to master database relationships in Laravel.