Why & when to use a Builder Class

A Test Method is divided in the 4 Parts: Setup, Exercise, Verify and Teardown (Meszaros).

Often you see that a huge and complicated part is the Setup, where you replace Dependencies with Test-Doubles to test a Class in Isolation. If you have complex Dependencies it is very easy to pollute your Test-Class with hundreds of lines of setup Code. If you want to test different scenarios, with different Input e.g. from Stubs, it get’s even more worst.

One way to get around it would be the use of a mocking Framework that generates Code. I prefer self-written Stubs, Fakes or Mocks for different reasons.

To make Test-Classes shorter and more expressive I use often of Helper-Classes that use the “Builder Pattern“. They can be a local Class if it is used for a single Test, or a global Class if it can be reused in several other Test’s.

In the simplest case you just call the build Method, that creates a bound Instance to satisfy another Method to call – here with a Customer.

lcl_customer_builder=>new(

     )->build( ).

In simple human readable Text you can add Method’s that set data for the instance to build.

lcl_customer_builder=>new(

     )->with_name(    

          ‘Max Mustermann’

     )->with_customer_id(

          ‘1234’

     )->build( ).


Also it is very comfortable if Class to build has own Dependencies you have to satisfy. Inside the Builder you can simply set them to Default or Null Implementations.


Without the Builder Pattern the Code may have looked like this:

DATA: lo_customer             TYPE REF TO lif_customer,    

           lo_fake_company     TYPE REF TO lif_company.

lo_customer =
     zcl_customer_factory=>get_instance(

          )->get_customer( ‘1234’ ).

lo_customer->set_name( ‘Max Mustermann’ ).

lo_customer->set_company(

          zcl_company_factory=>get_company_null( )

     ).

An Example – Build a complex Fake Class

Test Class under Test and it’s dependency

In our Example the Class under Test ZCL_EXAMPLE_CLASS_UNDER_TEST uses the Interface ZIF_DATE_OBJ_LOOKUP and it’s Implementation ZCL_DB_DATE_OBJ_LOOKUP. For demonstration purposes it takes a Date and returns a generic Object Reference.

To test the Class ZCL_EXAMPLE_CLASS_UNDER_TEST in isolation we have to replace the use of ZCL_DB_DATE_OBJ_LOOKUP – that would access the Database.

Definition of ZIF_DATE_OBJ_LOOKUP

INTERFACE zif_date_obj_lookup
   PUBLIC .

   METHODS get_obj_for_date
     IMPORTING
       !i_date TYPE dats
     RETURNING
       value(r_obj) TYPE REF TO object .

ENDINTERFACE.

Our Class under Test ZCL_EXAMPLE_CLASS_UNDER_TEST

CLASS zcl_example_class_under_test DEFINITION
  
PUBLIC
   FINAL
  
CREATE PUBLIC .

  
PUBLIC SECTION.

     METHODS:

          constructor,

          do_stuff

               RETURNING value(r_value) TYPE i.

   PRIVATE SECTION.

     DATA mo_date_obj_lookup TYPE REF TO zif_date_obj_lookup .

ENDCLASS.                    “ZCL_EXAMPLE_CLASS_UNDER_TEST DEFINITION

CLASS zcl_example_class_under_test IMPLEMENTATION.

  METHOD constructor.
    
CREATE OBJECT me->mo_date_obj_lookup
      
TYPE zcl_db_date_obj_lookup.
  
ENDMETHOD.                   

   METHOD do_stuff.
     ” Uses the dependency ZIF_DATE_OBJ_LOOKUP
     me->mo_date_obj_lookup->get_obj_for_date( … ).

  
ENDMETHOD.

ENDCLASS.                

The Fake Date Object Lookup Class

The Class lcl_fake_date_obj_lookup is the fake that will replace the Dependency for the class ZCL_DB_DATE_OBJ_LOOKUP. Instead the Original that use the Database, the Fake uses a Dictionary Class that returns an Object per Date.

CLASS lcl_fake_date_obj_lookup DEFINITION FINAL.

   PUBLIC SECTION.

     INTERFACES:
       zif_date_obj_lookup
.

     METHODS:
       constructor
        
IMPORTING
           io_dictionary
TYPE REF TO zif_dictionary.

   PRIVATE SECTION.

     DATA:
       mo_fake_data_dictionary
TYPE REF TO zif_dictionary.

ENDCLASS.                   

CLASS lcl_fake_date_obj_lookup IMPLEMENTATION.

   METHOD constructor.
     me
