Knobelaufgabe ( Sommer 2022 ) - Unit Test Coverage

Alles Rund um SAP®.
6 Beiträge • Seite 1 von 1
6 Beiträge Seite 1 von 1

Knobelaufgabe ( Sommer 2022 ) - Unit Test Coverage

Beitrag von black_adept (Top Expert / 3953 / 105 / 886 ) »
Moin allerseits,
wie inzwischen üblich bin ich über ein Problem gestolpert, dessen Lösung mich dann inspiriert hat daraus eine Knobelaufgabe für dieses Forum zu erstellen.
Diesmal geht es um Unit Tests - und hier ganz speziell um die Codeabdeckung der Tests.

Das ist eure Aufgabe: Es soll beim Aufruf der Unittests mit Coverage ( aus der GUI heraus - wie das aus Eclipse geht kann ich nicht sagen. Evtl. kann das jemand hier ergänzen ) aus dem Menü via Program->Execute->Unit Tests with->Coverage möglichst viel des Codes durch Testfälle abgedeckt werden. Und zwar sollen alle 3 Coveragetypen berücksichtigt werden. Branch-, Procedure- und Statementcoverage.
Im Programm ist eine Demotestklasse "lcl_coverage_test". Diese kann von euch modifiziert werden oder ihr könnt auch eigene Testklasse(n) und Testmethode(n) erstellen. Ihr dürft nur nicht die zu testende Klasse lcl_test verändern, außer dass ihr eigene Testklassen als befreundet deklarieren dürft.
Da die Methoden der in dem Programm vorhandenen Klasse nicht sonderlich sinnvoll sind, ist es auch nicht eure Aufgabe, die Funktion der jeweiligen Methoden zu überprüfen. Also kein Aufruf der CL_ABAP_UNIT_ASSERT=>xxx Methoden notwendig. Es geht tatsächlich nur um maximale Codeabdeckung.


Das hier ist das Programm, welches mit mehr Codeabdeckung erzeugenden Unittests versehen werden soll:

Code: Alles auswählen.

REPORT zknobel_coverage.

CLASS lcx DEFINITION INHERITING FROM cx_static_check.
ENDCLASS.

*--------------------------------------------------------------------*
* Class to test -  no changes allowed here
*--------------------------------------------------------------------*
CLASS lcl_coverage_test DEFINITION DEFERRED.
CLASS lcl_test DEFINITION FINAL FRIENDS lcl_coverage_test.
  PUBLIC SECTION.
    METHODS: constructor IMPORTING iv_seed  TYPE i
                                   iv_max   TYPE i
                                   iv_count TYPE i
                         RAISING   lcx,
      get_odd_max RETURNING VALUE(rv_max) TYPE i,
      get_log_sum RETURNING VALUE(rv_sum) TYPE f.

    DATA: mo_random          TYPE REF TO cl_abap_random,
          mt_numbers         TYPE STANDARD TABLE OF i WITH NON-UNIQUE DEFAULT KEY,
          mt_r_odd           TYPE RANGE OF i,
          mv_range_is_filled TYPE abap_bool.
ENDCLASS.

*--------------------------------------------------------------------*
* Unit testclass - you may change as much as you like
*--------------------------------------------------------------------*
CLASS lcl_coverage_test DEFINITION FOR TESTING  RISK LEVEL HARMLESS DURATION SHORT.
  PRIVATE SECTION.
    METHODS check_coverage FOR TESTING.
ENDCLASS.


