ABAP CDS Views Part 9: Unit Testing with Open SQL
ABAP CDS Views

ABAP CDS Views Part 9: Unit Testing with Open SQL

If you’ve been following this series, you already know how to build powerful, layered CDS views—from basic projections all the way through consumption views, OData exposure, and performance tuning. But here’s a question I get asked constantly by senior developers: how do you actually test CDS views in a reliable, automated way?

Testing CDS views is one of those areas that teams consistently underinvest in—until something breaks in production. In this installment of the ABAP CDS Views testing series, we’re going to fix that. I’ll walk you through a real-world approach to writing ABAP Unit Tests for CDS views using test doubles, Open SQL, and CDS Test Double Framework—so your data model stays correct as your system evolves.

“A CDS view without a test is just a query waiting to silently break.”

Why CDS View Testing Is Harder Than It Looks

Most developers assume testing CDS views means running a SELECT in SE16 and eyeballing the results. That works exactly once—during development. What you actually need is reproducible, automated test coverage that runs in your CI/CD pipeline and catches regressions before they reach production.

The challenge is that CDS views read from database tables, which means your tests are inherently tied to database state. This creates two problems:

  • Test data pollution: Real data in the system is noisy, inconsistent, and hard to control.
  • Side effects: Tests that touch the real database can fail in different system landscapes because the data differs.

SAP’s answer to this is the CDS Test Double Framework, introduced in ABAP 7.51. It lets you inject test data directly into a CDS view’s underlying entities during unit test execution—without touching the real database. Combined with ABAP Unit’s test class infrastructure, this gives you everything you need for reliable, isolated CDS tests.

We’ve already covered performance considerations in Part 8: Performance Optimization, Buffering Strategies, and Query Tuning and access control in Part 4: Advanced Annotations, Access Control, and DCL. Now let’s make your views testable.


Prerequisites: What You Need Before You Start

Before we dive into code, let’s align on what you need:

  • SAP S/4HANA 1909 or higher (CDS Test Double Framework requires ABAP 7.51+)
  • Eclipse ADT (ABAP Development Tools)—mandatory for running ABAP Unit tests against CDS
  • A CDS view with at least one underlying data source (interface view or basic view)
  • Understanding of ABAP Unit Test basics (FOR TESTING class pattern)

If your unit testing fundamentals need a refresh, I recommend reading ABAP Unit Testing in SAP S/4HANA: A Senior Architect’s Guide before continuing.


The CDS Test Double Framework: Architecture Overview

The CDS Test Double Framework works by replacing the data sources of a CDS view with in-memory test doubles during test execution. Here’s the mental model:


[Your CDS View Under Test]
         |
         v
[CDS Test Double Framework]
         |
         +--> [Test Double for Table A]  <-- you inject test data here
         |
         +--> [Test Double for Table B]  <-- you inject test data here

The framework intercepts the SELECT against the CDS view and routes it through your injected test data instead of the real database tables. Your CDS view logic—joins, filters, calculations, associations—is exercised exactly as it would be in production, but with controlled, predictable data.

The key class you’ll work with is CL_CDS_TEST_ENVIRONMENT.


Step-by-Step: Writing Your First CDS Unit Test

Step 1: Define the CDS View Under Test

Let’s start with a realistic, simple example. We have a basic interface view for sales order items:


@AbapCatalog.sqlViewName: 'ZI_SALESORDITEM'
@AbapCatalog.compiler.compareFilter: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Sales Order Item Interface View'
define view ZI_SalesOrderItem
  as select from vbap
{
  key vbeln    as SalesOrder,
  key posnr    as SalesOrderItem,
      matnr    as Material,
      kwmeng   as OrderQuantity,
      netpr    as NetPrice,
      waerk    as Currency
}

Now a consumption view that adds some business logic—filtering only open items with a net price greater than zero:


@AbapCatalog.sqlViewName: 'ZC_SALESORDITEM'
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Sales Order Item Consumption View'
define view ZC_SalesOrderItem
  as select from ZI_SalesOrderItem
{
  SalesOrder,
  SalesOrderItem,
  Material,
  OrderQuantity,
  NetPrice,
  Currency
}
where NetPrice > 0

The logic we want to test: only items with NetPrice > 0 should appear. Simple, but this is exactly the kind of filter logic that can be accidentally removed or changed during refactoring.


Step 2: Create the Test Class


CLASS zcl_tc_salesorder_item_view DEFINITION
  FINAL
  FOR TESTING
  RISK LEVEL HARMLESS
  DURATION SHORT.

  PRIVATE SECTION.

    CLASS-DATA:
      environment TYPE REF TO if_cds_test_environment.

    CLASS-METHODS:
      class_setup    RAISING cx_static_check,
      class_teardown.

    METHODS:
      setup          RAISING cx_static_check,
      teardown,
      test_only_positive_net_price FOR TESTING RAISING cx_static_check,
      test_zero_price_excluded      FOR TESTING RAISING cx_static_check,
      test_multiple_items_filtered  FOR TESTING RAISING cx_static_check.