->mo_fake_data_dictionary = io_dictionary.
  
ENDMETHOD.                   

   METHOD zif_date_obj_lookup~get_obj_for_date.
     r_obj
=
       me
->mo_fake_data_dictionary->get( |{ i_date }| ).
  
ENDMETHOD.                   

ENDCLASS.                   


The Fake Date Object Lookup – Builder


The Fake is build with the helper Class lcl_fake_date_lookup_bldr. This class contains logic to simplify the creation of Fakes with different Scenarios.  

CLASS lcl_fake_date_obj_lookup_bldr DEFINITION FINAL.

   PUBLIC SECTION.

     CLASS-METHODS:
      
new
         RETURNING
value(ro_self) TYPE REF TO lcl_fake_date_obj_lookup_bldr.

     METHODS:
       constructor
,

       start_at_date
         IMPORTING
           i_date
TYPE dats
         RETURNING
value(ro_self) TYPE REF TO lcl_fake_date_obj_lookup_bldr,
       ends_at_date
        
IMPORTING
           i_date
TYPE dats
         RETURNING
value(ro_self) TYPE REF TO lcl_fake_date_obj_lookup_bldr,
       returns_in_default
        
IMPORTING
           io_object
TYPE REF TO object
         RETURNING
value(ro_self) TYPE REF TO lcl_fake_date_obj_lookup_bldr,
       returns_at_date
        
IMPORTING
           i_date   
TYPE        dats
           io_object
TYPE REF TO object
         RETURNING
value(ro_self) TYPE REF TO lcl_fake_date_obj_lookup_bldr,
       build
         RETURNING
value(ro_fake_date_obj_bldr) TYPE REF TO zif_date_obj_lookup.

   PRIVATE SECTION.

     DATA:
           m_date_start                
TYPE dats,
           m_date_end                  
TYPE dats,
           mo_defalt_obj               
TYPE REF TO object,
           mo_dictionary_for_fake 
TYPE REF TO zif_dictionary.

     METHODS:
       get_value_for_date
        
IMPORTING
           i_date
TYPE dats
         RETURNING
value(ro_obj) TYPE REF TO object,
       set_value_for_date
        
IMPORTING
           i_date   
TYPE        dats
           io_object
TYPE REF TO object
         RETURNING
value(ro_obj) TYPE REF TO object,
       is_value_for_date_set
        
IMPORTING
           i_date
TYPE dats
         RETURNING
value(r_is_set) TYPE abap_bool,
       get_dict_with_default_ret_obj
         RETURNING
value(ro_fake_dictionary) TYPE REF TO zif_dictionary.

ENDCLASS.                   

CLASS lcl_fake_date_obj_lookup_bldr IMPLEMENTATION.

  
METHOD new.
    
CREATE OBJECT ro_self.
  
ENDMETHOD.                   

  
METHOD constructor.
    
CREATE OBJECT me->mo_dictionary_for_fake
      
TYPE zcl_dictionary.
  
ENDMETHOD.                   

  
METHOD start_at_date.
     me
->m_date_start = i_date.
     ro_self
= me.
  
ENDMETHOD.                   

   METHOD ends_at_date.
     me
->m_date_end = i_date.
     ro_self
= me.
  
ENDMETHOD.                   

   METHOD returns_in_default.
     me
->mo_defalt_obj = io_object.
     ro_self
= me.
  
ENDMETHOD.                   

   METHOD returns_at_date.
     me
->set_value_for_date(
         i_date   
= i_date
         io_object
= io_object
      
).
     ro_self
= me.
  
ENDMETHOD.                  

   METHOD build.
    
ASSERT me->mo_defalt_obj  IS BOUND.
    
ASSERT me->m_date_end > me->m_date_start.

     CREATE OBJECT ro_fake_date_obj_bldr
      
TYPE lcl_fake_date_obj_lookup
      
EXPORTING
         io_dictionary
= me->get_dict_with_default_ret_obj( ).
  
ENDMETHOD.                   

   METHOD get_value_for_date.
     ro_obj
=
       me
->mo_dictionary_for_fake->get( |{ i_date }| ).
  
ENDMETHOD.                   

   METHOD set_value_for_date.
     me
->mo_dictionary_for_fake->add(
        i_key  
= |{ i_date }|
        i_value
= io_object
     
).
  
ENDMETHOD.                   

   METHOD is_value_for_date_set.
     r_is_set
=
       me
->mo_dictionary_for_fake->exists( |{ i_date }| ).
  