CLASS lcl_test IMPLEMENTATION.

  METHOD constructor.
    IF iv_count < 0.
      RAISE EXCEPTION TYPE lcx.
    ENDIF.
    mo_random = cl_abap_random=>create( iv_seed ).
    mv_range_is_filled = abap_false.
    APPEND VALUE #( sign = 'I' option = 'EQ' low = 1 ) TO mt_r_odd.
    DO iv_count TIMES.
      APPEND mo_random->intinrange( low  = 0
                                    high = iv_max
                                  ) TO mt_numbers ASSIGNING FIELD-SYMBOL(<lv_number>).
      IF <lv_number> MOD 2 = 1.
        APPEND VALUE #( sign = 'I' option = 'EQ' low = <lv_number> ) TO mt_r_odd.
      ENDIF.
    ENDDO.
    SORT mt_r_odd.
    DELETE ADJACENT DUPLICATES FROM mt_r_odd.

    IF lines( mt_r_odd ) > 0.
      mv_range_is_filled = abap_true.
    ENDIF.
  ENDMETHOD.

  METHOD get_odd_max.

    DATA: rr_data TYPE REF TO data.
    FIELD-SYMBOLS: <lv_min_i> TYPE i.

    rr_data = cl_abap_exceptional_values=>get_min_value( rv_max ).
    ASSIGN rr_data->* TO <lv_min_i>.
    rv_max = <lv_min_i>.

    IF lines( mt_numbers ) = 0
      OR mv_range_is_filled = abap_false.
      RETURN.
    ENDIF.

    LOOP AT mt_numbers ASSIGNING FIELD-SYMBOL(<lv_number>) WHERE table_line IN mt_r_odd.
      IF <lv_number> > rv_max.
        rv_max = <lv_number>.
      ENDIF.
    ENDLOOP.

  ENDMETHOD.

  METHOD get_log_sum.

    LOOP AT mt_numbers ASSIGNING FIELD-SYMBOL(<lv_number>).
      TRY.
          rv_sum = rv_sum + 1 / <lv_number>.
        CATCH cx_root.
          CONTINUE.
      ENDTRY.
    ENDLOOP.

  ENDMETHOD.

ENDCLASS.


*--------------------------------------------------------------------*
* Unzureichende Unit tests
*--------------------------------------------------------------------*
CLASS lcl_coverage_test IMPLEMENTATION.
  METHOD check_coverage.
    DATA(lo_test) = NEW lcl_test( iv_seed  = 0
                                  iv_max   = 10000
                                  iv_count = 10
                                ).
    lo_test->get_odd_max( ).
    lo_test->get_log_sum( ).
  ENDMETHOD.
ENDCLASS.
Der Code der zu prüfenden Klasse ist sicherlich optimierbar. Aber dieser Code sollte bitte nicht angepasst werden sondern die Unittests sollen so auf ihn losgelassen werden.

Die von mir im Programm schon vorgegebene Demolösung mit der Klasse "lcl_coverage_test" bietet übrigens folgende Codeabdeckungen.
  • Branch Coverage: 52,38%
  • Procedure Coverage: 75,00%
  • Statement Coverage: 79,31%
Da ist was machbar!

P.S. Vielen Dank an Enno, mit dem ich mein Problem diskutieren konnte, denn diese Diskussion hat schlussendlich zu dieser Knobelaufgabe geführt. Da er auch als Betatester für diese Aufgabe herhalten musste, darf er erst später in den Wettbewerb oder die dann folgende Diskussion eingreifen.
live long and prosper
Stefan Schmöcker

email: stefan@schmoecker.de

gesponsert
Stellenangebote auf ABAPforum.com schalten
kostenfrei für Ausbildungsberufe und Werksstudenten


Re: Knobelaufgabe ( Sommer 2022 ) - Unit Test Coverage

Beitrag von a-dead-trousers (Top Expert / 4295 / 214 / 1146 ) »
hi.

Ich komm mit vier Aufrufvarianten auf eine Abdeckung von 80%/100%/100%. Nur die Klasse "LCL_TEST" berücksichtigt und den Abschnitt "START-OF-SELECTION:00" nicht mitgezählt.
(Details siehe PM)

lg ADT
Theory is when you know something, but it doesn't work.
Practice is when something works, but you don't know why.
Programmers combine theory and practice: Nothing works and they don't know why.

ECC: 6.18
Basis: 7.50

Re: Knobelaufgabe ( Sommer 2022 ) - Unit Test Coverage

Beitrag von ewx (Top Expert / 4789 / 295 / 629 ) »
Danke für die weitere Knobelaufgabe, Stefan!!
Ich melde mich jedoch schon mal vorher zu Wort. Wegen des Vorgehens in Eclipse:

Einmal STRG-SHIFT-F10 (wie im GUI auch), um die Unit Tests durchzuführen.
Dann in der Registerkarte ABAP Unit rechts auf das Play-Symbol klicken und "Rerun with Coverage" auswählen.
SNAG-0053.png
Zwischen Statement Coverage, Branch Coverage und Procedure Coverage schaltet man in dem Drei-Punkte-Menü in der Registerkarte ABAP Coverage um.

