diff --git a/14_ABAP_Unit_Tests.md b/14_ABAP_Unit_Tests.md new file mode 100644 index 0000000..18052bc --- /dev/null +++ b/14_ABAP_Unit_Tests.md @@ -0,0 +1,597 @@ + + +# A Glimpse on ABAP Unit Tests + +- [A Glimpse on ABAP Unit Tests](#a-glimpse-on-abap-unit-tests) + - [Unit Tests in ABAP](#unit-tests-in-abap) + - [High-Level Steps for ABAP Unit Tests](#high-level-steps-for-abap-unit-tests) + - [Creating Test Classes](#creating-test-classes) + - [Creating and Implementing Test Methods](#creating-and-implementing-test-methods) + - [Special Methods for Implementing the Test Fixture](#special-methods-for-implementing-the-test-fixture) + - [Handling Dependencies](#handling-dependencies) + - [Test Seams](#test-seams) + - [Running and Evaluating ABAP Unit Tests](#running-and-evaluating-abap-unit-tests) + - [More information](#more-information) + - [Executable Example](#executable-example) + + +This cheat sheet contains basic information about [unit testing](https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/index.htm?file=abenunit_test_glosry.htm) in ABAP. + +> **💡 Note**
+> - This cheat sheet focuses on testing methods. +> - See the [More Information](#-more-information) section for links to more in-depth information. +> - The executable examples are **not** suitable role models for ABAP unit tests. They are intended to give you a rough idea. You should always work out your own solution for each individual case. + +## Unit Tests in ABAP +- Unit tests ... + - ensure the functional correctness of individual software units (i.e. a unit of code whose execution has a verifiable effect). + - are designed to test that the individual components of a larger software unit work correctly during the development and quality assurance phases. Typically, such individual software units are methods (the focus of this cheat sheet). + - must be created and run by developers. +- In ABAP, developers have [ABAP Unit](https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/index.htm?file=abenabap_unit_glosry.htm) - a test tool integrated into the ABAP runtime framework - at their disposal. It can be used to run individual or mass tests, and to evaluate test results. Note that comprehensive test runs can be performed using the [ABAP Test Cockpit](https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/index.htm?file=abenabap_test_cockpit_glosry.htm) +- In ABAP programs, individual unit tests are implemented as [test methods](https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/index.htm?file=abentest_method_glosry.htm) of local [test classes](https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/index.htm?file=abentest_class_glosry.htm). + +

(back to top)

+ +## High-Level Steps for ABAP Unit Tests + +- Identify dependend-on components (DOC) in your production code (i.e. your class/method) that need to be tested, and prepare the code for testing. + - Examples of DOCs: In your production code, a method is called that is outside of your code. Or, for example, whenever there is an interaction with the database. + - This means that there are dependencies that need to be taken into account for a unit test. These dependencies should be isolated and replaced by a test double. +- Create/Implement test doubles + - You can create test doubles manually. For example, you hardcode the test data to be used when the test is executed. Note: Automatic creation is not covered here. + - Ideally, the testability of your code has been prepared by providing interfaces to the DOC. Interfaces facilitate the testability because you can simply implement the interface methods. +- Inject the test doubles to ensure that the test data is used during the test run. +- Create test classes and methods +- Run unit tests + +> **💡 Note**
+> Of course, if there are no dependend-on components in your production code, you can skip the considerations of dependency isolation, test doubles and their injection into the production code. + +

(back to top)

+ +## Creating Test Classes + +Before we look at test doubles and injections, we will look at the creation of the test classes and methods to get an idea of the code skeletons (in which test doubles and injections can be implemented). + +Test classes ... +- are special [local](https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/index.htm?file=abenlocal_class_glosry.htm) or [global classes](https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/index.htm?file=abenglobal_class_glosry.htm) in which tests for ABAP Unit are implemented in the form of test methods. Note: In this cheat sheet, the focus is on local test classes only. +- are created in [class pools](https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/index.htm?file=abenclass_pool_glosry.htm) in special [test includes](https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/index.htm?file=abentest_include_glosry.htm). See the *Test Classes* tab in the ADT. +- can only be used as part of test runs. +- are not generated in production systems, i.e. the source code of a test class is not part of the production code of its program. +- can contain test methods, the special methods for the [fixture](https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/index.htm?file=abenfixture_glosry.htm), and other components. + - It is recommended that all components required for ABAP unit tests are defined in test classes only (so that they cannot be generated in production systems and cannot be addressed by production code). The components also include test doubles and other helper classes that do not contain test methods. + + +The skeleton of a test class might look like this: +``` abap +"This code +CLASS ltc_test_class DEFINITION + FOR TESTING "Defines a class to be used in ABAP Unit + RISK LEVEL HARMLESS "Defines risk level, options: HARMLESS/CRITICAL/DANGEROUS + DURATION SHORT. "Expected test execution time, options: SHORT/MEDIUM/LONG + + ... + +ENDCLASS. + +CLASS ltc_test_class IMPLEMENTATION. + ... +ENDCLASS. +``` + +Notes: +- `FOR TESTING` can be used for multiple purposes: + - Creating a test class containing test methods + - Creating a test double + - Creating helper methods to support ABAP unit tests + - Note the possible [syntax options](https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/index.htm?file=abapclass_options.htm) before `FOR TESTING` +- Optional addition `RISK LEVEL ...`: + - `CRITICAL`: test changes system settings or customizing data (default) + - `DANGEROUS`: test changes persistent data + - `HARMLESS`: test does not change system settings or persistent data +- Optional addition `DURATION ...`: + - `SHORT`: execution time of only a few seconds is expected + - `MEDIUM`: execution time of about one minute is expected + - `LONG`: execution time of more than one minute is expected + + +To test protected or private methods, you must declare [friendship](https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/index.htm?file=abenfriend_glosry.htm) with the class to be tested (class under test). +Example: + +``` abap +"The code in this snippet refers to the test class in the test include. +"Test class, declaration part +CLASS ltc_test_class DEFINITION + FOR TESTING + RISK LEVEL HARMLESS + DURATION SHORT. + + ... + +ENDCLASS. + +"Declaring friendship +CLASS cl_class_under_test DEFINITION LOCAL FRIENDS ltc_test_class. + +"Test class, implementation part +CLASS ltc_test_class IMPLEMENTATION. + ... +ENDCLASS. +``` + +If you have multiple test classes in the test include, you can place the friendship declaration for all classes at the top, for example, as follows. Note the [`DEFERRED`](https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/index.htm?file=abapclass_deferred.htm) addition. + +``` abap +"Test include + +CLASS ltc_test_class_1 DEFINITION DEFERRED. +CLASS ltc_test_class_2 DEFINITION DEFERRED. +CLASS cl_class_under_test DEFINITION LOCAL FRIENDS ltc_test_class_1 + ltc_test_class_2. + +CLASS ltc_test_class_1 DEFINITION + FOR TESTING + RISK LEVEL HARMLESS + DURATION SHORT. + ... +ENDCLASS. + +CLASS ltc_test_class_1 IMPLEMENTATION. + ... +ENDCLASS. + +CLASS ltc_test_class_2 DEFINITION + FOR TESTING + RISK LEVEL HARMLESS + DURATION SHORT. + ... +ENDCLASS. + +CLASS ltc_test_class_2 IMPLEMENTATION. + ... +ENDCLASS. +``` + + +**Including Interfaces** +- Usually, all non-optional interface methods must be implemented. +- When you use the `PARTIALLY IMPLEMENTED` addition in test classes, you are not forced to implement all of the methods. +- It is particularly useful for interfaces to implement test doubles, and not all methods are necessary. + +Example of creating a test double: + +``` abap +"Test double class in a test include +CLASS ltd_test_double DEFINITION FOR TESTING. + PUBLIC SECTION. + INTERFACES some_intf PARTIALLY IMPLEMENTED. +ENDCLASS. + +CLASS ltd_test_double IMPLEMENTATION. + METHOD some_intf~some_meth. + ... + ENDMETHOD. +ENDCLASS. +``` + +

(back to top)

+ +## Creating and Implementing Test Methods + +Test methods ... +- are special [instance methods](https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/index.htm?file=abeninstance_method_glosry.htm) of a test class in which a test is implemented. + - As with test classes, the `FOR TESTING` addition also applies to the test method declaration. + - Note that there are other syntax options, such as [`ABSTRACT` and others](https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/index.htm?file=abapmethods_testing.htm). +- are called by the ABAP Unit framework during a test run. +- are used to call units of production code and to check the result. + - The results are checked using methods of the class `CL_ABAP_UNIT_ASSERT`. +- should be private or protected if the methods are inherited. +- are called in an undefined order. +- have no parameters. + +**Creating Test Methods** + +Example: +``` abap +"Test class in a test include +"Test class declaration part +CLASS ltc_test_class DEFINITION + FOR TESTING + RISK LEVEL HARMLESS + DURATION SHORT. + + PRIVATE SECTION. + "Note: As a further component in test classes, usually, a reference variable + "is created for an instance of the class under test. + DATA ref_cut TYPE REF TO cl_class_under_test. + + "Test method declaration + METHODS some_test_method FOR TESTING. + +ENDCLASS. + +"Test class implementation part +CLASS ltc_test_class IMPLEMENTATION. + + METHOD some_test_method. + ... "Here goes the implementation of the test. + ENDMETHOD. + +ENDCLASS. +``` + +**Implementing Test Methods** +- The implementation ideally follows the *given-when-then* pattern. + - *given*: Preparing the test, e.g. creating an instance of the class under test and a local test double (to inject test test double into the class under test) + - *when*: Calling the procedure to be tested + - *then*: Checking and evaluating the test result using the static methods of the `CL_ABAP_UNIT_ASSERT` class +- A selection of static methods of the class `CL_ABAP_UNIT_ASSERT` that can be used for the checks: + - `ASSERT_EQUALS`: Checks whether two data objects are the same. + - `ASSERT_BOUND`: Checks whether a reference variable is bound. + - `ASSERT_NOT_BOUND`: Negation of the one above. + - `ASSERT_INITIAL`: Checks whether a data object has its initial value. + - `ASSERT_NOT_INITIAL`: Negation of the one above. + - `ASSERT_SUBRC`: Checks the value of `sy-subrc`. + - `FAIL`: Triggers an error. +- For the class and methods, as well as the paramters, check the F2 information in the ADT. Depending on the method used, parameters can (or must) be specified. To name a few: + - `ACT`: of type `ANY` (in most cases), non-optional importing parameter of the methods, specifies a data object that is to be verified + - `EXP`: of type `ANY` (in most cases), a data object holding the expected value (non-optional for the `ASSERT_EQUALS` method) + - `MSG`: of type `CSEQUENCE`, error description + - `QUIT`: of type `INT1`, the specification affects the unit test flow control in case of an error, e.g. the constant `if_abap_unit_constant=>quit-no` can be used to determine that the unit test should not be terminated in case of an error + +The following code snippet shows a class declaration and implementation part in the production code. +A test method is to be created for the method. + +``` abap +"The code in this snippet refers to the production code in the global class. +"It shows a method declaration for a simple calculation in the class under test. + +"Class declaration part +CLASS cl_class_under_test DEFINITION + PUBLIC + CREATE PUBLIC. + + PRIVATE SECTION. + + METHODS: + multiply_by_two IMPORTING num TYPE i + RETURNING VALUE(result) TYPE i. + +... +"Class implementation part +CLASS cl_class_under_test IMPLEMENTATION. + +METHOD multiply_by_two. + result = num * 2. +ENDMETHOD. +... + +``` + +Example: Test method for the example method *multiply_by_two* in the production code + +As mentioned above, in simple cases, there may not be any dependend-on component in (a method of) the production code. Therefore, the functionality of the method in the production code is tested by simply comparing the value of the actual value (the value that is returned by the method, for example) and the expected value. + +``` abap +"The code in this snippet refers to the test class in the test include. +... + +"Test class, implementation part +CLASS ltc_test_class IMPLEMENTATION. + + "Test method implementation + METHOD some_test_method. + "given + "Creating an object of the class under test + "Assumption: A reference variable has been declared (DATA ref_cut TYPE REF TO cl_class_under_test.) + "A variable declared inline may not be the best choice if there is more than one + "test method in the class that also needs an object of the class under test. + "See also the setup method further down in this context. + ref_cut = NEW #( ). + "DATA(ref_cut) = NEW cl_class_under_test( ). + + "when + "Calling method that is to be tested + "As an example, 5 is inserted. The result should be 10, which is then checked. + DATA(result) = ref_cut->multiply_by_two( 5 ). + + "then + "Assertion + cl_abap_unit_assert=>assert_equals( + act = result + exp = 10 ). + +"Further optional parameters specified +* cl_abap_unit_assert=>assert_equals( +* act = result +* exp = 10 +* msg = |The result of 5 multiplied by 2 is wrong. It is { result }.| "Error message +* quit = if_abap_unit_constant=>quit-no ). "No test termination + + ENDMETHOD. + +ENDCLASS. +``` + +

(back to top)

+ +### Special Methods for Implementing the Test Fixture +- Special private methods for implementing the test [fixture](https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/index.htm?file=abenfixture_glosry.htm), which may include test data and test objects among others, can be included in the local test class. +- They are not test methods and the `FOR TESTING` addition cannot be used. +- They have no parameters. +- Instance methods: + - `setup`: Executed before each execution of a test method of a test class + - `teardown`: Executed after each execution of a test method of a test class +- Static methods + - `class_setup`: Executed once before all tests of the class + - `class_teardown`: Executed once after all tests of the class + +Example: + +``` abap +"Test class, declaration part +CLASS ltc_test_class DEFINITION + FOR TESTING + RISK LEVEL HARMLESS + DURATION SHORT. + + PRIVATE SECTION. + DATA: ref_cut TYPE REF TO cl_class_under_test. + + METHODS: + "special methods + setup, + teardown, + + "test methods + some_test_method FOR TESTING, + another_test_method FOR TESTING. + +ENDCLASS. + +"Test class, implementation part +CLASS ltc_test_class IMPLEMENTATION. + + METHOD setup. + "Creating an object of the class under test + ref_cut = NEW #( ). + + "Here goes, for example, code for preparing the test data, + "e.g. filling a database table used for test methods. + ... + ENDMETHOD. + + + METHOD some_test_method. + + "Method call + DATA(result) = ref_cut->multiply_by_two( 5 ). + + "Assertion + cl_abap_unit_assert=>... + + ENDMETHOD. + + METHOD another_test_method. + "Another method to be tested, it can also use the centrally created instance. + + "Calling method + DATA(result) = ref_cut->multiply_by_three( 6 ). + + "Assertion + cl_abap_unit_assert=>... + + ENDMETHOD. + + METHOD teardown. + "Here goes, for example, code to undo the test data preparation + "during the setup method (e.g. deleting the inserted database table entries). + ... + ENDMETHOD. + +ENDCLASS. +``` + +> **💡 Note**
+> You can also specify helper methods, for example, for recurring tasks such as the assertions. + +

(back to top)

+ +## Handling Dependencies + +The code snippets above covered test classes and methods in simple contexts without dependend-on components (DOC) in the production code. In more complex cases with dependencies in the production code, there are ways to deal with them. You can create test doubles and inject them into the production code during the test run. + +It is assumed that you have identified the dependend-on components (DOC) in a method in your production code. They were isolated (which may have involved some major rebuilding if the code is not created from scratch and the requirements for unit testing were taken into account, e.g. by creating an interface), and you want to replace them with a test double (by injection) so that your code can be properly tested without dependencies. + + +**Creating/Implementing test doubles** + +As recommended, you should ideally have an interface to the DOC. + +There are multiple ways to implement test doubles manually: +- Interface is available for DOC + - You simply create a local test double by implementing interface methods to create test data. See the code snippet in the *Including Interfaces* section. + - As mentioned above, the `PARTIALLY IMPLEMENTED` addition is useful (and only possible) for interfaces in test classes. +- No interface is available for the DOC + - You can create your own local interface, adapt your production code accordingly, and implement interface methods. +- If the DOC is a method in a class that allows inheritance (i.e. it is not defined as `FINAL`), you can inherit from the class and redefine methods for which you need a test double. + +> **💡 Note**
+> Instead of manually creating test doubles, the [ABAP OO Test Double Framework](https://help.sap.com/docs/ABAP_PLATFORM_NEW/c238d694b825421f940829321ffa326a/804c251e9c19426cadd1395978d3f17b.html?locale=en-US) helps you create test doubles automatically. + +**Injecting the test doubles** + +As described [here](https://help.sap.com/docs/ABAP_PLATFORM_NEW/c238d694b825421f940829321ffa326a/04a2d0fc9cd940db8aedf3fa29e5f07e.html?locale=en-US), there are multiple techniques for injecting test doubles to ensure that the test doubles are used during the test run. +To name two of them: +- Constructor injection +- Back door injection + + +*Constructor injection* +- This injection mechanism means that the test double is passed as a parameter to the instance constructor `constructor` of the class under test. +- `constructor` declaration: + - Has an optional importing parameter for the DOC. + - The parameter is typed with reference to the test double, i.e. an object of the test double is passed. +- `constructor` implementation: + - In the example beloew, it is assumed that an interface exists for the DOC, and a test double has been implemented that implements that interface. + - A reference variable with a type reference to the interface is declared in the declaration part of the class under test. + - When the unit test is executed, an object of the test double is created in the test method. This object is then passed to the `constructor`. A check is implemented to determine if the reference variable is bound. During the unit test execution, it is indeed bound, and the test double is injected. If the unit test is not run and the class is executed, an object of the *regular data provider* (e.g. a class that implements the interface for production use) is created. Therefore, it is ensured that the test double is only used in the context of a unit test run. + +Such a constructor injection might look like this. + +``` abap +"See the executable example for the complete picture. +"The code in this snippet refers to the production code. +... +"Class declaration part + DATA ref_data_provider TYPE REF TO if_data, + + "Optional parameter for passing an object of the test touble + METHODS constructor + IMPORTING iref_data_prov TYPE REF TO tld_test_double OPTIONAL. + +.... + +"Class implementation part +METHOD constructor. + "If the object of the test touble is not passed, an object is created for the actual data provider + + IF iref_data_prov IS BOUND. + + "Note: The parameter is only bound when running in ABAP unit test + ref_data_provider = iref_data_prov. + + ELSE. + + ref_data_provider = NEW cl_data_provider( ). + + ENDIF. +ENDMETHOD. +``` + + +*Back door injection* +- This injection mechanism means that a *back door* is created to inject a test double into the class under test. +- This *back door* is implemented by granting [friendship](https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/index.htm?file=abenfriend_glosry.htm) to the test class. This makes internal attributes of the class under test accessible from the test class. +- The back door injection enters the picture when internal attributes of the class under test are changed during the test run. That is, in the production code, you have declared a reference variable with a type reference to the data provider in the private section, for example. + - Note: Similar to above, in the example below, it is assumed that there is an interface for the DOC and that a test double has been implemented that implements that interface. +- When the unit test is executed, the private attribute of the class under test is changed, and an object of the test double is injected. + + +``` abap +"See the executable example for the complete picture. +"The code in this snippet refers to the test class. + +"Test class declaration part +... + +"class under test +DATA ref_cut TYPE REF TO cl_class_under_test. + +"if_data_provider: interface with which local test data are created +"by implementing an interface method +DATA ref_data_prov TYPE REF TO if_data_provider. + +... + + "Implementation of the setup method in the test class + METHOD setup. + + ref_cut = NEW #( ). + + "Assumption: The local test double ltd_test_double implements the interface if_data_provider + ref_data_prov = new ltd_test_double( ). + + "Back door injection + "The reference variable declared in the class under test is assigned an object + "of the test double. In the class under test, the reference variable is defined + "with type TYPE REF TO if_data_provider. + ref_cut->ref_var_defined_in_cut = ref_data_prov. + ENDMETHOD. +``` + +

(back to top)

+ +### Test Seams +- Seams are sections of the production source code that can be dynamically included or replaced. +- In ABAP, [test seams](https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/index.htm?file=abentest_seam_glosry.htm) can be used to replace source code in the production code by an injection when running unit tests. For more informarion, see [here](https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/index.htm?file=abaptest-seam.htm). +- This is particularly useful in situations where tests cannot be executed properly or are even prevented from doing so. For example: + - Authorization checks + - Reading or modifying persistent data from the database + - Creating test doubles +- If the code is not executed in the context of a unit test, there is no injection. The original code (i.e. the code in the block `TEST-SEAM ... END-TEST-SEAM`) is executed. +- A test seam can also be empty, and then some code can be injected where the test seam is declared. + + +Test seams can be implemented using the following syntax: + +``` abap +"The code in this snippet refers to the production code. + +... + +DATA some_table TYPE TABLE OF dbtab WITH EMPTY KEY. + +"TEST-SEAM + name of the test seam ... END-TEST-SEAM define a +"code block that is to be replaced when running unit tests. + +TEST-SEAM select_from_db. + SELECT * FROM dbtab + INTO TABLE @some_table. +END-TEST-SEAM. + +... +``` + +``` abap +"The code in this snippet refers to the code in the test class. + +... + +"TEST-INJECTION + name of the test seam ... END-TEST-INJECTION define a code +"block to replace the code block in the production code when running unit tests. +"In the example below, the DOC (a database access) is solved by providing local +"test data. + +TEST-INJECTION select_from_db. + some_table = VALUE #( + ( ... ) + ( ... ) ). +END-TEST-INJECTION. + +... +``` + +

(back to top)

+ +## Running and Evaluating ABAP Unit Tests +There are many ways to run ABAP unit tests as described [here](https://help.sap.com/docs/ABAP_PLATFORM_NEW/c238d694b825421f940829321ffa326a/4ec4c6c66e391014adc9fffe4e204223.html?locale=en-US). + +The focus of this cheat sheet is on running individual tests in a class that can be run directly in ADT. In your class in ADT (for example, in the class of the demonstration example), choose `Ctrl + Shift + F10` to run all tests in a class. You can also right-click anywhere in the code of the class and choose *Run as → ABAP Unit Test*. To run individual test classes or methods, place the cursor on the class/method name and run the unit test. + +The results of a test run are displayed and can be evaluated in the *ABAP Unit* tab in ADT. The *Failure Trace* section provides information about any errors found. + +If you are interested in the test coverage, you can choose `Ctrl + Shift + F11`, or make a right-click, choose *Run as → ABAP Unit Test With...*, select the *Coverage* checkbox and choose *Execute*. You can then check the results in the *ABAP Coverage* tab in ADT and see what code was tested and what was not. + +For more information about evaluating ABAP unit test results, see [here](https://help.sap.com/docs/ABAP_PLATFORM_NEW/c238d694b825421f940829321ffa326a/4ec49c5b6e391014adc9fffe4e204223.html?locale=en-US). + +

(back to top)

+ +## More information + +- [ABAP Unit in the ABAP Keyword Documentation](https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/index.htm?file=abenabap_unit.htm) +- [openSAP course: Writing Testable Code for ABAP](https://open.sap.com/courses/wtc1.OpenSAP+WTC1_W1U5+Writing+Testable+Code+for+ABAPComent) +- Information on the SAP Help Portal: + - [Unit Testing with ABAP Unit](https://help.sap.com/docs/ABAP_PLATFORM_NEW/c238d694b825421f940829321ffa326a/08c60b52cb85444ea3069779274b43db.html?locale=en-US) + - [ABAP Unit](https://help.sap.com/docs/ABAP_PLATFORM_NEW/ba879a6e2ea04d9bb94c7ccd7cdac446/491cfd8926bc14cde10000000a42189b.html?locale=en-US) +- The [ABAP OO Test Double Framework](https://help.sap.com/docs/ABAP_PLATFORM_NEW/c238d694b825421f940829321ffa326a/804c251e9c19426cadd1395978d3f17b.html?locale=en-US) based on class `CL_ABAP_TESTDOUBLE` simplifies and standardizes the creation and configuration of test doubles. +- [Testing repository objects](https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/index.htm?file=abentest_relations.htm) (ABAP Keyword Documentation) + +

(back to top)

+ +## Executable Example +[zcl_demo_abap_unit_test](./src/zcl_demo_abap_unit_test.clas.abap) + +Note ... +- the steps outlined [here](README.md#-getting-started-with-the-examples) about how to import and run the code. +- the comments in the executable example. \ No newline at end of file diff --git a/src/zcl_demo_abap_unit_test.clas.abap b/src/zcl_demo_abap_unit_test.clas.abap new file mode 100644 index 0000000..966b81e --- /dev/null +++ b/src/zcl_demo_abap_unit_test.clas.abap @@ -0,0 +1,532 @@ +*********************************************************************** +* +* ABAP Cheat Sheet: A Glimpse on ABAP Unit Tests +* +* -------------------------- PURPOSE ---------------------------------- +* - Example to demonstrate ABAP unit tests. +* - Topics covered: Test classes and test methods, special methods, +* implementing and injecting test doubles (constructor injection, +* back door injection, test seams) +* +* ----------------------- RUN ABAP UNIT TEST--------------------------- +* - Open the class with the ABAP Development Tools (ADT). +* - Choose Ctrl/Cmd + Shift + F10 to launch all tests in the class. +* You can also right-click somewhere in the class and choose +* Run as -> ABAP Unit Test. +* - The results of a test run are displayed in the ABAP Unit tab in ADT +* and can be evaluated. The Failure Trace section provides information +* on errors found. +* - If you are interested in test coverage, you can choose +* Ctrl/Cmd + Shift + F11, or make a right-click, choose Run as -> +* ABAP Unit Test With..., select the Coverage checkbox and choose +* Execute. You can then check the results in the ABAP Coverage tab, +* what code is tested and what not. +* +* ----------------------- RUN CLASS ----------------------------- +* - Open the class with the ABAP Development Tools (ADT). +* - Choose F9 to run the class. +* - Check the console output. +* - To understand the context and the ABAP syntax used, check the notes +* included in the class as comments or refer to the respective topic +* in the ABAP Keyword Documentation. +* - Due to the amount of output in the console, the examples include +* numbers (e. g. 1) ..., 2) ..., 3) ...) for the individual example +* sections. Plus, the variable name is displayed in most cases. Hence, +* to easier and faster find the relevant output in the console, just +* search in the console for the number/variable name (CTRL + F in the +* console) or use the debugger. +* +* ----------------------------- NOTE ----------------------------------- +* The code presented in this class is only meant for supporting the ABAP +* cheat sheets. It is not intended for direct use in a +* production system environment. The code examples in the ABAP cheat +* sheets are primarily intended to provide a better explanation and +* visualization of the syntax and semantics of ABAP statements and not to +* solve concrete programming tasks. For production application programs, +* a dedicated solution should therefore always be worked out for each +* individual case. There is no guarantee for either the correctness or +* the completeness of the code. In addition, there is no legal +* responsibility or liability for possible errors or their consequences +* which occur through the use of the example code. +* +*********************************************************************** +"!

ABAP cheat sheet: Unit test

+"! Example to demonstrate ABAP unit tests.
Choose F9 in ADT to run the class. +"! To run all unit tests of the class, choose Ctrl/Cmd + Shift + F10. +CLASS zcl_demo_abap_unit_test DEFINITION + PUBLIC + CREATE PUBLIC. + + PUBLIC SECTION. + + INTERFACES: if_oo_adt_classrun. + + CLASS-METHODS: class_constructor. + + "Optional parameter for the instance constructor for the purpose of + "constructor injection + METHODS constructor + IMPORTING iref_data_prov TYPE REF TO zdemo_abap_get_data_itf OPTIONAL. + + PROTECTED SECTION. + + TYPES carr_tab TYPE TABLE OF zdemo_abap_fli WITH EMPTY KEY. + + METHODS: select_flight_data IMPORTING carrier TYPE zdemo_abap_fli-carrid + RETURNING VALUE(flight_data) TYPE carr_tab. + + PRIVATE SECTION. + + TYPES: int_tab_so TYPE SORTED TABLE OF i WITH UNIQUE KEY table_line, + int_tab_st TYPE STANDARD TABLE OF i WITH EMPTY KEY, + occ_rate TYPE p LENGTH 4 DECIMALS 2, + + BEGIN OF nums_struc, + num1 TYPE i, + num2 TYPE i, + END OF nums_struc, + + nums_tab TYPE TABLE OF nums_struc WITH EMPTY KEY. + + DATA: seats_table TYPE zdemo_abap_get_data_itf=>carr_tab, + flight_tab TYPE TABLE OF zdemo_abap_fli, + + "Reference variable for back door injection + "Note: In the example, it is a local interface declared in the + "Local Types tab (the CCIMP include). To make the type known to + "the global class, see the Class-relevant Local Types tab (the CCDEF include). + data_provider_local_itf TYPE REF TO lif_get_data, + + "Reference variable for constructor injection + "In the example, the type refers to a global interface. + data_provider_global_itf TYPE REF TO zdemo_abap_get_data_itf. + + METHODS: + "Calculates the sum of two numbers + "This method demonstrates the use of the setup and teardown methods in the test class. + get_sum IMPORTING key TYPE zdemo_abap_tab1-key_field + char TYPE zdemo_abap_tab1-char1 + RETURNING VALUE(sum) TYPE i, + + "Calculates the common divisors and the greatest common divisor of two numbers + get_common_div_and_gcd IMPORTING a TYPE i + b TYPE i + EXPORTING common_divisors TYPE int_tab_so + gcd TYPE i, + + "Calculates the digit sum of a number + get_digit_sum IMPORTING num TYPE i + RETURNING VALUE(digit_sum) TYPE i, + + "Multiple methods that all do the same (they calculate the occupancy rate of flights) + "but serve different demonstration purposes for the ABAP unit tests in the example. + "The method implementations involve a depended-on component (DOC). In this case, + "it is a database access. + + "Method to demonstrate test double injection using inheritance and method redefinition + get_occ_rate_using_meth IMPORTING carrier_id TYPE zdemo_abap_fli-carrid + RETURNING VALUE(occupancy_rate) TYPE occ_rate, + + "Method to demonstrate test double injection using test seams + get_occ_rate_test_seam IMPORTING carrier_id TYPE zdemo_abap_fli-carrid + EXPORTING occupancy_rate TYPE occ_rate + num1 TYPE i + num2 TYPE i, + + "Method to demonstrate test double injection using back door injection and a local interface + get_occ_rate_local_itf IMPORTING carrier_id TYPE zdemo_abap_fli-carrid + RETURNING VALUE(occupancy_rate) TYPE occ_rate, + + "Method to demonstrate test double injection using constructor injection and a global interface + get_occ_rate_global_itf IMPORTING carrier_id TYPE zdemo_abap_fli-carrid + RETURNING VALUE(occupancy_rate) TYPE occ_rate. + +ENDCLASS. + +CLASS zcl_demo_abap_unit_test IMPLEMENTATION. + + METHOD if_oo_adt_classrun~main. + "Note: The example includes a couple of implementations for the methods + "declared above. And by choosing F9 in ADT, you can run the class and check the + "output in the console. + "However, the focus of the example is unit tests. Therefore, check the + "test classes and methods in the test include (Test Classes tab in ADT). + "Choose Ctrl/Cmd + Shift + F10 to launch all tests in the class and check the + "test results in the ABAP Unit tab in ADT. + + DATA(output) = NEW zcl_demo_abap_display( out ). + + output->display( `ABAP Cheat Sheet Demonstration Example: A Glimpse on ABAP Unit Tests` ). + output->display( `1) get_sum Method` ). + "This method demonstrates the use of the setup and teardown methods in the test class. + + DATA(sum) = get_sum( key = 1 char = 'aaa' ). + + output->display( input = sum name = `sum` ). + +********************************************************************** + + output->next_section( `2) get_common_div_and_gcd Method` ). + + "Filling an internal table with numbers on whose bases the common divisors and the + "greatest common divisor are to be calculated + DATA(tab) = VALUE nums_tab( ( num1 = 10 num2 = 20 ) + ( num1 = 100 num2 = 200 ) + ( num1 = 12 num2 = 6 ) + ( num1 = 5 num2 = 1 ) + ( num1 = 50 num2 = 50 ) + ( num1 = 4 num2 = 8 ) ). + + LOOP AT tab ASSIGNING FIELD-SYMBOL(). + + get_common_div_and_gcd( EXPORTING a = -num1 + b = -num2 + IMPORTING common_divisors = DATA(common_divs) gcd = DATA(gcd) ). + + output->display( |Common divisors of { -num1 } and { -num2 }| ). + + output->display( input = common_divs name = `common_divs` ). + + output->display( |Greatest common divisor of { -num1 } and { -num2 }: { gcd } | ). + + ENDLOOP. + + +********************************************************************** + + output->next_section( `3) get_digit_sum Method` ). + + "Filling an internal table with numbers on whose bases the digit sum is to be calculated + DATA(tab_i) = VALUE int_tab_so( ( 12 ) + ( 123 ) + ( 3 ) + ( 8246 ) + ( 1001001 ) + ( 0 ) ). + + LOOP AT tab_i ASSIGNING FIELD-SYMBOL(). + + DATA(digit_sum) = get_digit_sum( ). + + output->display( |The digit sum of { } is { digit_sum }.| ). + + ENDLOOP. + +********************************************************************** + + output->next_section( `4) get_occ_rate_using_meth Method` ). + "In the test class, this method demonstrates test double injection + "using inheritance and method redefinition. + + "Filling an internal table with carrier ids on whose bases the occupancy + "rate is to be calculated. + DATA(tab_str) = VALUE zdemo_abap_get_data_itf=>carr_tab( ( carrid = 'LH' ) + ( carrid = 'AA' ) + ( carrid = 'DL' ) ). + + LOOP AT tab_str ASSIGNING FIELD-SYMBOL(). + + DATA(occupancy_rate) = get_occ_rate_using_meth( -carrid ). + + output->display( |The occupancy rate for airline { -carrid } is { occupancy_rate }%.| ). + + ENDLOOP. + output->next_section( `5) get_occ_rate_test_seam Method` ). + "This method demonstrates test double injection using test seams. + + LOOP AT tab_str ASSIGNING FIELD-SYMBOL(). + + get_occ_rate_test_seam( EXPORTING carrier_id = -carrid + IMPORTING occupancy_rate = DATA(occupancy_rate_test_seam) + num1 = DATA(num1) + num2 = DATA(num2) ). + + output->display( |The occupancy rate for airline { -carrid } is { occupancy_rate_test_seam }%.| ). + + output->display( |num1: { num1 }| ). + output->display( |num2: { num2 }| ). + + ENDLOOP. + +********************************************************************** + + output->next_section( `6) get_occ_rate_local_itf Method` ). + "This method demonstrates test double injection using back door + "injection and a local interface. + + LOOP AT tab_str ASSIGNING FIELD-SYMBOL(). + + DATA(occupancy_rate_local_itf) = get_occ_rate_local_itf( -carrid ). + + output->display( |The occupancy rate for airline { -carrid } is { occupancy_rate_local_itf }%.| ). + + ENDLOOP. + +********************************************************************** + + output->next_section( `7) get_occ_rate_global_itf Method` ). + "This method demonstrates test double injection using constructor + "injection and a global interface. + + LOOP AT tab_str ASSIGNING FIELD-SYMBOL(). + + DATA(occupancy_rate_global_itf) = get_occ_rate_global_itf( -carrid ). + + output->display( |The occupancy rate for airline { -carrid } is { occupancy_rate_global_itf }%.| ). + + ENDLOOP. + + ENDMETHOD. + + METHOD class_constructor. + "Filling demo database tables. + zcl_demo_abap_flight_tables=>fill_dbtabs( ). + + "Preparing a demo database table for this example (get_sum method) + DELETE FROM zdemo_abap_tab1. + INSERT zdemo_abap_tab1 FROM @( + VALUE #( key_field = 1 char1 = 'aaa' char2 = 'bbb' num1 = 25 num2 = 75 ) ). + ENDMETHOD. + + METHOD constructor. + + "For demonstrating the back door injection + data_provider_local_itf = NEW lcl_data_prov_local_itf( ). + + "For demonstrating the constructor injection + IF iref_data_prov IS BOUND. + "Note: The parameter is only bound when you run the unit test. + "When you run the unit test and you debug, you will see that iref_data_prov + "has a type reference to LTD_TEST_DATA_GLOBAL_INTF. + + data_provider_global_itf = iref_data_prov. + + ELSE. + + data_provider_global_itf = NEW lcl_data_prov_glo_itf( ). + + ENDIF. + + ENDMETHOD. + + METHOD get_sum. + "The method selects a record from a database table and sums the values + "of two fields, both are of type i. + + SELECT SINGLE + FROM zdemo_abap_tab1 + FIELDS num1 + num2 AS sum + WHERE key_field = @key + AND char1 = @char + INTO @sum. + + ENDMETHOD. + + METHOD get_common_div_and_gcd. + "Calculates the common divisors and the greatest common divisor of two numbers + + CHECK a >= 1. + CHECK b >= 1. + + IF a >= b. + DATA(greater_num) = a. + DATA(lower_num) = b. + ELSE. + greater_num = b. + lower_num = a. + ENDIF. + + "Getting common divisors + DATA(div) = 1. + + WHILE div <= lower_num. + IF lower_num MOD div = 0. + DATA(divisor) = lower_num / div. + INSERT divisor INTO TABLE common_divisors. + ENDIF. + + div += 1. + ENDWHILE. + + LOOP AT common_divisors ASSIGNING FIELD-SYMBOL(). + + IF greater_num MOD <> 0. + DELETE common_divisors WHERE table_line = . + ENDIF. + + ENDLOOP. + + "Extracting the greatest common divisor from the list of common divisors + gcd = common_divisors[ lines( common_divisors ) ]. + + ENDMETHOD. + + METHOD get_digit_sum. + "Calculates the digit sum of a number + + CHECK num >= 0. + + DATA(converted_int) = CONV string( num ). + DATA(len) = strlen( converted_int ). + + DO len TIMES. + DATA(idx) = sy-index - 1. + digit_sum = digit_sum + converted_int+idx(1). + ENDDO. + + ENDMETHOD. + + METHOD get_occ_rate_test_seam. + "Method to demonstrate test double injection using test seams + "Note: The code is just for demonstration purposes. Of course, the result can be + "achieved more elegantly using SQL expressions, for example. + + TEST-SEAM select_flights. + "DOC + SELECT seatsmax, seatsocc + FROM zdemo_abap_fli + WHERE carrid = @carrier_id + INTO CORRESPONDING FIELDS OF TABLE @seats_table. + END-TEST-SEAM. + + DATA total_seatsmax_tm TYPE i. + DATA total_seatsocc_tm TYPE i. + + LOOP AT seats_table ASSIGNING FIELD-SYMBOL(). + + total_seatsmax_tm = total_seatsmax_tm + -seatsmax. + total_seatsocc_tm = total_seatsocc_tm + -seatsocc. + + ENDLOOP. + + occupancy_rate = total_seatsocc_tm / total_seatsmax_tm * 100. + + "Further examples for test seams + DATA(var) = 0. + + "Empty test seam; code is injected during unit test + "Check the output when running the class using F9 and + "the test results when running the unit test. + TEST-SEAM num1. + END-TEST-SEAM. + + IF var = 0. + num1 = 1. + ELSE. + num1 = 999. + ENDIF. + + num2 = 0. + + "Empty injection + "See the test class: The code that is included in the test + "seam should be excluded from the test. Therefore, the + "test injection block in the test class is empty. + "Check the output when running the class using F9 and + "the test results when running the unit test. + TEST-SEAM num2. + num2 = 123. + END-TEST-SEAM. + + ENDMETHOD. + + METHOD get_occ_rate_local_itf. + "Method to demonstrate test double injection using back door + "injection and a local interface + + DATA total_seatsmax_local_itf TYPE i. + DATA total_seatsocc_local_itf TYPE i. + + "Assumption: The original code in this method was as follows (the line commented out). + "It was identified as DOC (reading data from a database table). + + "DATA(flight_data) = select_flight_data( carrier = carrier_id ). + + "Instead of a method call like above and for a proper unit testing - a global interface + "is not available - a local interface is created, and + "an interface method is implemented. In this example, the local interface is + "created in the local types tab (CCIMP include): lif_get_data + "A local class (lcl_data_prov_local_itf) is also created in the CCIMP include. It + "implements the local interface. + + "When running the class with F9, the object used here refers to type lcl_data_prov_local_itf. + "When running the unit test, the object used here refers to type ltd_test_data_local_itf, + "i.e. the local test double is injected. + DATA(flight_data) = data_provider_local_itf->select_flight_data( carrier = carrier_id ). + + LOOP AT flight_data ASSIGNING FIELD-SYMBOL(). + + total_seatsmax_local_itf = total_seatsmax_local_itf + -seatsmax. + total_seatsocc_local_itf = total_seatsocc_local_itf + -seatsocc. + + ENDLOOP. + + occupancy_rate = total_seatsocc_local_itf / total_seatsmax_local_itf * 100. + + ENDMETHOD. + + METHOD get_occ_rate_global_itf. + "Method to demonstrate test double injection using constructor injection + "and a global interface + + DATA total_seatsmax_global_itf TYPE i. + DATA total_seatsocc_global_itf TYPE i. + + "Assumption: The original code in this method was as follows (the line commented out). + "It was identified as DOC (reading data from a database table). + + "DATA(flight_data) = select_flight_data( carrier = carrier_id ). + + "Instead of a method call like above and for a proper unit testing, a global interface + "is provided. + "In the example, an interface method is implemented in a local class in the local types + "tab (CCIMP include): lcl_data_prov_glo_itf + + "When running the class with F9, the object used here refers to type lcl_data_prov_glo_itf. + "When running the unit test, the object used here refers to type ltd_test_data_global_intf, + "i.e. the local test double is injected. + + DATA(flight_data) = data_provider_global_itf->select_flight_data( carrier = carrier_id ). + + LOOP AT flight_data ASSIGNING FIELD-SYMBOL(). + + total_seatsmax_global_itf = total_seatsmax_global_itf + -seatsmax. + total_seatsocc_global_itf = total_seatsocc_global_itf + -seatsocc. + + ENDLOOP. + + occupancy_rate = total_seatsocc_global_itf / total_seatsmax_global_itf * 100. + + ENDMETHOD. + + METHOD select_flight_data. + "Method that is identified as DOC in the method implementations above. + "This method is also used for demonstrating test double injection and method redefinition. + + SELECT seatsmax, seatsocc + FROM zdemo_abap_fli + WHERE carrid = @carrier + INTO CORRESPONDING FIELDS OF TABLE @flight_data. + ENDMETHOD. + + METHOD get_occ_rate_using_meth. + "This method demonstrates test double injection using inheritance and method redefinition. + + DATA total_seatsmax_no TYPE i. + DATA total_seatsocc_no TYPE i. + + "During the unit test, the redefined method in the test class is called. + DATA(flight_data) = select_flight_data( carrier = carrier_id ). + + LOOP AT flight_data ASSIGNING FIELD-SYMBOL(). + + total_seatsmax_no = total_seatsmax_no + -seatsmax. + total_seatsocc_no = total_seatsocc_no + -seatsocc. + + ENDLOOP. + + occupancy_rate = total_seatsocc_no / total_seatsmax_no * 100. + + ENDMETHOD. + +ENDCLASS. diff --git a/src/zcl_demo_abap_unit_test.clas.locals_def.abap b/src/zcl_demo_abap_unit_test.clas.locals_def.abap new file mode 100644 index 0000000..de73f08 --- /dev/null +++ b/src/zcl_demo_abap_unit_test.clas.locals_def.abap @@ -0,0 +1,4 @@ +*"* use this source file for any type of declarations (class +*"* definitions, interfaces or type declarations) you need for +*"* components in the private section +INTERFACE lif_get_data DEFERRED. diff --git a/src/zcl_demo_abap_unit_test.clas.locals_imp.abap b/src/zcl_demo_abap_unit_test.clas.locals_imp.abap new file mode 100644 index 0000000..f9e8bf2 --- /dev/null +++ b/src/zcl_demo_abap_unit_test.clas.locals_imp.abap @@ -0,0 +1,73 @@ +****************************************************** +* Local interface +****************************************************** +INTERFACE lif_get_data. + + TYPES: carr_tab TYPE TABLE OF zdemo_abap_fli WITH EMPTY KEY, + occ_rate TYPE p LENGTH 4 DECIMALS 2. + + METHODS: + select_flight_data IMPORTING carrier TYPE zdemo_abap_fli-carrid + RETURNING VALUE(flight_data) TYPE carr_tab, + + "This method is included to demonstrate the PARTIALLY IMPLEMENTED + "addition in the test class when implementing the test double + say_hello RETURNING VALUE(hi) TYPE string. + +ENDINTERFACE. + +****************************************************** +* Local class +* The class implements the local interface. +****************************************************** +CLASS lcl_data_prov_local_itf DEFINITION. + PUBLIC SECTION. + INTERFACES lif_get_data. +ENDCLASS. + +CLASS lcl_data_prov_local_itf IMPLEMENTATION. + + METHOD lif_get_data~select_flight_data. + + SELECT seatsmax, seatsocc + FROM zdemo_abap_fli + WHERE carrid = @carrier + INTO CORRESPONDING FIELDS OF TABLE @flight_data. + + ENDMETHOD. + + METHOD lif_get_data~say_hello. + hi = `Hello, ` && sy-uname && `.`. + ENDMETHOD. + +ENDCLASS. + +****************************************************** +* Local class that implements a global interface +* It serves the purpose of a data provider. It can be +* imagined as a global class, and a method that is +* implemented there and called by the class under test +* is identified as DOC. A local class is used in the +* example to keep the number of separate artifacts small. +****************************************************** +CLASS lcl_data_prov_glo_itf DEFINITION. + PUBLIC SECTION. + INTERFACES zdemo_abap_get_data_itf. +ENDCLASS. + +CLASS lcl_data_prov_glo_itf IMPLEMENTATION. + + METHOD zdemo_abap_get_data_itf~select_flight_data. + + SELECT seatsmax, seatsocc + FROM zdemo_abap_fli + WHERE carrid = @carrier + INTO CORRESPONDING FIELDS OF TABLE @flight_data. + + ENDMETHOD. + + METHOD zdemo_abap_get_data_itf~say_hello. + hi = `Hello, ` && sy-uname && `.`. + ENDMETHOD. + +ENDCLASS. diff --git a/src/zcl_demo_abap_unit_test.clas.testclasses.abap b/src/zcl_demo_abap_unit_test.clas.testclasses.abap new file mode 100644 index 0000000..0482e79 --- /dev/null +++ b/src/zcl_demo_abap_unit_test.clas.testclasses.abap @@ -0,0 +1,1048 @@ +*********************************************************************** +* Test class overview +* +* Note: +* - This test include contains multiple test classes and methods +* for unit testing the production code in the global class. +* - To run the unit test, choose CTRL/CMD + Shift + F10. See +* also the information in the global class (RUN ABAP UNIT TEST at the top). +* - Almost all test classes deliberately contain test methods which +* produce errors in the test and that can be checked in the ABAP Unit tab. +* Likewise, test methods are included for which the test does not fail. +* - All assertion methods include the parameters MSG (for displaying an +* error text in the test results) and QUIT (which is specified to not +* terminate test). +* +* Test classes +* Note the comments before the individual class declaration parts. +* +* - ltc_test_simple_1 +* - Simple example testing only one method; no special methods, +* no dependent-on component (DOC). +* +* - ltc_test_simple_2 +* - Testing mutliple simple methods, no DOCs +* - Special methods setup and teardown +* +*- ltc_test_dummy +* - Does not contain any methods of the production code to be tested. +* - Demonstrates the use of various static methods of the cl_abap_unit_assert class +* +*- ltc_test_seam +* - Testing one method for which a DOC has been identified +* - Demonstrates the use of test seams and their injection into the production code. +* +*- ltc_test_doc_global_itf +* - Testing one method for which a DOC has been identified and for which a global +* interface exists +* - A Local test double class is included +* - Uses constructor injection as injection mechanism +* +*- ltc_test_local_itf +* - Testing one method for which a DOC has been identified +* - There is no global interface available. Instead, a local interface is created. +* - A Local test double class is included +* - Uses back door injection as injection mechanism +* +*- ltc_test_redef +* - Testing one method for which a DOC has been identified - +* - A local test double class is created by redefining a method of +* the class under test. +* +*********************************************************************** + +"In this example, multiple test classes are created in the test include. +"Because private attributes are not accessible in local test classes, +"the local test classes are declared as local friends of the global class. +"in the example, a combined friendship declaration for all test classes is placed +"at the top of the test include. Prepending the friendship declaration with +"test class definitions and the DEFERRED addition makes the the test classes +"'known' at this stage and can thus be specified as local friends there. +CLASS ltc_test_simple_1 DEFINITION DEFERRED. +CLASS ltc_test_simple_2 DEFINITION DEFERRED. +CLASS ltc_test_doc_seam DEFINITION DEFERRED. +CLASS ltc_test_doc_global_itf DEFINITION DEFERRED. +CLASS ltc_test_doc_local_itf DEFINITION DEFERRED. +CLASS ltc_test_doc_redef DEFINITION DEFERRED. + +CLASS zcl_demo_abap_unit_test DEFINITION LOCAL FRIENDS ltc_test_simple_1 + ltc_test_simple_2 + ltc_test_doc_seam + ltc_test_doc_global_itf + ltc_test_doc_local_itf + ltc_test_doc_redef. + +*********************************************************************** +* Test class ltc_test_simple_1 +* +* - Simple example testing only one method; no special methods are declared +* - The tested method does not have a dependent-on component (DOC). It has one +* importing and a returning parameter. +* - The implementation of the test method provides some values against which +* the production method is tested a few times (using a loop). +* Expected values are also provided, on the basis of which the assertion is +* performed. +* - There are values included for which the assertion deliberately fails to +* demonstrate the errors in the ABAP Unit tab. +*********************************************************************** +CLASS ltc_test_simple_1 DEFINITION FOR TESTING + RISK LEVEL HARMLESS + DURATION SHORT. + + PRIVATE SECTION. + + METHODS: test_get_digit_sum FOR TESTING. + +ENDCLASS. + +CLASS ltc_test_simple_1 IMPLEMENTATION. + + METHOD test_get_digit_sum. + "The method to be tested calculates the digit sum of an number. + + "Creating an object of class under test + DATA(ref_cut) = NEW zcl_demo_abap_unit_test( ). + + "Filling an internal table and providing some values against which the production + "method is tested a few times. There are values included for which the assertion + "deliberately fails. + "num1 = actual parameter for the method call + "num2 = expected result of the method call + DATA(tab_digit_sum) = VALUE zcl_demo_abap_unit_test=>nums_tab( ( num1 = 124 num2 = 7 ) + ( num1 = 57 num2 = 12 ) + ( num1 = 11111 num2 = 5 ) + ( num1 = 1000 num2 = 1 ) + ( num1 = 0 num2 = 0 ) + + "To fail: + ( num1 = 124 num2 = 8 ) "correct: 7 + ( num1 = 57 num2 = 13 ) "correct: 12 + ( num1 = 11111 num2 = 6 ) ). "correct: 5 + + "Looping across the internal table and test the method with the values provided. + LOOP AT tab_digit_sum ASSIGNING FIELD-SYMBOL(). + + "Calling method that is to be tested + DATA(digit_sum) = ref_cut->get_digit_sum( -num1 ). + + "Assertion + cl_abap_unit_assert=>assert_equals( + act = digit_sum + exp = -num2 + msg = |The digit sum of { -num1 } is not { -num2 }. It is { digit_sum }.| + quit = if_abap_unit_constant=>quit-no ). + + ENDLOOP. + + ENDMETHOD. + +ENDCLASS. + +*********************************************************************** +* Test class ltc_test_simple_2 +* +* - Test class used to test simple methods that do not have DOCs in the +* production code. +* - Similar to the test class above, the implementation of the test method +* provides some values against which the production method is tested a +* few times (in some cases using a loop). Expected values are also provided, +* on the basis of which the assertion is executed. There are values +* included for which the assertion intentionally fails to display the errors +* in the ABAP Unit tab. +* - The test class contains the declaration and implementation of the +* special methods setup and teardown. The setup method involves creating +* an object of the class under test. This object is used by all test methods. +* - The get_sum method in the production code is tested here. The original +* implementation of the method includes a SELECT statement to retrieve +* a single record. The statement sums the values of two fields, both are of +* type i. For proper testability, the database table is prepared in the setup +* method (an appropriate record is added to the table). +* - In the teardown method, the newly added record is deleted again. +* - Because of this change in persistent data, the test class should have the +* RISK LEVEL DANGEROUS property. However, in this example, it is set to +* HARMLESS because, depending on your system settings, you may not be allowed +* to run unit tests with RISK LEVEL DANGEROUS. +*********************************************************************** +CLASS ltc_test_simple_2 DEFINITION FOR TESTING + RISK LEVEL HARMLESS + DURATION SHORT. + + PRIVATE SECTION. + + TYPES: BEGIN OF nums, + num1 TYPE i, + num2 TYPE i, + common_div TYPE zcl_demo_abap_unit_test=>int_tab_so, + gcd TYPE i, + END OF nums, + tab_type TYPE TABLE OF nums WITH EMPTY KEY. + + DATA: ref_cut TYPE REF TO zcl_demo_abap_unit_test. + + METHODS: test_get_common_div_gcd_ok FOR TESTING, + test_get_common_div_gcd_fail FOR TESTING, + + test_get_digit_sum_ok FOR TESTING, + test_get_digit_sum_fail FOR TESTING, + + test_get_sum_ok FOR TESTING, + test_get_sum_fail FOR TESTING, + + setup, + teardown. + +ENDCLASS. + + +CLASS ltc_test_simple_2 IMPLEMENTATION. + + METHOD setup. + + "Creating an object of the class under test + ref_cut = NEW #( ). + + "Preparing demo database table for testing the get_sum method + DELETE FROM zdemo_abap_tab1 + WHERE key_field = 987654321 + AND char1 = '@'. + + MODIFY zdemo_abap_tab1 FROM @( + VALUE #( key_field = 987654321 + char1 = '@' + num1 = 444 + num2 = 555 ) ). + + ENDMETHOD. + + METHOD teardown. + "Removing test data inserted into the demo database table for testing the get_sum method + DELETE FROM zdemo_abap_tab1 + WHERE key_field = 987654321 + AND char1 = '@'. + ENDMETHOD. + + METHOD test_get_common_div_gcd_ok. + "The method to be tested calculates the common divisors of two numbers + "and the greatest common divisor. + + "Filling an internal table and providing some values against which the production + "method is tested a few times (using a loop). + "num1/num2 = numbers on which the calculation is based + "common_div = table containing the expected common divisors + "gcd = expected greatest common divisor + DATA(tab) = VALUE tab_type( + ( num1 = 6 num2 = 3 common_div = VALUE #( ( 1 ) ( 3 ) ) gcd = 3 ) + ( num1 = 24 num2 = 36 common_div = VALUE #( ( 1 ) ( 2 ) ( 3 ) ( 4 ) ( 6 ) ( 12 ) ) gcd = 12 ) + ( num1 = 5 num2 = 7 common_div = VALUE #( ( 1 ) ) gcd = 1 ) + ( num1 = 21 num2 = 35 common_div = VALUE #( ( 1 ) ( 7 ) ) gcd = 7 ) ). + + "Looping across the internal table and test the method with the values provided. + LOOP AT tab ASSIGNING FIELD-SYMBOL(). + + "Calling method that is to be tested + ref_cut->get_common_div_and_gcd( EXPORTING a = -num1 + b = -num2 + IMPORTING common_divisors = DATA(a) + gcd = DATA(b) ). + + "Assertion + cl_abap_unit_assert=>assert_equals( + act = VALUE nums( common_div = a + gcd = b ) + exp = VALUE nums( common_div = -common_div + gcd = -gcd ) + msg = |Wrong result for { -num1 } and { -num2 }.| + quit = if_abap_unit_constant=>quit-no ). + + ENDLOOP. + + ENDMETHOD. + + METHOD test_get_common_div_gcd_fail. + "This method intentionally includes values to make the unit test fail. + + DATA(num1) = 6. + DATA(num2) = 3. + DATA(c_div) = VALUE zcl_demo_abap_unit_test=>int_tab_st( ( 1 ) ( 2 ) ). "correct: 1, 3 + DATA(g_c_d) = 4. "correct: 3 + + "Calling method that is to be tested + ref_cut->get_common_div_and_gcd( EXPORTING a = num1 + b = num2 + IMPORTING common_divisors = DATA(cd) + gcd = DATA(gr) ). + + "Assertion + cl_abap_unit_assert=>assert_equals( + act = VALUE nums( common_div = cd + gcd = gr ) + exp = VALUE nums( common_div = c_div + gcd = g_c_d ) + msg = |Wrong result for { num1 } and { num2 }.| + quit = if_abap_unit_constant=>quit-no ). + + ENDMETHOD. + + METHOD test_get_digit_sum_ok. + "The method to be tested calculates the digit sum of a number. + + + "Filling an internal table and providing some values against which the production + "method is tested a few times (using a loop). + "num1: Number on which the digit sum is calculated + "num2: Expected result + DATA(tab_digit_sum) = VALUE zcl_demo_abap_unit_test=>nums_tab( ( num1 = 124 num2 = 7 ) + ( num1 = 57 num2 = 12 ) + ( num1 = 11111 num2 = 5 ) + ( num1 = 1000 num2 = 1 ) + ( num1 = 0 num2 = 0 ) ). + + "Looping across the internal table and test the method with the values provided. + LOOP AT tab_digit_sum ASSIGNING FIELD-SYMBOL(). + + "Calling method that is to be tested + DATA(digit_sum) = ref_cut->get_digit_sum( -num1 ). + + "Assertion + cl_abap_unit_assert=>assert_equals( + act = digit_sum + exp = -num2 + msg = |The digit sum of { -num1 } is not { digit_sum }.| + quit = if_abap_unit_constant=>quit-no ). + + ENDLOOP. + + ENDMETHOD. + + METHOD test_get_digit_sum_fail. + "This method intentionally includes values to make the unit test fail. + + DATA(num) = 123. + DATA(dsum) = 7. "correct: 6 + + "Calling method that is to be tested + DATA(digit_sum) = ref_cut->get_digit_sum( num ). + + "Assertion + cl_abap_unit_assert=>assert_equals( + act = digit_sum + exp = dsum + msg = |The digit sum of { num } is not { dsum }.| + quit = if_abap_unit_constant=>quit-no ). + + ENDMETHOD. + + METHOD test_get_sum_ok. + "The method to be tested calculates the sum of two numbers. + "Note: The setup method has prepared data in the database table. + "See the comments above. + + DATA(exp_sum) = 999. + + "Calling method that is to be tested + DATA(act_sum) = ref_cut->get_sum( key = 987654321 char = '@' ). + + "Assertion + cl_abap_unit_assert=>assert_equals( + act = act_sum + exp = exp_sum + msg = |The expected sum { exp_sum } is wrong.| + quit = if_abap_unit_constant=>quit-no ). + + ENDMETHOD. + + METHOD test_get_sum_fail. + "This method intentionally includes values to make the unit test fail. + + DATA(exp_sum) = 998. "correct: 999 + DATA(act_sum) = ref_cut->get_sum( key = 987654321 char = '@' ). + + cl_abap_unit_assert=>assert_equals( + act = act_sum + exp = exp_sum + msg = |The expected sum { exp_sum } is wrong.| + quit = if_abap_unit_constant=>quit-no ). + + ENDMETHOD. + +ENDCLASS. + +*********************************************************************** +* Test class ltc_test_dummy +* +* - This test class does not contain any methods of the production code +* to be tested. +* - It only demonstrates the use of various static methods of the +* cl_abap_unit_assert class. +* - Since no (private) components of the production code are used, the +* test class is not made a local friend of the class under test. +*********************************************************************** +CLASS ltc_test_dummy DEFINITION FOR TESTING + RISK LEVEL HARMLESS + DURATION SHORT. + + PRIVATE SECTION. + + DATA: ref_cut TYPE REF TO zcl_demo_abap_unit_test. + + METHODS: test_multiple_asserts_ok FOR TESTING, + test_multiple_asserts_fail FOR TESTING, + setup. + +ENDCLASS. + +CLASS ltc_test_dummy IMPLEMENTATION. + + METHOD setup. + "Creating an object of the class under test + ref_cut = NEW #( ). + ENDMETHOD. + + METHOD test_multiple_asserts_ok. + "This method does not test a method of the class under test. + + DATA(ref) = NEW zcl_demo_abap_unit_test( ). + + DATA(is_true) = cl_abap_unit_assert=>assert_equals( + act = 1 + exp = 1 + msg = |assert_equals: Issue.| + quit = if_abap_unit_constant=>quit-no ). + + is_true = cl_abap_unit_assert=>assert_bound( + act = ref + msg = |assert_bound: Issue.| + quit = if_abap_unit_constant=>quit-no ). + + is_true = cl_abap_unit_assert=>assert_differs( + act = `hallo` + exp = `hallo!` + msg = |assert_differs: Issue.| + quit = if_abap_unit_constant=>quit-no ). + + is_true = cl_abap_unit_assert=>assert_initial( + act = 0 + msg = |assert_initial: Issue.| + quit = if_abap_unit_constant=>quit-no ). + + is_true = cl_abap_unit_assert=>assert_not_initial( + act = 1 + msg = |assert_not_initial: Issue.| + quit = if_abap_unit_constant=>quit-no ). + + FIND `A` IN `ABAP`. + + is_true = cl_abap_unit_assert=>assert_subrc( + exp = 0 + act = sy-subrc + msg = |assert_subrc: Issue.| + quit = if_abap_unit_constant=>quit-no ). + + ENDMETHOD. + + METHOD test_multiple_asserts_fail. + "This method intentionally includes values to make the unit test fail. + + DATA ref TYPE REF TO zcl_demo_abap_unit_test. + + DATA(is_true) = cl_abap_unit_assert=>assert_equals( + exp = 'This a string that is checked.' + act = 'This a string that is checked ... but it is not the expected string.' + msg = |assert_equals: Issue.| + quit = if_abap_unit_constant=>quit-no ). + + is_true = cl_abap_unit_assert=>assert_bound( + act = ref + msg = |assert_bound: Issue.| + quit = if_abap_unit_constant=>quit-no ). + + is_true = cl_abap_unit_assert=>assert_differs( + act = `hallo` + exp = `hallo` + msg = |assert_differs: Issue.| + quit = if_abap_unit_constant=>quit-no ). + + is_true = cl_abap_unit_assert=>assert_initial( + act = 1 + msg = |assert_initial: Issue.| + quit = if_abap_unit_constant=>quit-no ). + + is_true = cl_abap_unit_assert=>assert_not_initial( + act = 0 + msg = |assert_not_initial: Issue.| + quit = if_abap_unit_constant=>quit-no ). + + FIND `C` IN `ABAP`. + + is_true = cl_abap_unit_assert=>assert_subrc( + exp = 0 + act = sy-subrc + msg = |assert_subrc: Issue.| + quit = if_abap_unit_constant=>quit-no ). + + IF 1 <> 2. + cl_abap_unit_assert=>fail( + msg = |fail: Issue.| + quit = if_abap_unit_constant=>quit-no ). + ENDIF. + + ENDMETHOD. + +ENDCLASS. + + +*********************************************************************** +* Test class ltc_test_seam +* +* - This test class demonstrates the use of test seams and their injection +* into the production code. +* - In the production code, the method implementation includes a +* depended-on component (DOC), which in this case is a database access. +* The DOC is replaced by a test double using a test seam. +* - The method implementation contains further simple examples for +* test seam injections. +*********************************************************************** +CLASS ltc_test_doc_seam DEFINITION FOR TESTING + RISK LEVEL HARMLESS + DURATION SHORT. + + PRIVATE SECTION. + + DATA ref_cut TYPE REF TO zcl_demo_abap_unit_test. + + METHODS: test_get_occ_rate_seam_ok FOR TESTING, + test_get_occ_rate_seam_fail FOR TESTING, + setup. + +ENDCLASS. + + +CLASS ltc_test_doc_seam IMPLEMENTATION. + + METHOD setup. + + "Creating an object of the class under test + ref_cut = NEW #( ). + + ENDMETHOD. + + METHOD test_get_occ_rate_seam_ok. + "The method to be tested calculates the occupancy rate of flights + + "Creating test data + DATA carrier_id TYPE zdemo_abap_fli-carrid VALUE 'AB'. + DATA(expected_occupancy_rate) = '50.00'. + + "Injecting test seam into production code by replacing the code that is + "contained in the TEST-SEAM ... TEST-SEAM-END block + TEST-INJECTION select_flights. + seats_table = VALUE #( + ( carrid = carrier_id seatsmax = 100 seatsocc = 80 ) + ( carrid = carrier_id seatsmax = 100 seatsocc = 20 ) + ( carrid = carrier_id seatsmax = 200 seatsocc = 100 ) ). + END-TEST-INJECTION. + + "Further test seam injections + "The following example demonstrates an empty test seam in the production + "code. During the unit test run, the following code is injected. + TEST-INJECTION num1. + var = 1. + END-TEST-INJECTION. + + "empty injection + + "The following example demonstrates an empty test seam injection. + "There is code in a TEST-SEAM ... END-TEST-SEAM block in the + "production code. This code is replaced, and nothing is included + "for the test, i.e. during the unit test 'unwanted' code in the production + "code is ignored. + TEST-INJECTION num2. + END-TEST-INJECTION. + + "Calling method that is to be tested + ref_cut->get_occ_rate_test_seam( EXPORTING carrier_id = carrier_id + IMPORTING occupancy_rate = DATA(occupancy_rate) + num1 = DATA(num1) + num2 = DATA(num2) ). + + "Assertions + cl_abap_unit_assert=>assert_equals( + act = occupancy_rate + exp = expected_occupancy_rate + msg = |The expected occupancy rate for carrier { carrier_id } is wrong.| + quit = if_abap_unit_constant=>quit-no ). + + cl_abap_unit_assert=>assert_equals( + act = num1 + exp = 999 + msg = |num1: { num1 }| + quit = if_abap_unit_constant=>quit-no ). + + cl_abap_unit_assert=>assert_equals( + act = num2 + exp = 0 + msg = |num2: { num2 }| + quit = if_abap_unit_constant=>quit-no ). + + ENDMETHOD. + + METHOD test_get_occ_rate_seam_fail. + "This method intentionally includes values to make the unit test fail. + + "Creating test data + DATA carrier_id TYPE zdemo_abap_fli-carrid VALUE 'CD'. + DATA(expected_occupancy_rate) = '60.00'. + + "Code injection + TEST-INJECTION select_flights. + seats_table = VALUE #( + ( carrid = carrier_id seatsmax = 100 seatsocc = 50 ) + ( carrid = carrier_id seatsmax = 200 seatsocc = 100 ) + ( carrid = carrier_id seatsmax = 300 seatsocc = 100 ) ). + END-TEST-INJECTION. + + TEST-INJECTION num1. + var = 1. + END-TEST-INJECTION. + + TEST-INJECTION num2. + END-TEST-INJECTION. + + "Calling method that is to be tested + ref_cut->get_occ_rate_test_seam( EXPORTING carrier_id = carrier_id + IMPORTING occupancy_rate = DATA(occupancy_rate) + num1 = DATA(num1) + num2 = DATA(num2) ). + + "AssertionS + cl_abap_unit_assert=>assert_equals( + act = occupancy_rate + exp = expected_occupancy_rate + msg = |The expected occupancy rate for carrier { carrier_id } is wrong.| + quit = if_abap_unit_constant=>quit-no ). + + cl_abap_unit_assert=>assert_equals( + act = num1 + exp = 1 + msg = |num1: { num1 }| + quit = if_abap_unit_constant=>quit-no ). + + cl_abap_unit_assert=>assert_equals( + act = num2 + exp = 123 + msg = |num2: { num2 }| + quit = if_abap_unit_constant=>quit-no ). + + ENDMETHOD. + +ENDCLASS. + +*********************************************************************** +* Test class ltc_test_doc_global_itf +* +* - Tests one method of the global class; uses constructor injection +* - In this case, a dependent-on component (DOC) has been identified (a +* database access). +* - A global interface exists to overcome the DOC. +* - A local test double class is created. It implements the global +* interface. The method implementation contains manually created test +* data. +* +* Notes on constructor injection: +* - This means that a test double is passed as a parameter to the instance +* constructor of the class under test. +* - An interface reference variable is declared in the private section of +* the class under test, and its type references the global interface. +* - A local test class is created here for the test double. It implements +* the interface method needed for the test. Note the PARTIALLY IMPLEMENTED +* addition to the interface. +* - In this method implementation, local test data are created. +* - The global class/class under test has the following instance constructor +* declaration: +* - Has an optional importing parameter for the DOC. +* - The parameter is typed with reference to the test double, i.e. an +* object of the test double is passed. +* - Instance constructor implementation: When the unit test is executed, an +* object of the test double is created in the test method. This object is +* then passed to the constructor. A check is implemented to determine if +* the reference variable is bound. It is indeed bound during the unit test, +* and the test double is injected. +*********************************************************************** + +*********************************************************************** +* Local test double class +*********************************************************************** +CLASS ltd_test_data_global_intf DEFINITION FOR TESTING. + PUBLIC SECTION. + + "Note: Usually, you must implement all non-optional methods of interfaces. + "Without the addition PARTIALLY IMPLEMENTED, there would be a syntax error. + INTERFACES zdemo_abap_get_data_itf PARTIALLY IMPLEMENTED. + +ENDCLASS. + +CLASS ltd_test_data_global_intf IMPLEMENTATION. + METHOD zdemo_abap_get_data_itf~select_flight_data. + + CLEAR flight_data. + + "Providing test data + flight_data = SWITCH #( carrier + WHEN 'EF' THEN VALUE #( ( carrid = carrier seatsmax = 100 seatsocc = 50 ) + ( carrid = carrier seatsmax = 200 seatsocc = 150 ) + ( carrid = carrier seatsmax = 300 seatsocc = 100 ) ) + WHEN 'GH' THEN VALUE #( ( carrid = carrier seatsmax = 350 seatsocc = 200 ) + ( carrid = carrier seatsmax = 350 seatsocc = 250 ) + ( carrid = carrier seatsmax = 300 seatsocc = 150 ) ) ). + + ENDMETHOD. + +ENDCLASS. + +*********************************************************************** +* Test class ltc_test_doc_global_itf +*********************************************************************** +CLASS ltc_test_doc_global_itf DEFINITION FOR TESTING + RISK LEVEL HARMLESS + DURATION SHORT. + + PRIVATE SECTION. + DATA: ref_cut TYPE REF TO zcl_demo_abap_unit_test, + ref_data_prov TYPE REF TO zdemo_abap_get_data_itf. + + METHODS: setup, + test_get_occ_rate_glo_if_ok FOR TESTING, + test_get_occ_rate_glo_if_fail FOR TESTING. + +ENDCLASS. + +CLASS ltc_test_doc_global_itf IMPLEMENTATION. + + METHOD setup. + + "Creating an instance of the local test double + ref_data_prov = NEW ltd_test_data_global_intf( ). + + "Instance is provided for the constructor injection + ref_cut = NEW #( ref_data_prov ). + + ENDMETHOD. + + METHOD test_get_occ_rate_glo_if_ok. + "The method to be tested calculates the occupancy rate of flights. + + "(1) Calling method that is to be tested + "Due to constructor injection, the test double is used. + DATA(act_occ_rate) = ref_cut->get_occ_rate_global_itf( 'EF' ). + + "Assertion + DATA(exp_value) = '50.00'. + + cl_abap_unit_assert=>assert_equals( + act = act_occ_rate + exp = exp_value + msg = |The expected occupancy rate for carrier EF is wrong.| + quit = if_abap_unit_constant=>quit-no ). + + "(2) Calling method that is to be tested + act_occ_rate = ref_cut->get_occ_rate_global_itf( 'GH' ). + + "Assertion + exp_value = '60.00'. + + cl_abap_unit_assert=>assert_equals( + act = act_occ_rate + exp = exp_value + msg = |The expected occupancy rate for carrier GH is wrong.| + quit = if_abap_unit_constant=>quit-no ). + + ENDMETHOD. + + METHOD test_get_occ_rate_glo_if_fail. + "This method intentionally includes values to make the unit test fail. + + "(1) Calling method that is to be tested + DATA(act_occ_rate) = ref_cut->get_occ_rate_global_itf( 'EF' ). + + "Assertion to fail + DATA(exp_value) = '40.00'. "correct: 50.00 + + cl_abap_unit_assert=>assert_equals( + act = act_occ_rate + exp = exp_value + msg = |The expected occupancy rate for carrier EF is wrong.| + quit = if_abap_unit_constant=>quit-no ). + + "(2) Calling method that is to be tested + act_occ_rate = ref_cut->get_occ_rate_global_itf( 'GH' ). + + "Assertion to fail + exp_value = '90.00'. "correct: 60.00 + + cl_abap_unit_assert=>assert_equals( + act = act_occ_rate + exp = exp_value + msg = |The expected occupancy rate for carrier GH is wrong.| + quit = if_abap_unit_constant=>quit-no ). + + ENDMETHOD. + +ENDCLASS. + +*********************************************************************** +* Test class ltc_test_local_itf +* +* - Tests one method of the global class; uses back door injection +* - In this case, a dependent-on component (DOC) has been identified (a +* database access). The method is similar to the one above. +* - There is no global interface to overcome the DOC. Instead, a local +* interface is created in the local types (CCIMP include): lif_get_data +* - Additionally, a local class is implemented in the CCIMP include that +* implements the local interface. This local class provides the data +* for the method call in the global class (get_occ_rate_local_itf). +* - A local test double class is created here. It implements the local +* interface. The method implementation contains manually created test +* data. +* - In addition, the test class includes a helper method to demonstrate +* the separation of code recurring tasks into separate methods. +* +* Notes on back door injection: +* - This means that a back door is created to inject a test double into +* the class under test. +* - This back door is implemented by granting friendship to the test +* class. +* This makes internal attributes of the class under test accessible +* to the test class. +* - In the production code, a reference variable with type reference to +* the data provider is declared in the private section. +* - When the unit test is executed, the private attribute of the class +* under test is changed. +* - An object of the test double is injected. +*********************************************************************** + +*********************************************************************** +* Local test double class +*********************************************************************** +CLASS ltd_test_data_local_itf DEFINITION FOR TESTING. + PUBLIC SECTION. + INTERFACES lif_get_data PARTIALLY IMPLEMENTED. +ENDCLASS. + +CLASS ltd_test_data_local_itf IMPLEMENTATION. + METHOD lif_get_data~select_flight_data. + + CLEAR flight_data. + + "Providing test data + flight_data = SWITCH #( carrier + WHEN 'IJ' THEN VALUE #( ( carrid = carrier seatsmax = 300 seatsocc = 200 ) + ( carrid = carrier seatsmax = 350 seatsocc = 200 ) + ( carrid = carrier seatsmax = 350 seatsocc = 300 ) ) + WHEN 'KL' THEN VALUE #( ( carrid = carrier seatsmax = 350 seatsocc = 300 ) + ( carrid = carrier seatsmax = 350 seatsocc = 250 ) + ( carrid = carrier seatsmax = 300 seatsocc = 250 ) ) ). + + ENDMETHOD. + +ENDCLASS. + +*********************************************************************** +* Test class ltc_test_doc_local_itf +*********************************************************************** +CLASS ltc_test_doc_local_itf DEFINITION FOR TESTING + RISK LEVEL HARMLESS + DURATION SHORT. + + PRIVATE SECTION. + + DATA: ref_cut TYPE REF TO zcl_demo_abap_unit_test, + ref_data_prov TYPE REF TO lif_get_data. + + METHODS: setup, + + "Helper method + assert_occ_rate IMPORTING carrier_id TYPE zdemo_abap_fli-carrid + occ_rate TYPE lif_get_data=>occ_rate, + + "Test methods + test_get_occ_rate_lo_itf_ok FOR TESTING, + test_get_occ_rate_lo_itf_fail FOR TESTING. + +ENDCLASS. + + +CLASS ltc_test_doc_local_itf IMPLEMENTATION. + + METHOD setup. + + ref_cut = NEW #( ). + + ref_data_prov = NEW ltd_test_data_local_itf( ). + + "Back door injection + ref_cut->data_provider_local_itf = ref_data_prov. + + ENDMETHOD. + + METHOD assert_occ_rate. + + DATA(act_occ_rate) = ref_cut->get_occ_rate_local_itf( carrier_id ). + + cl_abap_unit_assert=>assert_equals( + act = act_occ_rate + exp = occ_rate + msg = |The expected occupancy rate for carrier { carrier_id } is wrong.| + quit = if_abap_unit_constant=>quit-no ). + + ENDMETHOD. + + + METHOD test_get_occ_rate_lo_itf_ok. + + assert_occ_rate( carrier_id = 'IJ' + occ_rate = '70.00' ). + + assert_occ_rate( carrier_id = 'KL' + occ_rate = '80.00' ). + + ENDMETHOD. + + METHOD test_get_occ_rate_lo_itf_fail. + "This method intentionally includes values to make the unit test fail. + + assert_occ_rate( carrier_id = 'IJ' + occ_rate = '20.00' ). "correct: 70.00 + + assert_occ_rate( carrier_id = 'KL' + occ_rate = '30.00' ). "correct: 80.00 + + ENDMETHOD. + +ENDCLASS. + +*********************************************************************** +* Test class ltc_test_redef +* +* - Tests one method of the global class; uses constructor injection +* - In this case, a dependent-on component (DOC) has been identified (a +* database access). +* - There are no global and local interfaces to overcome the DOC. +* - A local test double class is created by redefining a method of +* the class under test. The method implementation contains manually +* created test data. +* +* Notes: +* - A local test double is created in a separate test class. +* - The test double is created by redefining a method of a class. +* - In this simple and self-contained example, the global class is +* deliberately not declared as a final class, so that inheritance +* from it is allowed. The global class implements a method that +* particularly serves the purpose of the test. It is there only +* to have a self-contained example and to keep the number +* of separate artifacts low. You might imagine that the method is +* contained in another global class, and the global class under test +* uses that method. +********************************************************************** + +*********************************************************************** +* Local test double class +*********************************************************************** +CLASS ltd_test_data_redef DEFINITION FOR TESTING +INHERITING FROM zcl_demo_abap_unit_test. + + PROTECTED SECTION. + METHODS select_flight_data REDEFINITION. + +ENDCLASS. + +CLASS ltd_test_data_redef IMPLEMENTATION. + METHOD select_flight_data. + + CLEAR flight_data. + + "Providing test data + flight_data = SWITCH #( carrier + WHEN 'UV' THEN VALUE #( ( carrid = carrier seatsmax = 100 seatsocc = 50 ) + ( carrid = carrier seatsmax = 200 seatsocc = 150 ) + ( carrid = carrier seatsmax = 300 seatsocc = 100 ) ) + WHEN 'WX' THEN VALUE #( ( carrid = carrier seatsmax = 350 seatsocc = 200 ) + ( carrid = carrier seatsmax = 350 seatsocc = 250 ) + ( carrid = carrier seatsmax = 300 seatsocc = 150 ) ) ). + + ENDMETHOD. + +ENDCLASS. + +*********************************************************************** +* Test class ltc_test_doc_redef +*********************************************************************** +CLASS ltc_test_doc_redef DEFINITION FOR TESTING + RISK LEVEL HARMLESS + DURATION SHORT. + + PRIVATE SECTION. + DATA ref_cut TYPE REF TO zcl_demo_abap_unit_test. + + METHODS: setup, + test_get_occ_rate_redef_ok FOR TESTING, + test_get_occ_rate_redef_fail FOR TESTING. + +ENDCLASS. + +CLASS ltc_test_doc_redef IMPLEMENTATION. + + METHOD setup. + + ref_cut = NEW ltd_test_data_redef( ). + + ENDMETHOD. + + METHOD test_get_occ_rate_redef_ok. + + DATA(act_occ_rate) = ref_cut->get_occ_rate_using_meth( 'UV' ). + + DATA(exp_value) = '50.00'. + + cl_abap_unit_assert=>assert_equals( + act = act_occ_rate + exp = exp_value + msg = |The expected occupancy rate for carrier UV is wrong.| + quit = if_abap_unit_constant=>quit-no ). + + act_occ_rate = ref_cut->get_occ_rate_using_meth( 'WX' ). + + exp_value = '60.00'. + + cl_abap_unit_assert=>assert_equals( + act = act_occ_rate + exp = exp_value + msg = |The expected occupancy rate for carrier WX is wrong.| + quit = if_abap_unit_constant=>quit-no ). + + ENDMETHOD. + + METHOD test_get_occ_rate_redef_fail. + + DATA(act_occ_rate) = ref_cut->get_occ_rate_using_meth( 'UV' ). + + DATA(exp_value) = '40.00'. "correct: 50.00 + + cl_abap_unit_assert=>assert_equals( + act = act_occ_rate + exp = exp_value + msg = |The expected occupancy rate for carrier UV is wrong.| + quit = if_abap_unit_constant=>quit-no ). + + act_occ_rate = ref_cut->get_occ_rate_using_meth( 'WX' ). + + exp_value = '90.00'. "correct: 60.00 + + cl_abap_unit_assert=>assert_equals( + act = act_occ_rate + exp = exp_value + msg = |The expected occupancy rate for carrier WX is wrong.| + quit = if_abap_unit_constant=>quit-no ). + + ENDMETHOD. + +ENDCLASS. diff --git a/src/zcl_demo_abap_unit_test.clas.xml b/src/zcl_demo_abap_unit_test.clas.xml new file mode 100644 index 0000000..1525fa3 --- /dev/null +++ b/src/zcl_demo_abap_unit_test.clas.xml @@ -0,0 +1,17 @@ + + + + + + ZCL_DEMO_ABAP_UNIT_TEST + E + ABAP cheat sheet: Unit test + 1 + X + X + 5 + X + + + +