Skip to content

Recipe: Testing Your OData Service

This recipe shows how to write tests for your OData service at different levels using Pest and Orchestra Testbench.

Test setup

The library uses Orchestra Testbench for testing with a real Laravel application instance and SQLite in-memory database.

Base test case

php
<?php

namespace Tests;

use App\OData\ServiceRegistry;
use LaravelUi5\OData\ODataServiceProvider;
use LaravelUi5\OData\Service\Contracts\ODataServiceRegistryInterface;

abstract class ODataTestCase extends \Orchestra\Testbench\TestCase
{
    protected function defineDatabaseMigrations(): void
    {
        $this->loadMigrationsFrom(database_path('migrations'));
    }

    protected function getPackageProviders($app): array
    {
        return [ODataServiceProvider::class];
    }

    protected function setUp(): void
    {
        parent::setUp();

        // Bind your service registry
        $this->app->singleton(
            ODataServiceRegistryInterface::class,
            ServiceRegistry::class,
        );
    }
}

Tier 1: Unit tests (Edm layer)

Test your entity type definitions without Laravel:

php
<?php

use App\OData\ProductService;
use LaravelUi5\OData\Edm\Contracts\Container\PrimitiveTypeEnum;

it('defines a Product entity type with id key', function () {
    // Build the Edm manually to test structure
    $service = new ProductService();
    $schema = $service->schema();
    $edmx = $schema->getEdmx();

    $productType = $edmx->getSchemas()['Shop.Data']->getEntityType('Product');

    expect($productType)->not->toBeNull()
        ->and($productType->getKey())->toHaveCount(1)
        ->and($productType->getKey()[0]->getName())->toBe('id');
});

Tier 2: Resolver tests (SQLite in-memory)

Test that resolvers correctly translate query plans:

php
<?php

use LaravelUi5\OData\Driver\Sql\SqlEntitySetResolver;
use LaravelUi5\OData\Protocol\Planning\EntitySetQueryPlan;
// ... other imports

uses(Tests\ODataTestCase::class)
    ->beforeEach(function () {
        // Seed test data
        DB::table('products')->insert([
            ['name' => 'Widget', 'price' => 9.99, 'active' => true],
            ['name' => 'Gadget', 'price' => 24.50, 'active' => false],
        ]);
    });

it('resolves all products from the table', function () {
    $resolver = new SqlEntitySetResolver('products');
    $plan = buildPlan(); // helper that creates an EntitySetQueryPlan

    $rows = iterator_to_array($resolver->resolve($plan), false);

    expect($rows)->toHaveCount(2);
});

Tier 3: HTTP round-trip tests

Test the full request/response cycle:

php
<?php

uses(Tests\ODataTestCase::class)
    ->beforeEach(function () {
        App\Models\Product::create(['name' => 'Widget', 'price' => 9.99]);
        App\Models\Product::create(['name' => 'Gadget', 'price' => 24.50]);
    });

it('returns all products as JSON', function () {
    $response = $this->get('/odata/Products');

    $response->assertStatus(200);
    $json = $response->json();

    expect($json['value'])->toHaveCount(2)
        ->and($json['@odata.context'])->toContain('$metadata#Products');
});

it('filters products by price', function () {
    $response = $this->get('/odata/Products?$filter=price gt 10');

    $response->assertStatus(200);
    expect($response->json('value'))->toHaveCount(1)
        ->and($response->json('value.0.name'))->toBe('Gadget');
});

it('returns metadata as XML', function () {
    $response = $this->get('/odata/$metadata');

    $response->assertStatus(200);
    $response->assertHeader('content-type', 'application/xml');
});

it('returns 404 for unknown entity', function () {
    $response = $this->get('/odata/Products(999)');

    $response->assertStatus(404);
    expect($response->json('error.code'))->toBe('not_found');
});

it('returns service document', function () {
    $response = $this->get('/odata');

    $response->assertStatus(200);
    $sets = collect($response->json('value'));
    expect($sets->pluck('name'))->toContain('Products');
});

it('supports $select', function () {
    $response = $this->get('/odata/Products?$select=name');

    $response->assertStatus(200);
    $first = $response->json('value.0');
    expect($first)->toHaveKey('name')
        ->and($first)->not->toHaveKey('price');
});

it('supports $count', function () {
    $response = $this->get('/odata/Products?$count=true&$top=1');

    $response->assertStatus(200);
    expect($response->json('@odata.count'))->toBe(2)
        ->and($response->json('value'))->toHaveCount(1);
});

Tips

  • Use $this->withoutExceptionHandling() in setUp to get clear stack traces
  • Use SQLite in-memory (:memory:) for fast, isolated tests
  • Seed data in beforeEach so each test starts with a known state
  • Test edge cases: empty results, null values, special characters in $filter
  • Test error responses: invalid filter syntax, unknown entity sets, missing keys

OData: MIT | Core: BSL 1.1 | SDK: Commercial License