Die Markierung im Quelltext schaltet man mit dem Grün-Roten-Symbol daneben um.
SNAG-0054.png
Als Short cut kann man des Weiteren CTRL-SHIFT-F11 verwenden, dann werden die Unit Test inklusive Code Coverage ausgeführt.

Folgende Benutzer bedankten sich beim Autor ewx für den Beitrag (Insgesamt 4):
black_adepta-dead-trousersTronMurdock


Re: Knobelaufgabe ( Sommer 2022 ) - Unit Test Coverage

Beitrag von jocoder (Specialist / 339 / 3 / 101 ) »
Zur Info für andere Teilnehmer, bevor diese an der 100%-Hürde verzweifeln:
100% Zweigabdeckung wird nicht möglich sein. Die maximale Zweigabdeckung der Klasse lcl_test dürfte 90% sein.
Bild

Folgende Benutzer bedankten sich beim Autor jocoder für den Beitrag:
a-dead-trousers


Re: Knobelaufgabe ( Sommer 2022 ) - Unit Test Coverage

Beitrag von a-dead-trousers (Top Expert / 4295 / 214 / 1146 ) »
Danke für den Screenshot.
Jetzt bin ich auch bei 90%/100%/100%
Theory is when you know something, but it doesn't work.
Practice is when something works, but you don't know why.
Programmers combine theory and practice: Nothing works and they don't know why.

ECC: 6.18
Basis: 7.50

Re: Knobelaufgabe ( Sommer 2022 ) - Unit Test Coverage

Beitrag von black_adept (Top Expert / 3953 / 105 / 886 ) »
Moin allerseits,
ca 1,5 Wochen sind vergangen und es haben sich gerade mal 2 Wettbewerber gefunden. Liegt's daran, dass Ferienzeit ist, ist das Thema zu exotisch oder war die Aufgabe zu leicht/schwer? Ist auch egal - ich erkläre jetzt einfach was das Ziel dieser Knobelaufgabe war.
Ich glaube der einfachste Weg ist kurz zu erklären was gemacht wird, und dann baue ich sukzessive eine maximale Lösung auf mit Erklärungen warum was gemacht wird.

Zunächst einmal hat das zu prüfende Programm offensichtlich 2 Bereiche. Den (implizt vorhandenen) Zeitpunkt START-OF-SELECTION und die Klasse lcl_coverage_test. Die "Probanden" habe alle nur die Coverage der Klasse getestet, aber ein einfaches

Code: Alles auswählen.

* Main program
    submit (sy-repid) and return.
sorgt dafür, dass das Rahmenprogramm als vollständig geprüft gilt, einfach weil der Zeitpunkt START-OF-SELECTION erreicht wird und dort einfach nichts passiert.

Aber der interessante Teil ist ja schließlich das Testen der Klasse und alle Prozentzahlen, die ich ab hier liefere gelten für nur für diese Klasse und nicht für das gesamte Programm.
Wir tasten uns jetzt sukzessive an die optimale Lösung heran.
Prozedurabdeckung/Procedure coverage
Die Demolösung hat einen ersten Ansatz gegeben welches die Klasse mit ein paar Daten erzeugt und dann die beiden Methoden aufruft. Da hiermit alle in der Klasse vorhandenen Methoden aufgerufen wurden erreichen wir schon eine Prozedurabdeckung von 100%. Das war einfach.

Anweisungsabdeckung/Statement coverage
Allerdings hat dieser simple Ansatz bisher erst 93%,67%,67% Anweisungsabeckung für Constructor, get_log_sum und get_odd_max. Ein Doppelklick auf die jeweilige Methode offenbart, welche Befehle bisher nicht erreicht wurden.
  • Constructor - es fehlt das "RAISE EXCEPTION" in Zeile 38
  • GET_LOG_SUM - Es fehlt das CONTINUE auf Zeile 87 im Catch-Block
  • GET_ODD_MAX : Es fehlt das RETURN in Zeile 70 und der IF-Block in Zeile 74
Die RAISE-Bedingung im Konstruktor wird erreicht, wenn iv_count < 0 ist, somit fügen wir den Unit tests folgende Zeilen hinzu:

Code: Alles auswählen.

    TRY.
        NEW lcl_test( iv_seed = 0
                      iv_max  = 10
                      iv_count = - 100
                    ).
      CATCH cx_root.
    ENDTRY.
