- Knowledge
- technology
- OOP
- Tips
- Programming
- Tips
- Tutorial
- SEO
- Ranking
- Knowledge
- Special Day
- Seo
- Bug
- Data science
- Seo
- artificial intelligence
- Machine Learning
- Robotics
- happyNewYear2021
- newYearEve
- 2021
- Automation
- Smart Home
- Career
- Best Practices
- Git
- Logging
- Web Fundamentals
- DNS
- HTTPS
- Performance
- AI Tools
- ChatGPT
- Claude
- Gemini
- Laravel
- Eloquent
- MySQL
- HTTPS
- TLS
- Web Security
- Certificates
- Developer Life
- Debugging
- Docker
- DevOps
- Transactions
- Queues
- LLMs
- AI
- AI Coding
- Developer Tools
- React Native
- Expo
- Kate PMS
- Mobile Apps
- Laravel
- Authentication
- Sanctum
- Cookies
- API Design
- Payments
- Idempotency
- DeepSeek
- Open Source AI
- LLMs
- AI News
- Git
- Version Control
- AI Coding
- Prompting
- PHP
- Checklist
- MCP
- AI Agents
- OpenAI
- Architecture
- Microservices
- Modular Monolith
- Estimation
- Developer Life
- Project Planning
- Humour
- OAuth
- OpenID Connect
- Authentication
- Embeddings
- Vector Search
- RAG
- pgvector
- OpenAI
- GPT-4.1
- Codex CLI
- Events
- Testing
- Clean Code
- Maintainability
- Code Review
- Webhooks
- API
- Security
- Claude Code
- Workflow
- AI
- LLM
- Prompt Injection
- Mobile
- React
- Networking
- TCP
- UDP
- HTTP/3
- CLAUDE.md
- AWS
- Cloud Security
- Backups
- PHPUnit
- Software Engineering
- Leadership
- Communication
- RAG
- Embeddings
- AI Engineering
- IT Infrastructure
- Networking
- Access Control
- CI/CD
- GitHub Actions
- Gemini CLI
- Claude Code
- JavaScript
- Async/Await
- Node.js
- Promises
- Security
- Cryptography
- Passwords
- MySQL
- Database
- Vibe Coding
- Software Quality
- DNS
- Code Reading
- Onboarding
- Productivity
- Background Jobs
- Developer Humour
- Estimates
- Dev Life
- JWT
- o3-mini
- DeepSeek R1
- Rate Limiting
- Kate PMS
- E-Signing
- Audit Trail
- REST
- GraphQL
- API Design
- Laravel 12
- Upgrade Guide
- Open Source
- Self-Hosting
- Task Scheduling
- Cron
- Secrets
- CORS
- PHP
- PHP-FPM
- OPcache
- GitHub Copilot
- Software Architecture
- Engineering
- TypeScript
- JavaScript
- Type Safety
- AI Security
- React Native
- Product Design
- AI Agents
- Kiro
- Queues
- Redis
- RabbitMQ
- AWS SQS
- Nginx
- Apache
- GPT-5
- gpt-oss
- Clean Code
- Architecture
- Naming
- Documentation
- Career
- ADR
- Teamwork
- Supply Chain
- Kate HRM
- HR Software
- Permissions
- System Design
- Pagination
- SSH
- Linux
- Big O
- Databases
- Laravel Boost
- MCP
- Developer Skills
- Validation
- Databases
- Indexes
- Code Quality
- Deployment
- Developer Humour
- Feature Flags
- Code Review
- Pull Requests
- Docker
- Cursor
- Authorization
- RBAC
- Gemini
- Long Context
- PHP 8.4
- Caching
- Dependency Injection
- Web Performance
- Browser
- CSS
- Database
- Migrations
- ChatGPT
- AI for Developers
- Monitoring
- On-Call
- REST
- Backend
- SQL
- NoSQL
- Database Design
- Coding Agents
- Claude 4
- API Resources
- REST API
- Load Balancing
- Scaling
- AWS
- AI Tools
- Claude
- Sora 2
- CTE
- 2FA
- TOTP
- Programming Languages
- Prompts
- Developer Workflow
- API Gateway
- APIs
- Passport
- API Auth
- Learning
- Burnout
- Developer Growth
- Web Development
- SEO
- Kate Mall
- ChatGPT Atlas
- Agent Skills
- Middleware
- Laravel 12
- Collections
- Context Window
- Monitoring
- Commit Messages
- Self Review
- Growth
- Regex
- Programming Basics
- Text Processing
- Database Design
- Normalization
- Linux
- Server Security
- Linux Foundation
- Open Standards
- Legacy Code
- Documentation
- AI Workflow
- File Uploads
- Test Data
- Hashing
- Performance
- Caching
- Enums
- Scope Creep
- Estimation
- Codex
- Gemini CLI
- Timezones
- Carbon
- Bugs
- PHP 8.5
- Gemini 3
- GPT-5.1
- Data Integrity
- Event Loop
- Async
- Opus 4.5
- AI Models
- React
- Forms
- Frontend
- Backups
- AI Images
- DALL-E
- Midjourney
- Race Conditions
- Concurrency
- Legacy Code
- Refactoring
- Senior Engineer
- Scope
- LLM
- CDN
- Web
- Sub-Agents
- Soft Deletes
- Audit Log
- Concurrency
- AI Learning
- NestJS
- AI Evals
- Policies
- SPF DKIM DMARC
- Unicode
- UTF-8
- Knowledge Graph
- Value Objects
- Technical Debt
- Feature Flags
- Laravel Pennant
- Deployment
- Copilot
- Composer
- Dependencies
- Artisan
- Automation
- AWS S3
- Object Storage
- Cloud
- Small Language Models
- Ollama
- Production
- Sessions
- HTTP
- Mentoring
- SQL
- Virtual Machines
- Web Development
- HTTP/2
- QUIC
- Web Performance
- AI Integration
- LLM API
- SOLID
- OOP
- Hosting
- Serverless
- Merge Conflicts
- Temperature
- AI Development
- Reverse Proxy
- Nginx
- Infrastructure
- Verification
- Passkeys
- WebAuthn
- Teams
- Communication
- Stakeholders
- Monorepo
- CI/CD
- Versioning
- JSON Schema
- Livewire
- Inertia
- Meetings
- Distributed Systems
- Privacy
- Full-Stack
- T-Shaped Skills
- Money
- Notifications
- Web Security
- HTTP Headers
- CSP
- Function Calling
- Load Testing
- k6
- Data Extraction
- Debugging
- WebSockets
- SSE
- Real-Time
- Laravel Reverb
- Infrastructure as Code
- Terraform
- Side Projects
- Laravel Pint
- OpenAPI
- Swagger
- UX
- Multimodal
- Jest
- Pair Programming
- APIs
- Rate Limiting
- Resilience
- Dev Humour
- Design Tokens
- JWT
- API Keys
- Sessions
- PHPStan
- Rector
- Incidents
- Reporting
- Dashboards
- Zero Trust
- IAM
- Search
- Laravel Scout
- Junior Developers
- Mentoring
- Images
- WebP
- AVIF
- Bug Reports
- Let's Encrypt
- Design Docs
- Software Design
- Observers
- Replication
- Accountability
- Data Structures
- Reliability
- LLM Memory
- Error Handling
- Payments
- Payment Gateway
- Webhooks
- PCI DSS
- Observability
- OpenTelemetry
- Personal Brand
- Writing
- Conventions
- Dates
- Scheduling
- Disaster Recovery
- Compression
- Brotli
- Deadlines
- Developer Habits
- State Machines
- Tech Roles
- UUID
- ULID
- Horizon
- Planning
- Engineering Culture
- Ownership
- Soft Skills
- Socialite
- Cost Control
- Collations
- Unicode
- Octane
- PostgreSQL
Laravel Feature Tests That Don't Break Every Week
About Post
You rename a column. Nothing changes for the user. The API returns the same data, the app works exactly as before. And fourteen tests go red.
That's the moment a lot of teams quietly stop trusting their test suite. Tests that break on every refactor aren't protecting you. They're a tax. People start "fixing" them by updating the expected values until they pass, and at that point the tests check nothing at all.
The fix isn't fewer tests. It's tests that check the right thing. Here's how I write Laravel feature tests that survive refactors and still catch real bugs.
The rule underneath everything
Test what the user or the client can observe, not how the code does it.
A feature test should read like a short story: given this user and this data, when they make this request, then they get this response and this lasting effect happened. Whether the controller calls an Action class, a service or a job is none of the test's business. If you can rewrite the inside of a feature without changing what it does, your tests should stay green.
A brittle test, and why it breaks
Here's a test I've seen many versions of. It's for an endpoint where a tenant submits a repair request:
public function test_store(): void
{
$user = User::create([
'name' => 'Test', 'email' => '[email protected]',
'password' => bcrypt('secret'), 'role' => 'tenant',
]);
$response = $this->actingAs($user)->post('/api/repairs', [
'title' => 'Leaking tap', 'priority' => 'high',
]);
$response->assertExactJson(['data' => [
'id' => 1, 'title' => 'Leaking tap', 'priority' => 'high',
'status' => 'open', 'user_id' => 1,
]]);
}
Count the ways this breaks for reasons that have nothing to do with bugs:
- Add a required column to
usersand every test that builds a user by hand fails. - Add a
created_atfield to the response andassertExactJsonfails, even though nothing broke for clients. - Hard-coded IDs depend on what ran before. With MySQL, rolled-back transactions don't reset auto-increment, so
id1 is not guaranteed. post()instead ofpostJson()means a validation error returns a redirect, not a JSON error, so failures are confusing.
The same test, written to last
use RefreshDatabase;
public function test_tenant_can_submit_a_repair_request(): void
{
Event::fake([RepairRequested::class]);
$tenant = User::factory()->tenant()->create();
$response = $this->actingAs($tenant)->postJson('/api/repairs', [
'title' => 'Leaking tap',
'priority' => 'high',
]);
$response->assertCreated()
->assertJsonPath('data.title', 'Leaking tap')
->assertJsonPath('data.status', 'open')
->assertJsonStructure(['data' => ['id', 'title', 'priority', 'status']]);
$this->assertDatabaseHas('repair_requests', [
'user_id' => $tenant->id,
'title' => 'Leaking tap',
]);
Event::assertDispatched(RepairRequested::class);
}
The test name says what behaviour it protects. Each assertion checks something a client relies on. New fields in the response don't break it. A new column on users doesn't break it. A refactor of the controller doesn't break it. But if the endpoint stops saving the request, returns the wrong status, or forgets to fire the event that notifies the maintenance team, it fails. That's the balance you want.
Factories: build users in one place
Factories are what make the first problem disappear. When the users table gets a new required column, you update UserFactory once and every test keeps working. Use states for the variations your tests care about:
// database/factories/UserFactory.php
public function tenant(): static
{
return $this->state(fn (array $attributes) => ['role' => 'tenant']);
}
Now User::factory()->tenant()->create() reads like the business language, and only the attributes the test cares about are visible in the test. If a value matters for the assertion, pass it explicitly. If it doesn't, let the factory decide.
RefreshDatabase, and why order should never matter
The RefreshDatabase trait migrates the test database once and wraps each test in a transaction that's rolled back afterwards. Every test starts clean, and tests can run in any order, or in parallel with php artisan test --parallel.
If a test only passes when another test runs first, that's a hidden dependency waiting to break. Each test should create everything it needs.
Assert JSON shape, not JSON text
Laravel gives you several ways to check a response. Pick the loosest one that still catches the bug:
assertJsonPath('data.status', 'open')for the values that matter.assertJsonStructure([...])to guarantee the fields clients depend on exist, without fixing their values.assertJsonCount(3, 'data')for lists.assertJsonMissingPath('data.password')to make sure something sensitive is never exposed.assertExactJsononly when the exact payload is the contract, which is rare.
Fake the outside world
Feature tests should never send real emails, hit a real payment gateway or depend on the clock. Laravel's fakes handle all of it:
Mail::fake(),Notification::fake(),Queue::fake()andEvent::fake()record what would have happened, so you can assert on it.Http::fake()returns canned responses for external APIs. AddHttp::preventStrayRequests()so any unfaked call fails loudly instead of reaching the internet.$this->freezeTime()or$this->travel(31)->days()for anything involving due dates or expiry. A test that passes on the 15th and fails on the 31st is the worst kind of flaky.
One caution: fake at the edges, not in the middle. Faking your own classes to check that method A called method B brings back exactly the brittleness we're trying to remove.
The refactor test: if you could rewrite the feature's internals without changing its behaviour, and your tests would go red, those tests are checking implementation. Rewrite them to check outcomes.
A quick checklist for every feature test
- The name describes behaviour:
test_tenant_cannot_view_another_tenants_contract. - Data comes from factories with meaningful states.
RefreshDatabase, and no dependency on other tests.postJson/getJsonfor APIs, and a status assertion first.- Check the values that matter, the structure clients need, and the database effect.
- Fakes for mail, queues, HTTP and time.
- Test the unhappy paths too: validation errors (
assertJsonValidationErrors) and authorisation (assertForbidden).
What's the most fragile test you've ever had to maintain, and what finally made it stable?

Be first to comment it...