ENDCLASS.

CLASS zcl_tc_salesorder_item_view IMPLEMENTATION.

  METHOD class_setup.
    " Initialize the CDS test environment once for all test methods
    " We target ZI_SalesOrderItem as the underlying data source
    environment = cl_cds_test_environment=>create(
                    i_for_entity = 'ZI_SALESORDERITEM' ).
  ENDMETHOD.

  METHOD class_teardown.
    " Clean up environment after all tests complete
    environment->destroy( ).
  ENDMETHOD.

  METHOD setup.
    " Clear any injected test data before each test method
    environment->clear_doubles( ).
  ENDMETHOD.

  METHOD teardown.
    " Nothing to do per-test, but good practice to implement
  ENDMETHOD.

  METHOD test_only_positive_net_price.
    " Arrange: inject one item with positive price, one with zero
    DATA(lt_test_data) = VALUE zi_salesorditem_tab(
      ( vbeln = '0000001001'
        posnr = '000010'
        matnr = 'MAT-001'
        kwmeng = '10'
        netpr = '100.00'
        waerk = 'USD' )
      ( vbeln = '0000001001'
        posnr = '000020'
        matnr = 'MAT-002'
        kwmeng = '5'
        netpr = '0.00'
        waerk = 'USD' )
    ).

    environment->insert(
      i_tabname = 'VBAP'
      i_table   = lt_test_data ).

    " Act: select from the consumption view
    SELECT salesorder,
           salesorderitem,
           netprice
      FROM zc_salesorditem
      INTO TABLE @DATA(lt_result).

    " Assert: only one record with positive price
    cl_abap_unit_assert=>assert_equals(
      act = lines( lt_result )
      exp = 1
      msg = 'Only one item with positive NetPrice should be returned' ).

    cl_abap_unit_assert=>assert_equals(
      act = lt_result[ 1 ]-salesorderitem
      exp = '000010'
      msg = 'Item 000010 with NetPrice 100.00 must be returned' ).
  ENDMETHOD.

  METHOD test_zero_price_excluded.
    " Arrange: all items with zero price
    DATA(lt_test_data) = VALUE zi_salesorditem_tab(
      ( vbeln = '0000002001'
        posnr = '000010'
        matnr = 'MAT-003'
        kwmeng = '3'
        netpr = '0.00'
        waerk = 'EUR' )
    ).

    environment->insert(
      i_tabname = 'VBAP'
      i_table   = lt_test_data ).

    SELECT COUNT(*)
      FROM zc_salesorditem
      INTO @DATA(lv_count).

    cl_abap_unit_assert=>assert_equals(
      act = lv_count
      exp = 0
      msg = 'No items with zero NetPrice should appear in the view' ).
  ENDMETHOD.

  METHOD test_multiple_items_filtered.
    " Arrange: mixed data across multiple orders
    DATA(lt_test_data) = VALUE zi_salesorditem_tab(
      ( vbeln = '0000003001' posnr = '000010' matnr = 'MAT-A'
        kwmeng = '1' netpr = '50.00'  waerk = 'USD' )
      ( vbeln = '0000003001' posnr = '000020' matnr = 'MAT-B'
        kwmeng = '2' netpr = '0.00'   waerk = 'USD' )
      ( vbeln = '0000003002' posnr = '000010' matnr = 'MAT-C'
        kwmeng = '4' netpr = '200.00' waerk = 'EUR' )
      ( vbeln = '0000003002' posnr = '000020' matnr = 'MAT-D'
        kwmeng = '7' netpr = '-10.00' waerk = 'EUR' )
    ).

    environment->insert(
      i_tabname = 'VBAP'
      i_table   = lt_test_data ).

    SELECT salesorder, salesorderitem, netprice
      FROM zc_salesorditem
      ORDER BY salesorder, salesorderitem
      INTO TABLE @DATA(lt_result).

    " Only the two positive-priced items should be returned
    cl_abap_unit_assert=>assert_equals(
      act = lines( lt_result )
      exp = 2
      msg = 'Exactly 2 items with NetPrice > 0 expected' ).

    " Verify the negative price is also excluded (WHERE clause: NetPrice > 0)
    cl_abap_unit_assert=>assert_equals(
      act = lt_result[ 1 ]-salesorder
      exp = '0000003001'
      msg = 'First order should be 0000003001' ).

    cl_abap_unit_assert=>assert_equals(
      act = lt_result[ 2 ]-salesorder
      exp = '0000003002'
      msg = 'Second order should be 0000003002' ).
  ENDMETHOD.

ENDCLASS.

Key Patterns and Best Practices

Use CLASS_SETUP for Environment Initialization

Creating the CDS test environment is relatively expensive. Use CLASS_SETUP (runs once per test class) for environment creation, and SETUP (runs before each test method) for clearing doubles. This pattern gives you clean isolation without the overhead of creating a new environment for every test.

Target the Right Entity Level

The i_for_entity parameter in cl_cds_test_environment=>create() should point to the root interface view—the one that directly accesses database tables. The framework automatically resolves dependent views in the view hierarchy.

