Skip to main content

OData Mocking

Strategies for mocking OData services in Praman tests. Covers Playwright page.route() interception, SAP CAP mock server integration, and offline test patterns.

When to Mock OData​

ScenarioApproachWhen to Use
Unit/component testspage.route() interceptionNo backend needed, fast, deterministic
Integration testsCAP mock serverRealistic service behavior, schema validation
E2E testsReal SAP backendFull system validation, production-like
CI/CD pipelinepage.route() or CAP mockReliable, no SAP system dependency

Playwright page.route() Interception​

Basic OData Response Mock​

import { test, expect } from 'playwright-praman';

test.describe('Purchase Order List (Mocked)', () => {
test.beforeEach(async ({ page }) => {
// Intercept OData requests and return mock data
await page.route('**/sap/opu/odata/sap/API_PURCHASEORDER_PROCESS_SRV/**', async (route) => {
const url = route.request().url();

if (url.includes('PurchaseOrderSet')) {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
d: {
results: [
{
PurchaseOrder: '4500000001',
Supplier: '100001',
CompanyCode: '1000',
PurchasingOrganization: '1000',
NetAmount: '5000.00',
Currency: 'USD',
Status: 'Approved',
},
{
PurchaseOrder: '4500000002',
Supplier: '100002',
CompanyCode: '2000',
PurchasingOrganization: '2000',
NetAmount: '12000.00',
Currency: 'EUR',
Status: 'Pending',
},
],
},
}),
});
} else {
await route.continue();
}
});
});

test('display purchase orders from mock', async ({ ui5, ui5Navigation, ui5Matchers }) => {
await ui5Navigation.navigateToIntent('#PurchaseOrder-manage');

const table = await ui5.control({
controlType: 'sap.ui.comp.smarttable.SmartTable',
});
await ui5Matchers.toHaveRowCount(table, 2);
});
});

OData V4 Mock​

test.beforeEach(async ({ page }) => {
await page.route('**/odata/v4/PurchaseOrderService/**', async (route) => {
const url = route.request().url();

if (url.includes('PurchaseOrders') && route.request().method() === 'GET') {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
'@odata.context': '$metadata#PurchaseOrders',
value: [
{
PurchaseOrder: '4500000001',
Supplier: '100001',
NetAmount: 5000,
Currency: 'USD',
},
],
}),
});
} else {
await route.continue();
}
});
});

Mock OData $metadata​

Some UI5 apps require $metadata to render controls correctly:

const metadata = `<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="1.0" xmlns:edmx="http://schemas.microsoft.com/ado/2007/06/edmx">
<edmx:DataServices m:DataServiceVersion="2.0"
xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
<Schema Namespace="API_PURCHASEORDER" xmlns="http://schemas.microsoft.com/ado/2008/09/edm">
<EntityType Name="PurchaseOrderType">
<Key>
<PropertyRef Name="PurchaseOrder"/>
</Key>
<Property Name="PurchaseOrder" Type="Edm.String" MaxLength="10"/>
<Property Name="Supplier" Type="Edm.String" MaxLength="10"/>
<Property Name="CompanyCode" Type="Edm.String" MaxLength="4"/>
<Property Name="NetAmount" Type="Edm.Decimal" Precision="16" Scale="3"/>
<Property Name="Currency" Type="Edm.String" MaxLength="5"/>
</EntityType>
<EntityContainer Name="API_PURCHASEORDER_SRV" m:IsDefaultEntityContainer="true">
<EntitySet Name="PurchaseOrderSet" EntityType="API_PURCHASEORDER.PurchaseOrderType"/>
</EntityContainer>
</Schema>
</edmx:DataServices>
</edmx:Edmx>`;

await page.route('**/$metadata', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/xml',
body: metadata,
});
});

Mock Error Responses​

Test how the app handles OData errors:

test('handles OData error gracefully', async ({ ui5, ui5Navigation, page }) => {
await page.route('**/PurchaseOrderSet**', async (route) => {
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({
error: {
code: 'SY/530',
message: {
lang: 'en',
value: 'Internal server error during data retrieval',
},
innererror: {
errordetails: [
{
code: 'SY/530',
message: 'Database connection timeout',
severity: 'error',
},
],
},
},
}),
});
});

await ui5Navigation.navigateToIntent('#PurchaseOrder-manage');

// Verify the app shows an error message
const errorDialog = await ui5.control({
controlType: 'sap.m.Dialog',
properties: { type: 'Message' },
searchOpenDialogs: true,
});
expect(errorDialog).toBeTruthy();
});

Conditional Mocking (First Call vs Subsequent)​

let callCount = 0;

await page.route('**/PurchaseOrderSet**', async (route) => {
callCount++;

if (callCount === 1) {
// First call: return empty list
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ d: { results: [] } }),
});
} else {
// Subsequent calls: return data (simulates data creation)
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
d: {
results: [{ PurchaseOrder: '4500000001', Supplier: '100001' }],
},
}),
});
}
});