ENDMETHOD.                   

   METHOD get_dict_with_default_ret_obj.
    
DATA: lv_current_date TYPE dats.

     lv_current_date = me->m_date_start.

     WHILE lv_current_date <= me->m_date_end.

       IF me->is_value_for_date_set( lv_current_date ) = abap_false.
         me
->set_value_for_date(
             i_date   
= me->m_date_start
             io_object
= me->mo_defalt_obj
          
).
      
ENDIF.

       lv_current_date = lv_current_date + 1.
    
ENDWHILE.

     ro_fake_dictionary = me->mo_dictionary_for_fake.
  
ENDMETHOD.                   

ENDCLASS.                   



The Test Test-Class

CLASS ltcl_example_class_under_test DEFINITION
    
FOR TESTING
     DURATION SHORT
     RISK LEVEL HARMLESS
     FINAL
.

   PRIVATE SECTION.

     METHODS:
       get_test_instance
        
IMPORTING
           io_date_obj_lookup
TYPE REF TO zif_date_obj_lookup
         RETURNING
value(ro_fcut) TYPE REF TO zcl_example_class_under_test,

       test_3_days_with_one_problem FOR TESTING,
       test_10_days_with_2_problems
FOR TESTING.

     “… more complex tests

ENDCLASS.                   

I decided to inject the Dependency by overwriting the Member Attribute after Construction. This way allows you to replace also local dependencies e.g. if you use a local Interface LIF_DB to extract SQL Commands to a local Class. And it is easy to create an Instance of the Class. To get access to the private Member Attribute I made the Test-Class to a Friend of the Class under Test.
But this has the Disadvantage that the Test knows internals of the Class Implementation and that the Constructor cannot contain Logic that uses the to replaced Dependency. A Clean way is to add the Dependency to the Constructor Parameters.

CLASS zcl_example_class_under_test DEFINITION LOCAL FRIENDS
            ltcl_example_class_under_test
.

The Test Implementation is reduced into the simple creation of the Instance – and the Assertions. Every Assert Statement contains the Builder Class – which tells the reader in simple readable Text the preconditions. I always try to reduce local Variables to a minimum to keep Methods short & clean.

CLASS ltcl_example_class_under_test IMPLEMENTATION.

   METHOD get_test_instance.

     CREATE OBJECT ro_fcut.
     ro_fcut
->mo_date_obj_lookup = io_date_obj_lookup.

   ENDMETHOD.                   

   METHOD test_3_days_with_one_problem.

     cl_aunit_assert=>assert_equals(
        
exp = 3
         act
= me->get_test_instance(
                   lcl_fake_date_obj_lookup_bldr
=>new(
                    
)->start_at_date(
                      
‘20140101’
                    
)->ends_at_date(
                      
‘20140103’
                    
)->returns_in_default(
                       zcl_dummy2
=>new( )
                    
)->returns_at_date(
                       i_date   
= ‘20140102’
                       io_object
= zcl_dummy1=>new( )
                    
)->build( )
                
)->do_stuff( )
      
).

   ENDMETHOD.                   

   METHOD test_10_days_with_2_problems.

     cl_aunit_assert=>assert_equals(
      
exp = 5
       act
= me->get_test_instance(
                   lcl_fake_date_obj_lookup_bldr
=>new(
                    
)->start_at_date(
                      
‘20140201’
                    
)->ends_at_date(
                      
‘20140210’
                    
)->returns_in_default(
                       zcl_dummy2
=>new( )
                    
)->returns_at_date(
                       i_date   
= ‘20140202’
                       io_object
= zcl_dummy1=>new( )
                    
)->returns_at_date(
                       i_date   
= ‘20140209’
                       io_object
= zcl_dummy1=>new( )
                    
)->build( )
                
)->do_stuff( )
    
).

   ENDMETHOD.                   

ENDCLASS.                   

Conclusion

The Builder Pattern is another Tool in my Toolkit for Test-Driven Development. Complex Dependencies are often a signal that there’s something wrong with your Class Design, but you often have to test Classes that must be tested with varying values – and a builder can help you with that.

The Builder Class in the Example above contains quite a bit of logic – and untested logic as dependency for your class under Test may result in brittle Test’s. So you may have to test that logic as precondition before the actual Test’s. If you use a complex global Builder Class, it would contain own Test’s in it’s local Test-Class Include.

To report this post you need to login first.

Be the first to leave a comment

You must be Logged on to comment or reply to a post.

Leave a Reply