Lesson 3 of 6
Querying: $filter, $select, $expand and paging
10 min
Listen to this lesson
Most API tests are reads: check that a document exists with the right values, that a list contains what it should, that a filter returns the right subset. Getting the query exactly right is a testing skill in itself, because a wrong query fails quietly: it returns 200 with the wrong data, or with only the first page.
The query options you will use daily
| Option | Example (V2, Business Partner) | Purpose |
|---|---|---|
| $filter | $filter=BusinessPartnerCategory eq '2' | Organizations only |
| $filter with and | $filter=BusinessPartnerCategory eq '2' and CreationDate ge datetime'2026-01-01T00:00:00' | Combine conditions; V2 date literal syntax |
| $select | $select=BusinessPartner,BusinessPartnerFullName | Return only what the test checks |
| $expand | $expand=to_BusinessPartnerAddress | Include related entities in one call |
| $orderby | $orderby=BusinessPartner desc | Stable order for paging and comparisons |
| $top / $skip | $top=50&$skip=100 | Client-side paging |
| Count | $inlinecount=allpages (V2) or $count=true (V4) | Total number of matches with the page |
Query options with Business Partner examples.
V4 adds richer syntax: $filter=contains(BusinessPartnerFullName,'Fleet') where V2 uses substringof('Fleet',BusinessPartnerFullName), and V4 allows options inside an expand, such as $expand=_Item($select=Product,RequestedQuantity). Whether a service supports a function or a nested option depends on the implementation, so try it in the sandbox before writing it into a test.
Literals by type
| Type | OData V2 literal | OData V4 literal |
|---|---|---|
| String | '17100001' | '17100001' |
| Date and time | datetime'2026-10-14T00:00:00' | 2026-10-14T00:00:00Z (DateTimeOffset) or 2026-10-14 (Date) |
| Decimal | 12000.00M | 12000.00 |
| GUID | guid'...' | The GUID without prefix |
| Boolean | true / false | true / false |
Literal syntax changes between versions; a V2 date literal sent to a V4 service fails with 400.
Keys, leading zeros and encoding
- Leading zeros. Many SAP keys are character fields with leading zeros. In some services the key must be sent with them; others convert. Great Lakes Fleet Services is business partner 17100001, which some interfaces show as 0017100001. Read the metadata and test both forms once.
- Quotes. String keys and literals go in single quotes; a quote inside a value is doubled.
- Encoding. Spaces, quotes and special characters must be URL-encoded. Most clients do it; hand-built URLs in scripts often do not.
- Case. Property names are case-sensitive.
businesspartneris notBusinessPartner.
Paging: the quiet failure
Services may limit how many entries they return per request. When they do, the response contains a link to the next page: __next in V2 JSON, @odata.nextLink in V4. A test that counts "all open sales orders for Great Lakes" and reads only the first response is wrong as soon as the data grows. Always request a count, and follow next links until there are none.
Testing the filter itself
When the thing under test is a filter (an interface that pulls "orders changed since the last run", or a custom service with a date parameter), apply the boundary-value technique from sap-tst-201. Create or find records just before, exactly on and just after the boundary, and assert which come back. Date filters are the usual culprits: ge versus gt, time zones in V4 DateTimeOffset values, and V2 DateTime values that the backend reads as local time. A delta interface that misses records created at exactly midnight is a real and expensive defect.
$expand versus many calls
A test that needs a sales order and its items can make one call with $expand=to_Item or one call for the header and one per item. The expanded call is faster and is what Fiori apps typically do, so it also tests the path users take. Expanding several levels deep on large documents can be slow or hit service limits, though, so keep expands to what the assertions need.
Worked example: Great Lakes orders
In CMQ, Maria's regression set checks that sales order 1000457 for Great Lakes Fleet Services carries 50 pieces of CMC-ALT-100 at 240.00 USD. A precise read is GET /sap/opu/odata/sap/API_SALES_ORDER_SRV/A_SalesOrder('1000457')?$expand=to_Item&$select=SalesOrder,SoldToParty,TotalNetAmount,TransactionCurrency,to_Item/Material,to_Item/RequestedQuantity,to_Item/NetAmount. The test asserts SoldToParty 17100001, TotalNetAmount 12000.00, currency USD, one item with material CMC-ALT-100 and quantity 50. Property names come from the service metadata; verify them for your release before you rely on them.
- MetadataConfirm entity, keys, property names and filterable fields
- Sandbox trialTry the query shape on sample data
- Narrow$select only asserted fields; $orderby for stability
- Count and pageRequest a count; follow next links
- AssertBusiness values, not only status 200
What would you do?
A regression test checks that "all Great Lakes Fleet Services orders created this week are in status open". It returns 200 and passes, but Maria knows one order was blocked. The test reads A_SalesOrder with a $filter on SoldToParty and CreationDate. What should you check first?
- $filter, $select, $expand, $orderby, $top/$skip and count are the daily toolkit.
- V2 and V4 differ in functions (substringof versus contains), count and paging links.
- Watch leading zeros, quoting, encoding and case.
- Follow __next or @odata.nextLink and compare with the count before asserting.
Check your understanding · not graded
Lesson check
1. Which V2 option returns the total count with the page?
2. Where is the next-page link in a V4 JSON response?
3. Which V4 function matches the V2 substringof?
4. Why use $select in a read test?
5. A filter returns 200 and an empty list. What does that prove?