#24 ABAP Unit for a Screen
An abap2UI5 app is a global class, and a global class has a Test Classes include. That is the whole story of testing here, and the reason it stays short is a decision in the app rather than in the framework: the logic does not touch client.
main( ) dispatches. The methods it dispatches to read data, decide, and change attributes. Only view_display( ) and the message calls need the client, so a test calls the other methods directly and looks at the attributes afterwards:
CLASS zcl_app_overdue DEFINITION PUBLIC.
PUBLIC SECTION.
INTERFACES z2ui5_if_app.
TYPES:
BEGIN OF ty_s_invoice,
invoice TYPE string,
due TYPE d,
open TYPE abap_bool,
END OF ty_s_invoice.
TYPES ty_t_invoice TYPE STANDARD TABLE OF ty_s_invoice WITH EMPTY KEY.
DATA t_invoices TYPE ty_t_invoice.
DATA t_overdue TYPE ty_t_invoice.
DATA key_date TYPE d.
METHODS data_read.
METHODS overdue_calc.
PROTECTED SECTION.
DATA client TYPE REF TO z2ui5_if_client.
METHODS view_display.
PRIVATE SECTION.
ENDCLASS.
CLASS zcl_app_overdue IMPLEMENTATION.
METHOD z2ui5_if_app~main.
me->client = client.
CASE abap_true.
WHEN client->check_on_init( ).
key_date = sy-datum.
data_read( ).
overdue_calc( ).
view_display( ).
WHEN client->check_on_navigated( ).
view_display( ).
WHEN client->check_on_event( `RECALC` ).
overdue_calc( ).
ENDCASE.
ENDMETHOD.
METHOD data_read.
" in your system: SELECT FROM the invoice table
t_invoices = VALUE #( ( invoice = `4711` due = `20260801` open = abap_true )
( invoice = `4712` due = `20260930` open = abap_true )
( invoice = `4713` due = `20260701` open = abap_false ) ).
ENDMETHOD.
METHOD overdue_calc.
t_overdue = VALUE #( FOR inv IN t_invoices
WHERE ( open = abap_true AND due < key_date )
( inv ) ).
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`
)->ele( `Page`
)->a( n = `title` v = `Overdue`
)->tag( `DatePicker`
)->a( n = `value` v = client->_bind( key_date )
)->a( n = `valueFormat` v = `yyyyMMdd`
)->a( n = `displayFormat` v = `medium`
)->a( n = `change` v = client->_event( `RECALC` )
)->ele( `List`
)->a( n = `items` v = client->_bind( t_overdue )
)->ele( `items`
)->tag( `StandardListItem`
)->a( n = `title` v = `{INVOICE}`
)->a( n = `info` v = `{DUE}` ).
client->view_display( view->stringify( ) ).
ENDMETHOD.
ENDCLASS.The test lives in the include, and nothing in it is abap2UI5:
CLASS ltcl_overdue DEFINITION FINAL
FOR TESTING RISK LEVEL HARMLESS DURATION SHORT.
PRIVATE SECTION.
METHODS only_open_and_past_due FOR TESTING.
ENDCLASS.
CLASS ltcl_overdue IMPLEMENTATION.
METHOD only_open_and_past_due.
DATA(cut) = NEW zcl_app_overdue( ).
cut->key_date = `20260901`.
cut->data_read( ).
cut->overdue_calc( ).
cl_abap_unit_assert=>assert_equals(
act = lines( cut->t_overdue )
exp = 1
msg = `4711 is the only open invoice past the key date` ).
ENDMETHOD.
ENDCLASS.No client, no HTTP, no browser. The test sets attributes, calls a method, reads attributes — the same test it would be for a class with no screen at all, because up to view_display( ) it is a class with no screen at all.
Two things are worth keeping that way on purpose. Handler methods take their input from attributes rather than from client->get( ), so a test can set the input. And data_read( ) is its own method, so a test can either run the real SELECT against test data or fill the table by hand and test the logic alone.
The view itself has a different kind of check. The linter reconstructs the XML the chain builds and holds it against the UI5 metadata — unknown control, misspelled property, wrong type — without a system. Logic in ABAP Unit, view in the linter, and the roundtrip in a browser.
Keep the client out of the logic, and the logic is testable the way any ABAP class is.
Happy ABAPing! 🦖🦕🦣
