Step 12: Unit Tests
The app is in production since Step 11, and changes will keep coming — a new filter, a second editable field, the next framework release. What lets the next transport leave with confidence is the same thing as in every other ABAP project: unit tests. This step adds them to the walkthrough app, and the point of it is how little abap2UI5 gets in the way — the methods worth testing are plain ABAP, so the test class is one you could have written before ever hearing of this framework.
The Class Under Test
The app is unchanged from Step 10 — printed here in full so this step stands on its own:
CLASS zcl_app_walkthrough DEFINITION PUBLIC.
PUBLIC SECTION.
INTERFACES z2ui5_if_app.
TYPES:
BEGIN OF ty_s_invoice,
product TYPE string,
supplier TYPE string,
quantity TYPE string,
delivery_date TYPE string,
END OF ty_s_invoice.
DATA:
BEGIN OF s_search,
supplier TYPE string,
date_from TYPE string,
date_to TYPE string,
END OF s_search.
DATA t_invoices TYPE STANDARD TABLE OF ty_s_invoice WITH EMPTY KEY.
DATA s_edit TYPE ty_s_invoice.
PROTECTED SECTION.
DATA client TYPE REF TO z2ui5_if_client.
METHODS on_event.
METHODS view_display.
METHODS popup_edit_display.
METHODS data_read.
METHODS data_update.
PRIVATE SECTION.
ENDCLASS.
CLASS zcl_app_walkthrough IMPLEMENTATION.
METHOD z2ui5_if_app~main.
me->client = client.
IF client->check_on_navigated( ).
view_display( ).
ELSEIF client->check_on_event( ).
on_event( ).
ENDIF.
ENDMETHOD.
METHOD on_event.
" get_event( ) holds the name passed to _event( );
" get_event_arg( ) returns the extra argument attached via t_arg
CASE client->get_event( ).
WHEN `READ`.
data_read( ).
WHEN `EDIT`.
s_edit = VALUE #( t_invoices[ product = client->get_event_arg( ) ] OPTIONAL ).
popup_edit_display( ).
WHEN `SAVE`.
data_update( ).
client->popup_destroy( ).
client->message_toast_display( |{ s_edit-product } updated.| ).
WHEN `CANCEL`.
client->popup_destroy( ).
ENDCASE.
ENDMETHOD.
METHOD view_display.
DATA(view) = z2ui5_cl_ui5_view_builder=>factory(
)->ele( n = `View` ns = `mvc`
)->a( n = `xmlns` v = `sap.m`
)->a( n = `xmlns:mvc` v = `sap.ui.core.mvc`
)->a( n = `xmlns:form` v = `sap.ui.layout.form` ).
DATA(page) = view->ele( `Shell`
)->ele( `Page`
)->a( n = `title` v = `Walkthrough - Step 10`
)->a( n = `navButtonPress` v = client->_event_nav_app_leave( )
)->a( n = `showNavButton` b = client->check_app_prev_stack( ) ).
page->ele( n = `SimpleForm` ns = `form`
)->a( n = `title` v = `Selection`
)->a( n = `editable` v = `true`
)->ele( n = `content` ns = `form`
)->tag( `Label`
)->a( n = `text` v = `Supplier`
)->tag( `Input`
)->a( n = `value` v = client->_bind( s_search-supplier )
)->tag( `Label`
)->a( n = `text` v = `Delivery Date From`
)->tag( `DatePicker`
)->a( n = `value` v = client->_bind( s_search-date_from )
)->a( n = `valueFormat` v = `yyyy-MM-dd`
)->tag( `Label`
)->a( n = `text` v = `Delivery Date To`
)->tag( `DatePicker`
)->a( n = `value` v = client->_bind( s_search-date_to )
)->a( n = `valueFormat` v = `yyyy-MM-dd`
)->tag( `Button`
)->a( n = `text` v = `Read Invoices`
)->a( n = `press` v = client->_event( `READ` )
)->a( n = `type` v = `Emphasized` ).
DATA(tab) = page->ele( `Table`
)->a( n = `headerText` v = `Invoices`
)->a( n = `items` v = client->_bind( t_invoices ) ).
tab->ele( `columns`
)->ele( `Column`
)->tag( `Text`
)->a( n = `text` v = `Product`
)->end(
)->ele( `Column`
)->tag( `Text`
)->a( n = `text` v = `Supplier`
)->end(
)->ele( `Column`
)->tag( `Text`
)->a( n = `text` v = `Quantity`
)->end(
)->ele( `Column`
)->tag( `Text`
)->a( n = `text` v = `Delivery Date`
)->end(
)->ele( `Column`
)->a( n = `width` v = `10%` ).
tab->ele( `items`
)->ele( `ColumnListItem`
)->ele( `cells`
)->tag( `Text`
)->a( n = `text` v = `{PRODUCT}`
)->tag( `Text`
)->a( n = `text` v = `{SUPPLIER}`
)->tag( `Text`
)->a( n = `text` v = `{QUANTITY}`
)->tag( `Text`
)->a( n = `text` v = `{DELIVERY_DATE}`
)->tag( `Button`
)->a( n = `icon` v = `sap-icon://edit`
)->a( n = `tooltip` v = `Edit delivery date`
)->a( n = `press` v = client->_event( val = `EDIT`
t_arg = VALUE #( ( `${PRODUCT}` ) ) ) ).
client->view_display( view->stringify( ) ).
ENDMETHOD.
METHOD popup_edit_display.
DATA(popup) = z2ui5_cl_ui5_view_builder=>factory(
)->ele( n = `FragmentDefinition` ns = `core`
)->a( n = `xmlns` v = `sap.m`
)->a( n = `xmlns:core` v = `sap.ui.core`
)->ele( `Dialog`
)->a( n = `title` v = |Edit { s_edit-product }|
)->ele( `content`
)->tag( `Label`
)->a( n = `text` v = `Delivery Date`
)->tag( `DatePicker`
)->a( n = `value` v = client->_bind( s_edit-delivery_date )
)->a( n = `valueFormat` v = `yyyy-MM-dd`
)->end(
)->ele( `buttons`
)->tag( `Button`
)->a( n = `text` v = `Cancel`
)->a( n = `press` v = client->_event( `CANCEL` )
)->tag( `Button`
)->a( n = `text` v = `Save`
)->a( n = `press` v = client->_event( `SAVE` )
)->a( n = `type` v = `Emphasized` ).
client->popup_display( popup->stringify( ) ).
ENDMETHOD.
METHOD data_read.
" demo data — in your system, replace this with a SELECT, e.g.:
" SELECT product, supplier, quantity, delivery_date
" FROM zinvoice
" WHERE supplier LIKE @s_search-supplier
" AND delivery_date BETWEEN @s_search-date_from AND @s_search-date_to
" INTO TABLE @t_invoices.
t_invoices = VALUE #(
( product = `Pineapple` supplier = `ACME` quantity = `21` delivery_date = `2026-07-15` )
( product = `Milk` supplier = `Green Growers` quantity = `4` delivery_date = `2026-07-20` )
( product = `Canned Beans` supplier = `Corner Deli` quantity = `3` delivery_date = `2026-08-01` )
( product = `Salad` supplier = `Green Growers` quantity = `2` delivery_date = `2026-08-10` )
( product = `Bread` supplier = `Corner Deli` quantity = `1` delivery_date = `2026-08-12` ) ).
IF s_search-supplier IS NOT INITIAL.
" NS = `contains no string` — drop the rows whose supplier does not match
DELETE t_invoices WHERE supplier NS s_search-supplier.
ENDIF.
IF s_search-date_from IS NOT INITIAL.
DELETE t_invoices WHERE delivery_date < s_search-date_from.
ENDIF.
IF s_search-date_to IS NOT INITIAL.
DELETE t_invoices WHERE delivery_date > s_search-date_to.
ENDIF.
ENDMETHOD.
METHOD data_update.
" in your system, persist the change with an UPDATE, e.g.:
" UPDATE zinvoice SET delivery_date = @s_edit-delivery_date
" WHERE product = @s_edit-product.
t_invoices[ product = s_edit-product ]-delivery_date = s_edit-delivery_date.
ENDMETHOD.
ENDCLASS.The Test Class
Tests live where they always live: in the class's Test Classes include (the Test Classes tab in ADT). Nothing abap2UI5-specific is in them:
CLASS ltcl_walkthrough DEFINITION DEFERRED.
CLASS zcl_app_walkthrough DEFINITION LOCAL FRIENDS ltcl_walkthrough.
CLASS ltcl_walkthrough DEFINITION FINAL
FOR TESTING RISK LEVEL HARMLESS DURATION SHORT.
PRIVATE SECTION.
METHODS read_filters_by_supplier FOR TESTING.
METHODS read_filters_by_date FOR TESTING.
METHODS update_writes_back FOR TESTING.
ENDCLASS.
CLASS ltcl_walkthrough IMPLEMENTATION.
METHOD read_filters_by_supplier.
DATA(cut) = NEW zcl_app_walkthrough( ).
cut->s_search-supplier = `Green Growers`.
cut->data_read( ).
cl_abap_unit_assert=>assert_equals(
act = lines( cut->t_invoices )
exp = 2
msg = `expected exactly the two Green Growers invoices` ).
ENDMETHOD.
METHOD read_filters_by_date.
DATA(cut) = NEW zcl_app_walkthrough( ).
cut->s_search-date_from = `2026-08-01`.
cut->data_read( ).
LOOP AT cut->t_invoices INTO DATA(ls_invoice).
cl_abap_unit_assert=>assert_true(
act = xsdbool( ls_invoice-delivery_date >= `2026-08-01` )
msg = |{ ls_invoice-product } lies before the date filter| ).
ENDLOOP.
ENDMETHOD.
METHOD update_writes_back.
DATA(cut) = NEW zcl_app_walkthrough( ).
cut->data_read( ).
cut->s_edit = VALUE #( product = `Milk` delivery_date = `2026-09-01` ).
cut->data_update( ).
cl_abap_unit_assert=>assert_equals(
act = cut->t_invoices[ product = `Milk` ]-delivery_date
exp = `2026-09-01` ).
ENDMETHOD.
ENDCLASS.Run them as always — in ADT with Ctrl+Shift+F10. Three tests, three different seams:
read_filters_by_supplierfills the selection screen's backing structures_search— a plain public attribute — callsdata_readand counts the result. No button was pressed and no view was built: the test enters through the same attribute the UI binds.read_filters_by_datedoes the same for the date filter and checks a property of every row rather than a count.update_writes_backplays the popup workflow without the popup: fills_editthe way the dialog's bindings would, calldata_update, and check the table. When the demo data becomes a realUPDATEin Step 11, this is the test that grows a test double for the database layer — the seam is already in place.
Why This Worked
- The tests never mock the framework.
mainis a dispatcher and the handler methods do not takeclientas a parameter — so the logic under test is reachable without a single framework object. That is the payoff of Step 10's structure. LOCAL FRIENDSopens the protected section.data_readanddata_updateare protected — implementation, not model. The two lines above the test class (DEFERRED, thenLOCAL FRIENDS) let the test class call them anyway, without making them public for everyone. Do not skip them: withoutLOCAL FRIENDSthe class fails to activate on a real system even where a linter stays quiet.- Public attributes are the natural test interface. The same attributes the framework serializes and the view binds —
s_search,t_invoices,s_edit— are what tests fill and assert on. The UI enters the class the same way the test does.
What the unit tests deliberately do not cover is the view: whether Table has an items aggregation is not a question ABAP can answer. That check exists too, without a system — the abap2UI5 linter reconstructs the view from the builder chain and validates it against UI5, and the tooling page shows how it runs in CI next to these tests.
What to Take Away
- An abap2UI5 app is testable like any ABAP class, because it is one — no UI5 runtime, no HTTP, no mocks
- Test through the same public attributes the view binds; call the data methods directly
DEFERRED+LOCAL FRIENDSis the pattern for testing protected methods — and a missingLOCAL FRIENDSis an activation error, not a style issue- Views are checked by the linter, logic by unit tests; together they run without an SAP system
That is the end of the walkthrough: one class, grown from a message box to a tested app in production. The Cookbook covers every topic again as a reference chapter, and the sample catalogues continue from here.
