1

For a Laravel Test of my Product API I am trying to check if the right number of prices are listed for all products. The API returns Json and the prices are items in the products.*.prices arrays.

I am new to Laravel Test.

How do I loop through all the products and check their prices?

$response = $this->getJson('/api/product/');

$json_array = $response->json();
$test_passed = true;

for ($i = 0; $i < count($json_array); $i++) {
  //for simplification I check here if all the products have 2 prices
  $test_passed = $response->assertJsonCount(2, "product.$i.prices");
}

//check if all the test passed
$this->assertTrue($passed_test);

This works but it runs out of memory really fast. What am I doing wrong?

UPDATE:

I found in the Laravel API that AssertableJson has and ->each() method. I was trying to play around with this to make something like

$response->assertJson(fn (AssertableJson $json) =>
  $json->each(fn (AssertableJson $json) =>
    //2 will be dynamic in the final implementation.
    $json->has('prices', 2) 
    )
  );

This doesn't work, Apperently... I can't figure out how this ->each() works and there are no examples online to be found. All I can find are the Laravel docs, yet they give no example with nested arrarys.

St. Jan
  • 284
  • 3
  • 17
  • Would [this answer](https://stackoverflow.com/a/70847332/6309) help? – VonC Mar 17 '23 at 09:54
  • @vonC thank you for the reply! Sadly enough it doesn't help. This part I understand to, what I would like it to iterate through all Products ('data' as that answer calls it) and check all sizes of every prices array in the products. so loop through nested arrays using the AssertableJson functions...basicly;-) – St. Jan Mar 17 '23 at 12:23
  • Can you send the json body that you tested with in your question? – Farid Saravi Mar 19 '23 at 14:14
  • @FaridSaravi ofcourse you can access it through http://jsonblob.com/1087309750307405824 – St. Jan Mar 20 '23 at 09:41
  • Can't check right now but isn't it `$json->has(key)` and `$json->where(key, value)`? Does `$json->has('prices')->where('prices', 2)` work? – IGP Mar 21 '23 at 01:36
  • @IGP sadly enough no, because there are many possible outcomes, and I would like to test it dynamically so that it iterates over all the sub arrays. – St. Jan Mar 21 '23 at 08:20
  • Can you post an example of the response? The entire json? – IGP Mar 21 '23 at 17:02
  • @IGP ofcourse you can access it through https://jsonblob.com/1087309750307405824 – St. Jan Mar 21 '23 at 18:07

2 Answers2

1

Since you should be wiping your database clean for every test to start with a clean slate, I think the steps to take are

  1. Create an arbitrary amount of Product models with an arbitrary amount of Price models associated with them
  2. Call the api endpoint
  3. Assert there's only as many Product models as we specified in step 1.
  4. Assert each Product has as many Price models associated with them as we specified in step 1.
public function test_products_api_returns_correct_amount_of_products()
{
    // ARRANGE
    Product::factory()->count(15)->create()

    // ACT
    $response = $this->getJson('/api/product/');

    // ASSERT
    $response->assertJsonCount(15, 'products');
}

public function test_products_api_returns_correct_amount_of_prices_for_each_product()
{
    // ARRANGE
    $counts = [1, 3, 5, 7, 10];
    foreach ($counts as $count) {
        Product::factory()
            ->has(Price::factory()->count($count)) // or ->hasPrices($count)
            ->create();
    }

    // ACT
    $response = $this->getJson('/api/product/');

    // ASSERT
    foreach ($counts as $index => $count) {
        $response->assertJsonCount($count, "products.$index.prices");
    }
}

Here notice I'm using the key value of the array in the foreach. This is not possible with the AssertableJson syntax because the Closure does not accept a second parameter (unlike the Collection's each method where I could use the key if I wanted to with $collection->each(function ($item, $key) { ... });.

The AssertableJson API is a bit limited so you cannot really use it for this test, unless you make a single type of product.

public function test_products_api_returns_correct_amount_of_prices_for_each_product()
{
    // ARRANGE
    Product::factory()
        ->count(15)
        ->has(Price::factory()->count(5))
        ->create();
    }

    // ACT
    $response = $this->getJson('/api/product/');

    // ASSERT
    $response->assertJson(fn (AssertableJson $json) =>
        $json->has('products', 15, fn (AssertableJson $product) =>
            $product->count('prices', 5)->etc()
        ) 
    );
    // or
    $response->assertJson(fn (AssertableJson $json) =>
        $json->has('products', 15, fn (AssertableJson $product) =>
            $product->has('prices', 5, fn (AssertableJson $price) => 
                // assertions about the products.*.prices.* array.
            )->etc()
        ) 
    );
}

Another test you can create and that is sort of important for json apis is a test against the returned json structure. In your case it would look like this