Für die Methode GeT_LOG_SUM muss irgendwas im TRY-CATCH Block schief gehen, damit wir in den CATCH-Block gelangen. Und wenn ich arithmethische Anweisungen sehe fallen mir immer "Teilen durch Null" und "Überlaufprobleme" ein. Wir haben hier ein "geteilt" in der Anweisung rv_sum = rv_sum + 1 / <lv_number>. Wir müssen also dafür sorgen, dass in der Zahlenliste eine 0 auftauchte. Das ist am einfachsten machbar, indem wir lv_max im Konstruktor auf 0 setzen -dann sind alle Zahlen der Liste = 0. Alternativ können wir auch die Anzahl der Einträge viel größer machen als die Anzahl die möglichen Listeinträge. Dann wird es sehr wahrscheinlich, dass einer der Einträge = 0 ist und wenn man ein wenig mit dem Seed rumspielt sollte man auch so einen "ehrlicheren" Testfall erzeugen. Aber wir wählen den dreisten Ansatzu und fügen dem Unit test folgenden Aufruf hinzu:

Code: Alles auswählen.

* Log Sum --> CATCH-Block
     NEW lcl_test( iv_seed     = 0
                      iv_max   = 0
                      iv_count = 1
                    )->get_log_sum( ).
Das "RETURN" in der Methode GET_ODD_SUM wird erreicht, wenn die Tabelle MT_NUMBERS leer ist oder mv_range_is_filled abap_false. Wenn man sich den Konstruktor genauer anschaut wird man feststellen, dass letztere Bedingung nie erfüllt werden wird, somit muss wohl die 1. Bedingung erfüllt werden: Die Tabelle mt_numbers muss leer sein --> iv_count = 0 welches zu folgendem Aufruf führt

Code: Alles auswählen.

* get_odd_max:  RETURN
    NEW lcl_test( iv_seed = 0
                  iv_max  = 10
                  iv_count = 0
                )->get_odd_max( ).
Jetzt muss "nur" noch irgendwie der Tabellenkörper des LOOPS in der Methode GET_ODD_SUM erreicht werden. D.h, die WHERE Bedingung "WHERE table_line IN mt_r_odd" muss erfüllt sein. Und hier ist mir ein Fehler unterlaufen, da die Range mt_r_odd immer nur aus einer einzigen Zeile besteht. Gedacht war eigentlich, dass sie alle ungeraden Zahlen aufnimmt, aber ein Flüchtgkeitsfehler im Constructor, der diese Range erstellt, wo ich vergessen habe, dass SORT Zahlenfelder ignoriert wenn diese nicht explizit angegeben werden führt dazu, dass mehr oder minder zufällig irgend eine der Zeilen der Tabelle mt_sort übrig bleibt. Wie dem auch sei - auch hier kann man mit einem einfachen Ansatz weiterkommen. Wir brauchen eine ungerade Zahl in der Liste, welche mit einer der ungeraden Zahlen der Range übereinstimmt. Da im Konstruktor explizit die Zahl 1 in die Range eingefügt wird können wir im Konstruktor mit dem Parameter iv_max = 1 dafür sorgen, dass wir nur die 0 und 1 in der Zahlenliste haben. Dass die Zahl 1 auch tatsächlich in der Zahlenliste ist bewerkstelligen wir, indem wir count hoch ansetzen ( z.B. 10000 ). Dann haben wir 10000 Zahlen in der Liste von denen ~ die Hälfte 0 und die andere Hälfte 1 ist. Die Wahrscheinlichkeit, dass alles Nullen sind, ist hinreichend gering. --> Der folgende Unit-Test sorgt für Abhilfe

Code: Alles auswählen.

* get_odd_max: --> Body of LOOP
    NEW lcl_test( iv_seed  = 0
                  iv_max   = 1
                  iv_count = 10000
                )->get_odd_max( ).
Zweigabdeckung/ Branch coverage
Die ganzen obigen Tests haben auch die Zweigabdeckung schon recht weit nach oben getrieben und wir haben aktuell 89%,75%,100% Zweigabeckung für Constructor, get_log_sum und get_odd_max
Die Frage ist nur: Wo zum Geier sind die fehlenden Prozente im Constructor und get_log_sum, wo doch alle Programmzeilen durchlaufen wurden?

