laravel tests Invalid argument supplied for foreach()

02 Sep 2019 . Tips and Tricks . 597 views

If you faced the issue with laravel tests Invalid argument supplied for foreach(), this post might be helpful for you. Recently I was trying to test something, where I was facing this issue. I will write the code in details with the scenario, hope it will be helpful for you.

Scenario

I allow my user to create a post where user can able to choose category or categories for this post at a time. Meaning, during creating post it will store the categories belongs to this post.

Tables

The 3 tables structure are-

posts

- id
- title
- slug
- details

categories

- id
- title
- slug

category_post

- id
- post_id
- category_id

Relationships

I am showing the relationships between posts and categories.

App\Post.php

<?php

namespace App;

use App\Category;
use App\CategoryPost;
use Illuminate\Database\Eloquent\Model;

class Category extends Model
{
	/**
	 * A post is belongs to a Category
	 */
    public function category()
    {
        return $this->hasMany(Category::class);
    }

    /**
     * A post has many CategoryPost
     */
    public function categoryPost()
    {
        return $this->hasMany(CategoryPost::class);
    }
}

App\Category.php

<?php

namespace App;

use App\post;
use App\CategoryPost;
use Illuminate\Database\Eloquent\Model;

class Category extends Model
{
	/**
	 * A Category has Many Posts
	 */
    public function post()
    {
        return $this->hasMany(Post::class);
    }

    /**
     * A Category has many CategoryPost
     */
    public function categoryPost()
    {
        return $this->hasMany(CategoryPost::class);
    }
}

App\CategoryPost.php

<?php

namespace App;

use App\Post;
use App\Category;
use Illuminate\Database\Eloquent\Model;

class CategoryPost extends Model
{
	/**
	 * A CategoryPost is belongs to a Category
	 */
    public function category()
    {
        return $this->belongsTo(Category::class);
    }

    /**
     * A post has many CategoryPost
     */
    public function post()
    {
        return $this->belongsTo(Post::class);
    }
}

Writing Test.

Now I want to write a test that executes the following criteria-

  • as a user, I should able to submit the add new post route that will generate-
    • Create a new post
    • Add selected categories in the category_post table

PostTest.php


/** @test */
public function as_a_logged_in_user_I_should_submit_the_create_new_post_form()
    {
        $this->withoutExceptionHandling();

        $user = factory(User::class)->create([
            'flag' => 1,
            'password' => bcrypt($password = 'ILoveLaravel')
        ]);

        $hasUser = $user ? true : false;

        $this->assertTrue($hasUser);

        $response = $this->post(route('login'), [
            'email' => $user->email,
            'password' => $password,
            'flag' => 1
        ]);

        $postData = [
            'title' => $title = 'This is the title',
            'slug' => str_slug($title),
            'details' => 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. A aliquam aperiam autem, cum cumque cupiditate deleniti eius fugiat magnam, minima officiis porro qui quidem quis quo rem veniam. Et, ipsam.',
        ];

        $eventResponse = $this->post(route('post.store'), $postData);

        $eventResponse->assertStatus(201);

        $eventResponse->assertRedirect(route('events.add'));
    }

Real Coding

Now, let's write some real coding in the controller. We have a dedicated controller for that called PostController.

public function store(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'title' => 'required',
            'details' => 'required',
        ]);

        if ($validator->fails()) {
            return redirect(route('post.add'))
                ->withErrors($validator)
                ->withInput();
        };

        // create a new Post
        $post = Post::create([
            'title' => $request->title,
            'slug' => str_slug($request->title, '-'),
            'details' => $request->details,
        ]);

        // Redirect and Success Message.
    }

Until now, the code and testing working fine. No issue what so ever in my side.

If you notice closely that, I didn't store any category details in the category_post table. Let's add in the controller first.


public function store(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'title' => 'required',
            'details' => 'required',
        ]);

        if ($validator->fails()) {
            return redirect(route('post.add'))
                ->withErrors($validator)
                ->withInput();
        };

        // create a new Post
        $post = Post::create([
            'title' => $request->title,
            'slug' => str_slug($request->title, '-'),
            'details' => $request->details,
        ]);

        // add records in the category_post table.
        // $request->categories will return [1,2,3] like this
        foreach ($request->categories as $category){
            CategoryPost::create([
                'post_id' => $post->id,
                'category_id' => $category,
            ]);
        }

        // Redirect and Success message.
    }

Code is working fine in the browser. Now, let's try in the unit testing.

vendor/bin/phpunit

And I believe you will see some sort of error called Invalid argument supplied for foreach() . I have found that. In the beginning, I didn't have any single clue of it. After a deep research, I realize that it because $request->categories is missing in the testing.

So, in the test $postData, I have added a new array called categories, and it will be like this-

$postData = [
            'title' => $title = 'This is the title',
            'slug' => str_slug($title),
            'details' => 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. A aliquam aperiam autem, cum cumque cupiditate deleniti eius fugiat magnam, minima officiis porro qui quidem quis quo rem veniam. Et, ipsam.',
            'categories' => $categories = [1,2,3,4]
        ];

The whole testing method will be like this-

/** @test */
public function as_a_logged_in_user_I_should_submit_the_create_new_post_form()
    {
        $this->withoutExceptionHandling();

        $user = factory(User::class)->create([
            'flag' => 1,
            'password' => bcrypt($password = 'ILoveLaravel')
        ]);

        $hasUser = $user ? true : false;

        $this->assertTrue($hasUser);

        $response = $this->post(route('login'), [
            'email' => $user->email,
            'password' => $password,
            'flag' => 1
        ]);

        $postData = [
            'title' => $title = 'This is the title',
            'slug' => str_slug($title),
            'details' => 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. A aliquam aperiam autem, cum cumque cupiditate deleniti eius fugiat magnam, minima officiis porro qui quidem quis quo rem veniam. Et, ipsam.',
            'categories' => $categories = [1,2,3,4]
        ];

        $eventResponse = $this->post(route('post.store'), $postData);

        $eventResponse->assertStatus(201);

        $eventResponse->assertRedirect(route('events.add'));
    }

Now if I run vendor/bin/phpunit finally and I get everything is green.

Although this article is quite long, however, hope it will be helpful for you.

Thank you for reading.