public function test_products_api_returns_the_correct_structure()
{
    // ARRANGE
    ... make all the product, prices, options, etc

    // ACT
    $response = $this->getJson('/api/product/');

    // ASSERT
    $response->assertJsonStructure([
        'products' => [
            '*' => [
                'product_id',
                'name',
                'description',
                'included',
                'is_active',
                'prices' => [
                    '*' => [
                        'price_id',
                        'weight',
                        'height',
                        'length',
                        'depth',
                        'pieces',
                        'color',
                        'price',
                        'start_time',
                        'end_time',
                        'sales' => [
                            '*' => [
                                'sale_id',
                                'sale_price',
                                'sale_start_time',
                                'sale_end_time',
                            ],
                        ],
                    ],
                ],
                'photos' => [
                    '*' => [
                        'photo_id',
                        'alt',
                        'path',
                    ],
                ],
                'properties' => [
                    '*' => [
                        'property_id',
                        'name',
                        'description',
                        'quantitative',
                        'position',
                        'is_active',
                        'properties_properties' => [
                            '*' => [
                                'properties_property_id',
                                'name',
                                'icon',
                                'path',
                                'attributes' => [
                                    '*' => [
                                        'product_id',
                                        'properties_property_id',
                                        'position',
                                        'highlight',
                                    ],
                                ],
                            ],
                        ],
                    ],
                ],
                'options' => [
                    '*' => [
                        'option_id',
                        'name',
                        'place_holder',
                        'position',
                        'is_active',
                        'options_options' => [
                            '*' => [
                                'options_option_id',
                                'name',
                                'position',
                                'price',
                                'is_active',
                            ],
                        ],
                    ],
                ],
            ],
        ],
    ]);
}

I tested this last one against the json you posted (jsonblob.com/1087309750307405824) and it passes.

IGP
  • 14,160
  • 4
  • 26
  • 43
  • Hi thanks for writing a assertJsonStructure. Sadly enough I had this one in place already, but good to see I wrote it correct. I am looking to count the number of items in sub_arrays and sub_sub_arrays using the AssertableJson ->each() method. I can't get that to work. any ideas on that? – St. Jan Mar 23 '23 at 07:53
  • 1
    You can't do it with the full array as I've explained. It is too limited. There is no way to pass in an array key to the closure, so you can't do something equivalent to what I did with the foreach – IGP Mar 23 '23 at 15:53
  • 1
    I've added an example with the Fluent AssertableJson. I'm not using the `each` method and instead I'm using `has`. It's pretty much a combination of `count` and `each`. For example `->has('products', 15, fn (...) => ...)` asserts a key named `products` exists, has 15 elements in it and then runs the Closure for each of them. – IGP Mar 23 '23 at 16:17
  • As an aside, are you creating the products inside the test? If you are please share the code you're using. If you're not and you're testing against what the production API returns, well that's not something I'd recommend at all. – IGP Mar 23 '23 at 16:49
  • oo really why is that not recommended? – St. Jan Mar 24 '23 at 07:19
  • O, your you first two comments are very much clearing it up for me! I didn't get that from your anwser, sorry. Now that I am reading it again it is much clearer, thx! – St. Jan Mar 24 '23 at 07:24
  • 1
    It's not recommended because tests can have side effects. This is only a get query so there *shouldn't* be any side effects to worry about but if you ever end up using analytics, they'd be giving you wrong information because your tests also hit the API. What if you're testing how to insert things? Are you going to manipulate the production database? – IGP Mar 24 '23 at 15:04
  • 1
    In your `phpunit.xml` file, you have some fields to add a testing database. You can use an in-memory sqlite database and that would cover most of your use cases but you can also create a separate testing db in your dbms of choice. Using the RefreshDatabase trait where needed, you create the database for every test that needs it, starting with a clean slate. That way, you can be sure your tests are the only thing that cause side effects and it's easier to test things. This is why in the ARRANGE steps I kept inserting new data. – IGP Mar 24 '23 at 15:09
  • I get you point, but this is ofcourse the case. This test is part of a micro service environment. So there is a test environment with is spun-up by Gitlab CI/CD. This is completely separated from production, database and all. But thanks for the heads up. – St. Jan Mar 25 '23 at 10:37
0

Based on your JSON that you posted here: https://jsonblob.com/1087309750307405824 some minor change can fix your problem

$response = $this->getJson('/api/product/');
$test_passed = true;

for ($i = 0; $i < count($response->json("products")); $i++) {
  //for simplification I check here if all the products have 2 prices
  $test_passed = $response->assertJsonCount(2, "products.$i.prices");
}

//check if all the test passed
$this->assertTrue($passed_test);

If your code runs out of memory, it could be that you get all products in your database and product JSON size is too large or could be memory limitation in your system.

For checking php memory usage, you can use memory_get_usage function in your "for loop"

Farid Saravi
  • 173
  • 1
  • 4
  • Hi, thx so much for your anwser, I am really looking for a way to use the AssertableJson->each() method here. You got any idea how to get rit of that for loop? – St. Jan Mar 23 '23 at 12:04