{"id":487,"date":"2025-08-28T10:40:39","date_gmt":"2025-08-28T10:40:39","guid":{"rendered":"https:\/\/1v0.net\/blog\/?p=487"},"modified":"2025-08-28T10:40:41","modified_gmt":"2025-08-28T10:40:41","slug":"how-to-write-feature-tests-in-laravel-for-apis","status":"publish","type":"post","link":"https:\/\/1v0.net\/blog\/how-to-write-feature-tests-in-laravel-for-apis\/","title":{"rendered":"How to Write Feature Tests in Laravel for APIs"},"content":{"rendered":"\n<h2 class=\"wp-block-heading\"><strong>How to Write Feature Tests in Laravel for APIs<\/strong><\/h2>\n\n\n\n<p>Feature tests validate full request lifecycles\u2014routes, middleware, controllers, policies, database, and JSON responses. In this guide, you\u2019ll create API endpoints, secure them, and write expressive feature tests that assert status codes, payload structure, and side effects.<\/p>\n\n\n\n<div class=\"wp-block-spacer\" style=\"height:100px\" aria-hidden=\"true\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Prerequisites and Setup<\/strong><\/h2>\n\n\n\n<p>Ensure you have a working API stack and database test environment. If you\u2019re building a token-based API, consider first reading <a href=\"\/blog\/how-to-build-a-rest-api-with-laravel-12-sanctum\">How to Build a REST API with Laravel 12 &amp; Sanctum<\/a> and <a href=\"\/blog\/securing-laravel-apis-with-sanctum-complete-guide\">Securing Laravel APIs with Sanctum: Complete Guide<\/a>.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"Bash\" data-shcb-language-slug=\"bash\"><span><code class=\"hljs language-bash\">php artisan make:model Post -mf\nphp artisan make:controller Api\/PostController --api\nphp artisan make:<span class=\"hljs-built_in\">test<\/span> Api\/PostApiTest<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Bash<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">bash<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This scaffolds a <code>Post<\/code> model, migration, factory, an API controller, and a feature test class to exercise the HTTP layer.<\/p>\n\n\n\n<div class=\"wp-block-spacer\" style=\"height:100px\" aria-hidden=\"true\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Create Routes and Controller Methods<\/strong><\/h2>\n\n\n\n<p>Define minimal endpoints for listing and creating posts. These examples assume you\u2019ll protect <code>store<\/code> with auth middleware (e.g., Sanctum).<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ routes\/api.php<\/span>\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Support<\/span>\\<span class=\"hljs-title\">Facades<\/span>\\<span class=\"hljs-title\">Route<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Http<\/span>\\<span class=\"hljs-title\">Controllers<\/span>\\<span class=\"hljs-title\">Api<\/span>\\<span class=\"hljs-title\">PostController<\/span>;\n\nRoute::get(<span class=\"hljs-string\">'\/posts'<\/span>, &#91;PostController::class, <span class=\"hljs-string\">'index'<\/span>]);\nRoute::middleware(<span class=\"hljs-string\">'auth:sanctum'<\/span>)-&gt;post(<span class=\"hljs-string\">'\/posts'<\/span>, &#91;PostController::class, <span class=\"hljs-string\">'store'<\/span>]);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p><code>GET \/posts<\/code> is public for demonstration; <code>POST \/posts<\/code> requires authentication via <code>auth:sanctum<\/code>. Adjust to your security needs.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ app\/Http\/Controllers\/Api\/PostController.php<\/span>\n<span class=\"hljs-keyword\">namespace<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Http<\/span>\\<span class=\"hljs-title\">Controllers<\/span>\\<span class=\"hljs-title\">Api<\/span>;\n\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Http<\/span>\\<span class=\"hljs-title\">Controllers<\/span>\\<span class=\"hljs-title\">Controller<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Models<\/span>\\<span class=\"hljs-title\">Post<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Http<\/span>\\<span class=\"hljs-title\">Request<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Http<\/span>\\<span class=\"hljs-title\">Response<\/span>;\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">PostController<\/span> <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-title\">Controller<\/span>\n<\/span>{\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">index<\/span><span class=\"hljs-params\">()<\/span>\n    <\/span>{\n        <span class=\"hljs-keyword\">return<\/span> response()-&gt;json(&#91;\n            <span class=\"hljs-string\">'data'<\/span> =&gt; Post::latest()-&gt;select(&#91;<span class=\"hljs-string\">'id'<\/span>,<span class=\"hljs-string\">'title'<\/span>,<span class=\"hljs-string\">'body'<\/span>])-&gt;paginate(<span class=\"hljs-number\">10<\/span>)\n        ]);\n    }\n\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">store<\/span><span class=\"hljs-params\">(Request $request)<\/span>\n    <\/span>{\n        $validated = $request-&gt;validate(&#91;\n            <span class=\"hljs-string\">'title'<\/span> =&gt; &#91;<span class=\"hljs-string\">'required'<\/span>,<span class=\"hljs-string\">'string'<\/span>,<span class=\"hljs-string\">'max:120'<\/span>],\n            <span class=\"hljs-string\">'body'<\/span>  =&gt; &#91;<span class=\"hljs-string\">'required'<\/span>,<span class=\"hljs-string\">'string'<\/span>],\n        ]);\n\n        $post = Post::create($validated);\n\n        <span class=\"hljs-keyword\">return<\/span> response()-&gt;json(&#91;\n            <span class=\"hljs-string\">'message'<\/span> =&gt; <span class=\"hljs-string\">'Created'<\/span>,\n            <span class=\"hljs-string\">'data'<\/span>    =&gt; &#91;\n                <span class=\"hljs-string\">'id'<\/span>    =&gt; $post-&gt;id,\n                <span class=\"hljs-string\">'title'<\/span> =&gt; $post-&gt;title,\n                <span class=\"hljs-string\">'body'<\/span>  =&gt; $post-&gt;body,\n            ],\n        ], Response::HTTP_CREATED);\n    }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The controller returns predictable JSON structures so your tests can assert both shape and content. For clean response formatting in larger projects, also see <a href=\"\/blog\/how-to-use-eloquent-api-resources-for-clean-apis\">How to Use Eloquent API Resources for Clean APIs<\/a>.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ database\/migrations\/xxxx_xx_xx_create_posts_table.php<\/span>\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Database<\/span>\\<span class=\"hljs-title\">Migrations<\/span>\\<span class=\"hljs-title\">Migration<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Database<\/span>\\<span class=\"hljs-title\">Schema<\/span>\\<span class=\"hljs-title\">Blueprint<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Support<\/span>\\<span class=\"hljs-title\">Facades<\/span>\\<span class=\"hljs-title\">Schema<\/span>;\n\n<span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-title\">Migration<\/span> <\/span>{\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">up<\/span><span class=\"hljs-params\">()<\/span>: <span class=\"hljs-title\">void<\/span> <\/span>{\n        Schema::create(<span class=\"hljs-string\">'posts'<\/span>, <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-params\">(Blueprint $table)<\/span> <\/span>{\n            $table-&gt;id();\n            $table-&gt;string(<span class=\"hljs-string\">'title'<\/span>);\n            $table-&gt;text(<span class=\"hljs-string\">'body'<\/span>);\n            $table-&gt;timestamps();\n        });\n    }\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">down<\/span><span class=\"hljs-params\">()<\/span>: <span class=\"hljs-title\">void<\/span> <\/span>{\n        Schema::dropIfExists(<span class=\"hljs-string\">'posts'<\/span>);\n    }\n};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This migration creates a minimal <code>posts<\/code> table. Keep columns concise to make focused tests faster and more reliable.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ database\/factories\/PostFactory.php<\/span>\n<span class=\"hljs-keyword\">namespace<\/span> <span class=\"hljs-title\">Database<\/span>\\<span class=\"hljs-title\">Factories<\/span>;\n\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Database<\/span>\\<span class=\"hljs-title\">Eloquent<\/span>\\<span class=\"hljs-title\">Factories<\/span>\\<span class=\"hljs-title\">Factory<\/span>;\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">PostFactory<\/span> <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-title\">Factory<\/span>\n<\/span>{\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">definition<\/span><span class=\"hljs-params\">()<\/span>: <span class=\"hljs-title\">array<\/span>\n    <\/span>{\n        <span class=\"hljs-keyword\">return<\/span> &#91;\n            <span class=\"hljs-string\">'title'<\/span> =&gt; <span class=\"hljs-keyword\">$this<\/span>-&gt;faker-&gt;sentence(<span class=\"hljs-number\">6<\/span>),\n            <span class=\"hljs-string\">'body'<\/span>  =&gt; <span class=\"hljs-keyword\">$this<\/span>-&gt;faker-&gt;paragraph(),\n        ];\n    }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Factories provide quick, expressive test data. For deeper patterns, see <a href=\"\/blog\/using-laravel-factories-seeders-for-test-data\">Using Laravel Factories and Seeders for Test Data<\/a>.<\/p>\n\n\n\n<div class=\"wp-block-spacer\" style=\"height:100px\" aria-hidden=\"true\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Writing Feature Tests for API Endpoints<\/strong><\/h2>\n\n\n\n<p>Below is the complete test suite for a small Posts API. Right after the code, you\u2019ll find a line-by-line breakdown explaining every import, trait, method, assertion, and why it matters.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ tests\/Feature\/Api\/PostApiTest.php<\/span>\n<span class=\"hljs-keyword\">namespace<\/span> <span class=\"hljs-title\">Tests<\/span>\\<span class=\"hljs-title\">Feature<\/span>\\<span class=\"hljs-title\">Api<\/span>;\n\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Tests<\/span>\\<span class=\"hljs-title\">TestCase<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Models<\/span>\\<span class=\"hljs-title\">User<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Models<\/span>\\<span class=\"hljs-title\">Post<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Foundation<\/span>\\<span class=\"hljs-title\">Testing<\/span>\\<span class=\"hljs-title\">RefreshDatabase<\/span>;\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">PostApiTest<\/span> <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-title\">TestCase<\/span>\n<\/span>{\n    <span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">RefreshDatabase<\/span>;\n\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">test_can_list_posts_as_json<\/span><span class=\"hljs-params\">()<\/span>\n    <\/span>{\n        Post::factory()-&gt;count(<span class=\"hljs-number\">3<\/span>)-&gt;create();\n\n        $response = <span class=\"hljs-keyword\">$this<\/span>-&gt;getJson(<span class=\"hljs-string\">'\/api\/posts'<\/span>);\n\n        $response-&gt;assertOk()\n                 -&gt;assertJsonStructure(&#91;<span class=\"hljs-string\">'data'<\/span> =&gt; &#91;<span class=\"hljs-string\">'data'<\/span> =&gt; &#91;&#91;<span class=\"hljs-string\">'id'<\/span>,<span class=\"hljs-string\">'title'<\/span>,<span class=\"hljs-string\">'body'<\/span>]]]]);\n    }\n\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">test_cannot_create_post_when_unauthenticated<\/span><span class=\"hljs-params\">()<\/span>\n    <\/span>{\n        $response = <span class=\"hljs-keyword\">$this<\/span>-&gt;postJson(<span class=\"hljs-string\">'\/api\/posts'<\/span>, &#91;\n            <span class=\"hljs-string\">'title'<\/span> =&gt; <span class=\"hljs-string\">'Hello'<\/span>,\n            <span class=\"hljs-string\">'body'<\/span>  =&gt; <span class=\"hljs-string\">'World'<\/span>,\n        ]);\n\n        $response-&gt;assertUnauthorized();\n    }\n\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">test_authenticated_user_can_create_post<\/span><span class=\"hljs-params\">()<\/span>\n    <\/span>{\n        $user = User::factory()-&gt;create();\n\n        $response = <span class=\"hljs-keyword\">$this<\/span>-&gt;actingAs($user, <span class=\"hljs-string\">'sanctum'<\/span>)\n                          -&gt;postJson(<span class=\"hljs-string\">'\/api\/posts'<\/span>, &#91;\n                              <span class=\"hljs-string\">'title'<\/span> =&gt; <span class=\"hljs-string\">'My Title'<\/span>,\n                              <span class=\"hljs-string\">'body'<\/span>  =&gt; <span class=\"hljs-string\">'Body text'<\/span>,\n                          ]);\n\n        $response-&gt;assertCreated()\n                 -&gt;assertJsonPath(<span class=\"hljs-string\">'data.title'<\/span>, <span class=\"hljs-string\">'My Title'<\/span>);\n\n        <span class=\"hljs-keyword\">$this<\/span>-&gt;assertDatabaseHas(<span class=\"hljs-string\">'posts'<\/span>, &#91;<span class=\"hljs-string\">'title'<\/span> =&gt; <span class=\"hljs-string\">'My Title'<\/span>]);\n    }\n\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">test_validation_errors_are_returned<\/span><span class=\"hljs-params\">()<\/span>\n    <\/span>{\n        $user = User::factory()-&gt;create();\n\n        $response = <span class=\"hljs-keyword\">$this<\/span>-&gt;actingAs($user, <span class=\"hljs-string\">'sanctum'<\/span>)\n                          -&gt;postJson(<span class=\"hljs-string\">'\/api\/posts'<\/span>, &#91;\n                              <span class=\"hljs-string\">'title'<\/span> =&gt; <span class=\"hljs-string\">''<\/span>,   <span class=\"hljs-comment\">\/\/ invalid<\/span>\n                              <span class=\"hljs-string\">'body'<\/span>  =&gt; <span class=\"hljs-string\">''<\/span>,   <span class=\"hljs-comment\">\/\/ invalid<\/span>\n                          ]);\n\n        $response-&gt;assertStatus(<span class=\"hljs-number\">422<\/span>)\n                 -&gt;assertJsonValidationErrors(&#91;<span class=\"hljs-string\">'title'<\/span>,<span class=\"hljs-string\">'body'<\/span>]);\n    }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p><strong>Namespace &amp; Imports<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>namespace Tests\\Feature\\Api;<\/code> \u2014 Organizes this test under <code>Feature\/Api<\/code> so it\u2019s discoverable by PHPUnit and matches your folder structure.<\/li>\n<li><code>use Tests\\TestCase;<\/code> \u2014 Base Laravel test case boots the application, loads the HTTP kernel, and gives you helpers like <code>getJson<\/code>, <code>postJson<\/code>, and <code>actingAs<\/code>.<\/li>\n<li><code>use App\\Models\\User;<\/code> and <code>use App\\Models\\Post;<\/code> \u2014 Import Eloquent models you use in factories and database assertions.<\/li>\n<li><code>use Illuminate\\Foundation\\Testing\\RefreshDatabase;<\/code> \u2014 Trait that wraps each test in a transaction or re-runs migrations to ensure a clean database for isolation and reliability.<\/li>\n<\/ul>\n\n\n\n<p><strong>Class &amp; Trait<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>class PostApiTest extends TestCase<\/code> \u2014 Extends the Laravel-aware <code>TestCase<\/code> so your app container, routing, middleware, and database are available.<\/li>\n<li><code>use RefreshDatabase;<\/code> \u2014 Ensures each test starts with a known DB state (empty tables unless your migrations seed).<\/li>\n<\/ul>\n\n\n\n<p><strong>Test 1: Listing Posts as JSON<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>Post::factory()-&gt;count(3)-&gt;create();<\/code> \u2014 Seeds three posts using your factory so the index endpoint has data to return.<\/li>\n<li><code>$this-&gt;getJson('\/api\/posts');<\/code> \u2014 Performs a JSON GET request to your API route, automatically setting the <code>Accept: application\/json<\/code> header.<\/li>\n<li><code>$response-&gt;assertOk();<\/code> \u2014 Expects HTTP 200; ensures the route is registered and didn\u2019t error.<\/li>\n<li><code>assertJsonStructure(['data' =&gt; ['data' =&gt; [[ 'id','title','body' ]]]])<\/code> \u2014 Validates the JSON shape. The duplication of <code>data<\/code> is intentional if you\u2019re wrapping a Laravel paginator: the outer <code>data<\/code> is your envelope; the inner <code>data<\/code> is the paginated array of items. Each item must include <code>id<\/code>, <code>title<\/code>, and <code>body<\/code>.<\/li>\n<\/ul>\n\n\n\n<p><strong>Test 2: Unauthenticated Create Should Fail<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>$this-&gt;postJson('\/api\/posts', [...])<\/code> \u2014 Sends a JSON POST to the protected endpoint without credentials.<\/li>\n<li><code>-&gt;assertUnauthorized();<\/code> \u2014 Expects HTTP 401, which implies your route is correctly behind <code>auth:sanctum<\/code> (or equivalent).<\/li>\n<li>Purpose: Ensures that unauthenticated users can\u2019t create resources, protecting data integrity.<\/li>\n<\/ul>\n\n\n\n<p><strong>Test 3: Authenticated Create Should Succeed<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>$user = User::factory()-&gt;create();<\/code> \u2014 Creates a real user record to authenticate requests against.<\/li>\n<li><code>$this-&gt;actingAs($user, 'sanctum')<\/code> \u2014 Authenticates as <code>$user<\/code> using the <code>sanctum<\/code> guard so the request passes the <code>auth:sanctum<\/code> middleware.<\/li>\n<li><code>-&gt;postJson('\/api\/posts', ['title' =&gt; 'My Title','body' =&gt; 'Body text'])<\/code> \u2014 Submits valid payload to create a post.<\/li>\n<li><code>$response-&gt;assertCreated();<\/code> \u2014 Expects HTTP 201 Created, matching REST semantics in your controller\u2019s <code>store<\/code> method.<\/li>\n<li><code>-&gt;assertJsonPath('data.title','My Title');<\/code> \u2014 Reads a specific JSON path and asserts the value, confirming the API echoes created data consistently.<\/li>\n<li><code>$this-&gt;assertDatabaseHas('posts',['title' =&gt; 'My Title']);<\/code> \u2014 Verifies the side effect (DB write) happened correctly, not just the HTTP layer.<\/li>\n<\/ul>\n\n\n\n<p><strong>Test 4: Validation Errors Are Returned<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>$this-&gt;actingAs($user, 'sanctum')<\/code> \u2014 Ensures we\u2019re testing pure validation, not auth.<\/li>\n<li><code>-&gt;postJson('\/api\/posts', ['title' =&gt; '', 'body' =&gt; ''])<\/code> \u2014 Sends invalid input to trigger the validator.<\/li>\n<li><code>$response-&gt;assertStatus(422);<\/code> \u2014 422 Unprocessable Entity is the expected status for validation failures in JSON APIs.<\/li>\n<li><code>-&gt;assertJsonValidationErrors(['title','body']);<\/code> \u2014 Confirms the error bag contains keys for the invalid fields, which your frontend can render inline.<\/li>\n<\/ul>\n\n\n\n<p><strong>Why <code>getJson<\/code>\/<code>postJson<\/code> Instead of <code>get<\/code>\/<code>post<\/code>?<\/strong> These helpers automatically set JSON headers, ensuring your app returns JSON responses (e.g., validation error format) instead of redirecting with HTML. This keeps tests deterministic and representative of real API clients.<\/p>\n\n\n\n<p><strong>Why <code>RefreshDatabase<\/code>?<\/strong> It guarantees each test runs against a clean schema and data set. This prevents hidden coupling between tests (e.g., leftovers from a previous test) and makes failures reproducible.<\/p>\n\n\n\n<p><strong>Why Assert Both HTTP and Database?<\/strong> HTTP assertions confirm routing, middleware, and controller logic, while database assertions confirm side effects. Together, they validate behavior end to end.<\/p>\n\n\n\n<p>With these explanations, you can confidently modify routes, policies, validation rules, and response formats while keeping your tests expressive and trustworthy.<\/p>\n\n\n\n\n<div class=\"wp-block-spacer\" style=\"height:100px\" aria-hidden=\"true\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Sample Test Output<\/strong><\/h2>\n\n\n\n<p>Running the suite with <code>php artisan test<\/code> should show all green when your endpoints and tests align.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"Bash\" data-shcb-language-slug=\"bash\"><span><code class=\"hljs language-bash\">PHPUnit 10.*\/Laravel Test Runner\n\n   PASS  Tests\\Feature\\Api\\PostApiTest\n  \u2713 test_can_list_posts_as_json\n  \u2713 test_cannot_create_post_when_unauthenticated\n  \u2713 test_authenticated_user_can_create_post\n  \u2713 test_validation_errors_are_returned\n\n  Tests:  4 passed\n  Assertions: 11\n  Time: 0.89s<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Bash<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">bash<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>For a failing sample (e.g., missing auth), PHPUnit highlights the failing expectation with a diff of expected vs. actual, plus a stack trace line to jump into your test file and controller.<\/p>\n\n\n\n<div class=\"wp-block-spacer\" style=\"height:100px\" aria-hidden=\"true\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Advanced Tips: Policies, Rate Limits, and Resources<\/strong><\/h2>\n\n\n\n<p>Layer in authorization and rate limiting for production realism. Use policies to gate actions and add tests that assert <code>403<\/code> for forbidden operations. For token issuance and scopes, consider <a href=\"\/blog\/how-to-add-jwt-authentication-to-laravel-apis\">How to Add JWT Authentication to Laravel APIs<\/a> if JWT fits your architecture.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ Example: assert rate limiting headers exist (if enabled)<\/span>\n$response = <span class=\"hljs-keyword\">$this<\/span>-&gt;getJson(<span class=\"hljs-string\">'\/api\/posts'<\/span>);\n$response-&gt;assertOk();\n<span class=\"hljs-keyword\">$this<\/span>-&gt;assertTrue($response-&gt;headers-&gt;has(<span class=\"hljs-string\">'X-RateLimit-Remaining'<\/span>));<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>These checks help verify production-grade API behavior beyond basic CRUD: authorization, throttling, and predictable resource shapes.<\/p>\n\n\n\n<div class=\"wp-block-spacer\" style=\"height:100px\" aria-hidden=\"true\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Wrapping Up<\/strong><\/h2>\n\n\n\n<p>You built API endpoints and wrote comprehensive feature tests covering listing, authentication, creation, and validation. You also learned how to read test output and assert headers and JSON shapes\u2014key skills for keeping APIs robust during rapid iteration.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>What\u2019s Next<\/strong><\/h2>\n\n\n\n<p>Deepen your API testing and security with these related guides:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"\/blog\/how-to-build-a-rest-api-with-laravel-12-sanctum\">How to Build a REST API with Laravel 12 &amp; Sanctum<\/a><\/li>\n<li><a href=\"\/blog\/securing-laravel-apis-with-sanctum-complete-guide\">Securing Laravel APIs with Sanctum: Complete Guide<\/a><\/li>\n<li><a href=\"\/blog\/how-to-use-eloquent-api-resources-for-clean-apis\">How to Use Eloquent API Resources for Clean APIs<\/a><\/li>\n<\/ul>\n\n","protected":false},"excerpt":{"rendered":"<p>How to Write Feature Tests in Laravel for APIs Feature tests validate full request lifecycles\u2014routes, middleware, controllers, policies, database, and JSON responses. In this guide, you\u2019ll create API endpoints, secure them, and write expressive feature tests that assert status codes, payload structure, and side effects. Prerequisites and Setup Ensure you have a working API stack [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":491,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[7],"tags":[25,86,22,87],"class_list":["post-487","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-laravel","tag-api","tag-phpunit","tag-security","tag-testing"],"_links":{"self":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts\/487","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/comments?post=487"}],"version-history":[{"count":1,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts\/487\/revisions"}],"predecessor-version":[{"id":490,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts\/487\/revisions\/490"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/media\/491"}],"wp:attachment":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/media?parent=487"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/categories?post=487"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/tags?post=487"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}