Und hier haben wir schon die erste Merkwürdigkeit: In der Methode GET_LOG_SUM ist keine Bedingung, welche durch einen fehlenden Zweig nicht erfasst wurde, aber die Zweigabdeckung behauptet, dass nur 3 von 4 Zweigen durchlaufen wurden. Hier habe ich nach längerer Diskussion mit Enno begriffen, dass ein LOOP / ENDLOOP zwei Zweige darstellt. Einer, wenn der LOOP-Körper erreicht wird, der andere wenn nicht. Wenn also der LOOP gar nicht durchlaufen wird, wird der noch fehlende Zweig abgehandelt. Da hier keine Bedingung angegeben ist, muss dafür die Tabelle, über die die Schleife läuft leer sein --> count = 0 im Konstruktor führt zu folgendem Unittest, mit dem die Zweigabdeckung auch dieser Methode auf 100% geht:

Code: Alles auswählen.

* Log Sum --> Do not enter loop
    NEW lcl_test( iv_seed = 0
                  iv_max  = 10
                  iv_count = 0
                )->get_log_sum( ).

Bleibt noch der Konstruktor: Wo ist der fehlende Zweig? Das Problem ist die letzte IF-Bedingung in Zeile 54. das IF lines( mt_r_odd ) > 0 ist immer erfüllt, weil wir in Zeile 42 eine Zeile in diese Tabelle einfügen. Und die Zweigabdeckung zählt jeden Ausgang einer Bedingung und ob dieser in mind. einem der Unittests erfüllt wurde. Durch die Programmkonstellation im Konstruktor kann das hier nie erfüllt werden, so dass die Zweigabdeckung des Konrtuktors nicht höher werden kann. Wenn man jetzt behauptet, dass man in diesem Fall ja die Bedingung in Zeile 54 rauswerfen kann , weil die IF-Bedingung de facto immer TRUE ist. Wenn man die Zeilen 54 und 56 auskommentiert, kommt man auf die 100% Zweigabdeckung, aber das Ändern des Originalcodes war ja explizit ausgeschlossen worden.


Was bedeutet das für Unitests und den eigenen Programmierstil: Anweisungskombinationen wie

Code: Alles auswählen.

IF table is not initial. LOOP at TABLE... ohne Bedingung
führen dazu dass der Tabellenkörper immer durchlaufen wird, was zu einer reduzierten Codeabdeckung führt. Desweiteren führen auch Anweisungen wie if 1 = 0 oder if 1 = 1. ( z.B. für Cross-Reference ) dazu, dass in diese Bedingung entweder immer Wahr oder immer Falsch ist, so dass nie beide Zweige durchlaufen werden können. Und beides sind Techniken, die man hier und da ob der besseren Wart/Lesbarkeit des Codes durchaus einfügt.
Und schlussendlich ist es extrem schwer in einem etwas längeren Coding die Stellen zu finden, wo ein Zweig nicht durchlaufen wurde. Denn wer weiß schon, was SAP als Zweig zählt.
Für die Fleißbienen hier im Forum: Probiert doch mal aus, ob "DELETE ADJACENT DUPLICATES FROM..." oder "DELETE itab WHERE" so wie LOOP implizit einen Branch aufmachen?

Folgende Benutzer bedankten sich beim Autor black_adept für den Beitrag (Insgesamt 4):
ewxa-dead-trousersqyurryusTron

live long and prosper
Stefan Schmöcker

email: stefan@schmoecker.de

Seite 1 von 1

Vergleichbare Themen

3
Antw.
294
Views
Knobelaufgabe ( Sommer 2023 ) - Robuster Programmablauf
von black_adept » 26.06.2023 12:51 • Verfasst in SAP - Allgemeines
12
Antw.
698
Views
Knobelaufgabe (April 2022) - Laufzeit XSTRING
von black_adept » 11.04.2022 09:35 • Verfasst in SAP - Allgemeines
2
Antw.
354
Views
Unit test für Adobe Forms
von Lucyalison » 11.03.2022 13:44 • Verfasst in ABAP® Core
3
Antw.
281
Views
Wie viel Unit-Test darf es denn sein?
von der_neuling » 14.06.2022 15:25 • Verfasst in ABAP® für Anfänger
0
Antw.
449
Views

Newsletter Anmeldung

Keine Beiträge verpassen! Wöchentlich versenden wir lesenwerte Beiträge aus unserer Community.
Die letzte Ausgabe findest du hier.
Details zum Versandverfahren und zu Ihren Widerrufsmöglichkeiten findest du in unserer Datenschutzerklärung.