If your consumption view chains through multiple interface views, you may need to create test doubles for each layer. Use the i_dependency_list parameter to specify additional entities:


environment = cl_cds_test_environment=>create(
  i_for_entity      = 'ZI_SALESORDERITEM'
  i_dependency_list = VALUE #(
    ( name = 'ZI_CUSTOMER' )
    ( name = 'ZI_MATERIAL' )
  ) ).

Test One Business Rule Per Method

Each test method should verify exactly one aspect of your view’s behavior. Don’t cram multiple assertions covering different scenarios into a single method. This way, when a test fails, the failure message immediately tells you which rule broke—not just that something broke.

Use Descriptive Method Names

Method names like test_only_positive_net_price read like specifications. When you look at the test runner output in ADT, these names become your living documentation of what the view is supposed to do.


Testing CDS Views with Parameters

If your CDS view uses input parameters (as covered in Part 5: Virtual Elements, Parameters, and Session Variables), you pass them directly in the SELECT statement during testing:


METHOD test_with_input_parameter.
  " Assume ZC_SalesOrderByDate has a $parameters.P_Date
  DATA(lt_data) = VALUE zc_salesordbydate_tab( " ... test rows ... ).
  environment->insert( i_tabname = 'VBAP' i_table = lt_data ).

  SELECT salesorder, orderdate
    FROM zc_salesordbydate( P_Date = '20240101' )
    INTO TABLE @DATA(lt_result).

  " Assert based on parameter filtering
  cl_abap_unit_assert=>assert_not_initial(
    act = lt_result
    msg = 'Should return orders for the given date' ).
ENDMETHOD.

Common Pitfalls and How to Avoid Them

Pitfall 1: Testing the Wrong Layer

Don’t write tests that SELECT directly from database tables and then compare to the CDS view result. You’re just testing that the database has data, not that your view logic is correct. Always test via the CDS view itself.

Pitfall 2: Forgetting to Clear Doubles

If you forget to call environment->clear_doubles() in your SETUP method, test data from one test bleeds into the next. This causes flaky, order-dependent tests that are a nightmare to debug. Always clear.

Pitfall 3: Overly Complex Test Data

The power of test doubles is that you control the data completely. Resist the temptation to create elaborate test datasets. Use the minimum data needed to prove or disprove the specific rule you’re testing. Three rows with clear values beat thirty rows with realistic-looking noise.

Pitfall 4: Ignoring Access Control

If your view has @AccessControl.authorizationCheck: #CHECK, authorization checks run during SELECT—even in tests. For unit testing, either use #NOT_REQUIRED on test-specific views or inject the right authorization context. This is another reason to keep your interface views and consumption views clearly separated, as we discussed in Part 4 on DCL and Access Control.


Integrating CDS Tests into Your Development Workflow

The real value of CDS unit tests comes when they run automatically. Here’s how to integrate them into your workflow:

  • Run locally in ADT: Right-click your test class → Run As → ABAP Unit Test. Green means your view logic is intact.
  • ABAP Test Cockpit (ATC): Configure ATC to run unit tests as part of code quality checks. Any CDS view change that breaks a test fails the ATC check.
  • gCTS + CI/CD: If you’re using git-based Change and Transport System with a CI/CD pipeline, integrate abapunit runs into your pipeline stages. Tests failing in the pipeline means the change doesn’t merge.

This connects directly to the clean code philosophy discussed in ABAP Clean Code in Practice: Refactoring Legacy SAP Code to Modern Standards—tests are not optional, they are what makes your refactoring safe.


What to Test and What Not to Test

A practical question I always get from teams: do I need 100% test coverage on every CDS view?

My answer: test business logic, not infrastructure. Here’s a mental checklist:

Test ThisSkip This
WHERE clause filtersSimple field projections with no logic
CASE expressions and calculated fieldsViews that are pure pass-throughs
Parameter-driven filteringPlatform-level framework behavior
Join conditions and cardinalityAnnotations (test via OData/Fiori instead)
Currency/unit conversionsDCL access control (integration test territory)

Focus your unit testing effort where business rules live. That’s where bugs hide, and that’s where your tests add the most value.


Conclusion: Make Your CDS Views Trustworthy

You’ve invested significant effort building a well-architected CDS view stack—layered views, proper annotations, performance tuning, OData exposure. Don’t let all that work be undermined by untested business logic that silently breaks during a transport.

The CDS Test Double Framework gives you a clean, fast, and reliable way to verify your view logic automatically. Start small: pick your most business-critical CDS view, write three focused tests for its WHERE clause or calculated fields, and run them in ADT. Once you see that green bar, you’ll wonder how you shipped CDS views without it.

In the next installment of this series, we’ll look at extending standard SAP CDS views using extensions and custom fields—a crucial skill for S/4HANA upgrade-safe customizing. Stay tuned.


Found this useful? Share it with your SAP development team—especially those who think “I tested it manually in DEV” counts as a test strategy. Drop a comment below: what’s the most complex CDS view logic you’ve had to test? I’d love to compare notes.