Mock Factory Pattern​

Create reusable mock factories for consistent test data:

// tests/mocks/odata-mocks.ts

interface PurchaseOrderMock {
PurchaseOrder: string;
Supplier: string;
CompanyCode: string;
NetAmount: string;
Currency: string;
Status: string;
}

export function createPurchaseOrderMock(
overrides: Partial<PurchaseOrderMock> = {},
): PurchaseOrderMock {
return {
PurchaseOrder: '4500000001',
Supplier: '100001',
CompanyCode: '1000',
NetAmount: '5000.00',
Currency: 'USD',
Status: 'Approved',
...overrides,
};
}

export function createODataV2Response<T>(results: T[]) {
return { d: { results } };
}

export function createODataV4Response<T>(value: T[], context: string) {
return { '@odata.context': context, value };
}

Usage:

import { createPurchaseOrderMock, createODataV2Response } from './mocks/odata-mocks';

await page.route('**/PurchaseOrderSet**', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(
createODataV2Response([
createPurchaseOrderMock(),
createPurchaseOrderMock({ PurchaseOrder: '4500000002', Status: 'Pending' }),
]),
),
});
});

SAP CAP Mock Server​

For more realistic OData mocking, use SAP CAP (Cloud Application Programming Model) to run a local OData service with schema validation.

Setup​

# Install CAP CLI
npm install --save-dev @sap/cds-dk

# Initialize mock project
mkdir tests/mock-server && cd tests/mock-server
cds init --add hana

Define Service Schema​

// tests/mock-server/srv/purchase-order-service.cds
using { cuid, managed } from '@sap/cds/common';

entity PurchaseOrders : cuid, managed {
PurchaseOrder : String(10);
Supplier : String(10);
CompanyCode : String(4);
PurchasingOrganization : String(4);
NetAmount : Decimal(16,3);
Currency : String(5);
Status : String(20);
Items : Composition of many PurchaseOrderItems on Items.parent = $self;
}

entity PurchaseOrderItems : cuid {
parent : Association to PurchaseOrders;
ItemNo : String(5);
Material : String(40);
Quantity : Decimal(13,3);
Plant : String(4);
NetPrice : Decimal(16,3);
}

service PurchaseOrderService {
entity PurchaseOrderSet as projection on PurchaseOrders;
}

Seed Test Data​

// tests/mock-server/db/data/PurchaseOrders.csv
PurchaseOrder;Supplier;CompanyCode;PurchasingOrganization;NetAmount;Currency;Status
4500000001;100001;1000;1000;5000.000;USD;Approved
4500000002;100002;2000;2000;12000.000;EUR;Pending
4500000003;100003;1000;1000;800.000;USD;Draft

Playwright Config with CAP Server​

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
webServer: [
{
command: 'npx cds serve --project tests/mock-server --port 4004',
port: 4004,
reuseExistingServer: !process.env.CI,
timeout: 30_000,
},
],

use: {
baseURL: 'http://localhost:8080', // UI5 app server
},

projects: [
{
name: 'with-mock-server',
testDir: './tests/integration',
use: { ...devices['Desktop Chrome'] },
},
],
});

Proxy OData Requests to CAP​

// In your UI5 app's xs-app.json or proxy config, route OData to CAP:
// /sap/opu/odata/sap/API_PURCHASEORDER_PROCESS_SRV → http://localhost:4004/odata/v4/PurchaseOrderService

// Or use page.route() to redirect:
await page.route('**/sap/opu/odata/**', async (route) => {
const url = new URL(route.request().url());
url.hostname = 'localhost';
url.port = '4004';
url.pathname = url.pathname.replace(
'/sap/opu/odata/sap/API_PURCHASEORDER_PROCESS_SRV',
'/odata/v4/PurchaseOrderService',
);
await route.continue({ url: url.toString() });
});

HAR File Replay​

Record and replay real OData traffic for deterministic tests:

// Record: run test once with HAR recording
test.use({
// @ts-expect-error -- Playwright context option
recordHar: {
path: 'tests/fixtures/odata-traffic.har',
urlFilter: '**/odata/**',
},
});

// Replay: use recorded HAR in subsequent test runs
test('replay recorded OData traffic', async ({ page }) => {
await page.routeFromHAR('tests/fixtures/odata-traffic.har', {
url: '**/odata/**',
update: false,
});

// Test runs against recorded responses — fast, deterministic
});

Best Practices​

PracticeWhy
Mock at the OData level, not DOMUI5 models parse OData responses — mock the data, not the rendered HTML
Include $metadata mocksSmartFields/SmartTables need metadata to render correctly
Use mock factoriesConsistent, type-safe test data across tests
Test error responses tooValidate your app handles 400, 403, 500 gracefully
Reset mocks between testsUse beforeEach to set up fresh routes
Match $filter and $expandMock responses should respect OData query options when possible