diff --git a/docs/check_documentation.md b/docs/check_documentation.md index b43627a6..c1a0bd97 100644 --- a/docs/check_documentation.md +++ b/docs/check_documentation.md @@ -61,3 +61,4 @@ - [Text Assembly](checks/text-assembly.md) - [Unit-Test Coverages (Statement, Branch and Procedure)](checks/unit-test-coverages.md) - [Unit-Test Assert Validator](checks/unit_test_assert.md) +- [Unit-Test Missing Assert](checks/unit_test_missing_assert.md) diff --git a/docs/checks/unit_test_missing_assert.md b/docs/checks/unit_test_missing_assert.md new file mode 100644 index 00000000..bd97803d --- /dev/null +++ b/docs/checks/unit_test_missing_assert.md @@ -0,0 +1,60 @@ +[code pal for ABAP](../../README.md) > [Documentation](../check_documentation.md) > [Unit-Test Missing Assert](unit_test_missing_assert.md) + +## Unit-Test Missing Assert + +### What is the intent of the check? + +This check identifies unit test methods that do not include any assertions while utilizing the `CL_ABAP_UNIT_ASSERT` class. A unit test that lacks any assertions or validation of expected behavior is fundamentally meaningless. + +### How does the check work? + +The check evaluates each unit test method to determine whether at least one occurrence of the `CL_ABAP_UNIT_ASSERT` class is present. A finding is raised if no static methods of `CL_ABAP_UNIT_ASSERT` have been implemented. + +### How to solve the issue? + +Please include a meaningful assertion using `ASSERT...`, or invoke `SKIP` or `FAIL` to automate a specific behavior of the test method. + +### What to do in case of exception? + +Currently, there is no pseudo comment available to disable the check. + +### Example + +Before the check: + +```abap +METHODS initialize FOR TESTING. + +... + + METHOD initialize. + + f_cut = NEW #( ). + + ENDMETHOD. +``` + +After the check: + +```abap +METHODS initialize FOR TESTING. + +... + + METHOD initialize. + f_cut = NEW #( ). + + cl_abap_unit_assert=>assert_bound( + act = f_cut + msg = 'Instance was not created. Why?' ). + ENDMETHOD. +``` + +### Further Readings & Knowledge + +* [Clean ABAP: Don't obsess about coverage](https://github.com/SAP/styleguides/blob/main/clean-abap/CleanABAP.md#dont-obsess-about-coverage) +* [Unit testing with ABAP unit](https://help.sap.com/docs/SAP_S4HANA_CLOUD/25cf71e63940453397a32dc2b7676947/08c60b52cb85444ea3069779274b43db.html?q=abap%20unit%20test) + + + + diff --git a/src/checks/y_check_missing_ut_assert.clas.abap b/src/checks/y_check_missing_ut_assert.clas.abap new file mode 100644 index 00000000..d919bf02 --- /dev/null +++ b/src/checks/y_check_missing_ut_assert.clas.abap @@ -0,0 +1,114 @@ +CLASS y_check_missing_ut_assert DEFINITION + PUBLIC + INHERITING FROM y_check_base + CREATE PUBLIC. + + PUBLIC SECTION. + + METHODS constructor. + + PROTECTED SECTION. + METHODS inspect_tokens REDEFINITION. + + PRIVATE SECTION. + + METHODS is_testing_method + IMPORTING method_name TYPE string + RETURNING VALUE(is_testing_method) TYPE abap_bool. +ENDCLASS. + + +CLASS y_check_missing_ut_assert IMPLEMENTATION. + + METHOD constructor. + super->constructor( ). + + settings-disable_threshold_selection = abap_true. + settings-disable_on_testcode_selection = abap_true. + settings-disable_on_prodcode_selection = abap_true. + settings-apply_on_productive_code = abap_false. + settings-apply_on_test_code = abap_true. + settings-threshold = 0. + settings-prio = c_warning. + settings-documentation = |{ c_docs_path-checks }unit_test_missing_assert.md|. + settings-pseudo_comment = '"#EC CI_MISS_ASSERT'. + + relevant_statement_types = VALUE #( ( scan_struc_stmnt_type-class_definition ) + ( scan_struc_stmnt_type-method ) ). + + relevant_structure_types = VALUE #( ). + + set_check_message( 'Missing assertion in unit test method "&1"' ). + ENDMETHOD. + + + METHOD inspect_tokens. + + " Check if method part of a class FOR TESTING + CHECK is_test_code( statement ). + + " Start at each method + CHECK get_token_abs( statement-from ) = 'METHOD'. + + " Check method on detail + DATA(token_index) = statement-from. + DATA(method_name) = get_token_abs( token_index + 1 ). + + " Check if method is marked as FOR TESTING + IF NOT is_testing_method( method_name ). + RETURN. + ENDIF. + + " Check inside method + LOOP AT ref_scan->tokens ASSIGNING FIELD-SYMBOL() + FROM token_index. + IF to_upper( -str ) = 'ENDMETHOD'. + EXIT. + ENDIF. + + IF to_upper( -str ) CP |CL_ABAP_UNIT_ASSERT=>*|. + " If at least one ASSERT was found, all fine we can leave here. + RETURN. + ENDIF. + token_index = token_index + 1. + ENDLOOP. + + " Up to here now valid coding was found, so finding has to be raised. + DATA(check_configuration) = detect_check_configuration( statement ). + + raise_error( statement_level = statement-level + statement_index = index + statement_from = statement-from + check_configuration = check_configuration + parameter_01 = |{ method_name }| ). + + ENDMETHOD. + + + METHOD is_testing_method. + + IF method_name IS INITIAL. + is_testing_method = abap_false. + RETURN. + ENDIF. + + LOOP AT ref_scan->tokens ASSIGNING FIELD-SYMBOL() WHERE str = 'METHODS'. + " Safeguarding boundaries + IF lines( ref_scan->tokens ) < sy-tabix + 3. + CONTINUE. + ENDIF. + + IF ( to_upper( -str ) = 'METHODS' ) + AND ( to_upper( ref_scan->tokens[ sy-tabix + 1 ]-str ) = method_name ) + AND ( to_upper( ref_scan->tokens[ sy-tabix + 2 ]-str ) = 'FOR' ) + AND ( to_upper( ref_scan->tokens[ sy-tabix + 3 ]-str ) = 'TESTING' ). + is_testing_method = abap_true. + RETURN. + ENDIF. + ENDLOOP. + + is_testing_method = abap_false. + + ENDMETHOD. + +ENDCLASS. diff --git a/src/checks/y_check_missing_ut_assert.clas.testclasses.abap b/src/checks/y_check_missing_ut_assert.clas.testclasses.abap new file mode 100644 index 00000000..a71b4164 --- /dev/null +++ b/src/checks/y_check_missing_ut_assert.clas.testclasses.abap @@ -0,0 +1,64 @@ +CLASS local_test_class DEFINITION INHERITING FROM y_unit_test_base FOR TESTING RISK LEVEL HARMLESS DURATION SHORT. + PROTECTED SECTION. + METHODS get_cut REDEFINITION. + METHODS get_code_with_issue REDEFINITION. + METHODS get_code_without_issue REDEFINITION. + METHODS get_code_with_exemption REDEFINITION. +ENDCLASS. + + +CLASS local_test_class IMPLEMENTATION. + + METHOD get_cut. + result ?= NEW y_check_missing_ut_assert( ). + ENDMETHOD. + + + METHOD get_code_with_issue. + result = VALUE #( ( 'REPORT y_example. ' ) + ( ' CLASS y_example_class DEFINITION FOR TESTING. ' ) + ( ' PUBLIC SECTION. ' ) + ( ' PROTECTED SECTION. ' ) + ( ' METHODS test FOR TESTING. ' ) + ( ' ENDCLASS. ' ) + + ( ' CLASS y_example_class IMPLEMENTATION. ' ) + ( ' METHOD test. ' ) + ( ' RETURN. ' ) + ( ' ENDMETHOD. ' ) + ( ' ENDCLASS. ' ) ). + ENDMETHOD. + + + METHOD get_code_without_issue. + result = VALUE #( ( 'REPORT y_example. ' ) + ( ' CLASS y_example_class DEFINITION FOR TESTING. ' ) + ( ' PUBLIC SECTION. ' ) + ( ' PROTECTED SECTION. ' ) + ( ' METHODS test FOR TESTING. ' ) + ( ' ENDCLASS. ' ) + + ( ' CLASS y_example_class IMPLEMENTATION. ' ) + ( ' METHOD test. ' ) + ( ' CL_ABAP_UNIT_ASSERT=>ASSERT_BOUND( act = me ). ' ) + ( ' ENDMETHOD. ' ) + ( ' ENDCLASS. ' ) ). + ENDMETHOD. + + + METHOD get_code_with_exemption. + result = VALUE #( ( 'REPORT y_example. ' ) + ( ' CLASS y_example_class DEFINITION FOR TESTING. ' ) + ( ' PUBLIC SECTION. ' ) + ( ' PROTECTED SECTION. ' ) + ( ' METHODS test FOR TESTING.' ) + ( ' ENDCLASS. ' ) + + ( ' CLASS y_example_class IMPLEMENTATION. ' ) + ( ' METHOD test. "#EC CI_MISS_ASSERT ' ) + ( ' RETURN. ' ) + ( ' ENDMETHOD. ' ) + ( ' ENDCLASS. ' ) ). + ENDMETHOD. + +ENDCLASS. diff --git a/src/checks/y_check_missing_ut_assert.clas.xml b/src/checks/y_check_missing_ut_assert.clas.xml new file mode 100644 index 00000000..3dd8bedc --- /dev/null +++ b/src/checks/y_check_missing_ut_assert.clas.xml @@ -0,0 +1,24 @@ + + + + + + Y_CHECK_MISSING_UT_ASSERT + E + Missing CL_ABAP_UNIT_ASSERT usage at test method + 1 + X + X + X + X + + + + IS_TESTING_METHOD + E + Check if given method is marked FOR TESTING + + + + +