ABAP Test Double フレームワークの使用方法
ABAPのTest DoubleのフレームワークであるABAP Unitを用いることで、テスト対象(以下CUT)が依存する先のクラス(以下DOC)のUnit test時のふるまいを変えることができます。ただし、依存性注入パターンに基づいたプロダクションコードを前提としています。
補足
Test DoubleのDoubleは、影武者という意味です。このエントリでは、依存性注入とはコンストラクタ・インジェクションです。つまり、CUTのクラスのコンストラクタに、引数の型としてDOCのオブジェクトのインタフェースをとります。
例
zcl_greetings
がテスト対象のクラスだとします。一方、 zcl_timlo
は、 zcl_greetings
から呼び出されます。本来、 zcl_timlo
はテストする対象ではありません。しかし、普通にテストしようとすると、 zcl_timlo
もついてきてしまいます。これを、「 zcl_greetings
は、zcl_timlo
に依存する」といいます。
この2つのクラスにインタフェースを定義します。zcl_greetings
に対して zif_greetings
、 zcl_timlo
に対して zif_timlo
を作成します。
上記は、zcl_greetings
のクラスの中に、zcl_timlo
というクラス名を書いています。お互いのクラス名を使わず、インタフェース名だけを使うように変えてみます。
下図は、インタフェースを示す (I) と、クラスを示す (C) が交互になります。クラスどうしは線がつながっていません。ABAP のTest Doubleはこの状態を前提としています。
依存性注入
では、zcl_greetings
は、zcl_timlo
をどうやって使えばいいかというと、本番の場合にはABAPならレポートプログラム(main
)、テストの場合にはテストクラスのsetup
メソッドか、各テストメソッドに書きます。
timlo = new zcl_timlo( ).
cut = new zcl_greetings( timlo ). " コンストラクタの引数にDOCをセット
timlo ?= cl_abap_testdouble=>create( 'zif_timlo' ).
cut = new zcl_greetings( timlo ). " コンストラクタの引数にDOCをセット
例題
午前に"Good morning!"、午後に"Good afternoon!"と返すプログラムを考えます。期待結果が実行した時刻によって変るのですが、テストが午前中にPassし、午後にFailするのでは困ります。
狙いは、Test doubleによって、zcl_timlo
のふるまいを変えることで、午前と午後の状態を作ることです。1
zcl_timlo
のメソッド get_local_time
は本来sy-timelo
を返します。これを使う代わりに、フレームワークで作ったインスタンスに代役を務めさせ、固定値を返すようにします。
プロダクションコード:インタフェース
インタフェースを定義します。
INTERFACE zif_timlo
PUBLIC.
METHODS get_local_time
RETURNING VALUE(r_result) TYPE t.
ENDINTERFACE.
クラスの定義と実装
ローカル時刻を返すクラスを実装します。
CLASS zcl_timlo DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES zif_timlo.
ENDCLASS.
CLASS zcl_timlo IMPLEMENTATION.
METHOD zif_timlo~get_local_time.
r_result = sy-timlo.
ENDMETHOD.
ENDCLASS.
時刻が午前の時には文字列Good morning、午後には文字列Good afternoonを返すクラスおよびメソッドを実装します。引数に zif_timlo
の参照をとるコンストラクタを用いています。コンストラクタ・インジェクションによる依存性注入です。
CLASS zcl_greetings DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES zif_greetings.
METHODS constructor
IMPORTING io_timlo TYPE REF TO zif_timlo.
PRIVATE SECTION.
CONSTANTS noon TYPE t VALUE '120000'.
DATA timlo TYPE REF TO zif_timlo.
ENDCLASS.
CLASS zcl_greetings IMPLEMENTATION.
METHOD zif_greetings~say_greetings.
IF timlo->get_local_time( ) < noon.
r_result = `Good morning!`.
ELSE.
r_result = `Good afternoon!`.
ENDIF.
ENDMETHOD.
METHOD constructor.
timlo = io_timlo.
ENDMETHOD.
ENDCLASS.
なお、zcl_greetings
についても、インタフェースが定義されています。
INTERFACE zif_greetings
PUBLIC.
METHODS say_greetings
RETURNING VALUE(r_result) TYPE string.
ENDINTERFACE.
テスト
テストは、zcl_greetings
のローカルクラスlcl_greetings
として定義します。2種類のテストメソッドがあり、それぞれ午前のケース、午後のケースを表します。
CLASS lcl_greetings DEFINITION FINAL
FOR TESTING RISK LEVEL HARMLESS DURATION SHORT.
PRIVATE SECTION.
METHODS _01_say_morning_greetings FOR TESTING.
METHODS _02_say_afternoon_greetings FOR TESTING.
ENDCLASS.
各テストメソッド で、cl_abap_testdouble=>create()
を呼び出します。これは、DOCであるzcl_timlo
のかわりに、同じインタフェースのインスタンスが生成をおこないます。ただ、生成されるオブジェクトはObject型なので、?=
をつかってキャストをします。=
だと、静的にzif_timlo
型に割り当てられずエラーとなります。?=
を使うと、有効化時のチェックを回避できます。
DATA double_timlo TYPE REF TO zif_timlo.
double_timlo ?= cl_abap_testdouble=>create( 'zif_timlo' ).
cl_abap_testdouble=>configure_call( double_timlo )
でreturn値を設定します。どのメソッドが対象かについて、一度メソッドをコールすることでテストフレームワークに知らせます。
METHOD _01_say_morning_greetings.
DATA double_timlo TYPE REF TO zif_timlo.
DATA cut TYPE REF TO zif_greetings.
double_timlo ?= cl_abap_testdouble=>create( 'zif_timlo' ).
cut = NEW zcl_greetings( double_timlo ).
cl_abap_testdouble=>configure_call( double_timlo )->returning( '11:00:00' ).
double_timlo->get_local_time( ). " 一度空呼び
cl_abap_unit_assert=>assert_equals( exp = 'Good morning!' "期待値
act = cut->say_greetings( ) "テストの結果
msg = 'Assert: 01 greeting should be good morning' ).
ENDMETHOD.
あらためてテスト全体
CLASS lcl_greetings DEFINITION FINAL
FOR TESTING RISK LEVEL HARMLESS DURATION SHORT.
PRIVATE SECTION.
METHODS _01_say_morning_greetings FOR TESTING.
METHODS _02_say_afternoon_greetings FOR TESTING.
ENDCLASS.
CLASS lcl_greetings IMPLEMENTATION.
METHOD _01_say_morning_greetings.
DATA double_timlo TYPE REF TO zif_timlo.
DATA cut TYPE REF TO zif_greetings.
double_timlo ?= cl_abap_testdouble=>create( 'zif_timlo' ).
cut = NEW zcl_greetings( double_timlo ).
cl_abap_testdouble=>configure_call( double_timlo )->returning( '11:00:00' ).
double_timlo->get_local_time( ).
cl_abap_unit_assert=>assert_equals( exp = 'Good morning!'
act = cut->say_greetings( )
msg = 'Assert: 01 greeting should be good morning' ).
ENDMETHOD.
METHOD _02_say_afternoon_greetings.
DATA double_timlo TYPE REF TO zif_timlo.
DATA cut TYPE REF TO zif_greetings.
double_timlo ?= cl_abap_testdouble=>create( 'zif_timlo' ).
cut = NEW zcl_greetings( double_timlo ).
cl_abap_testdouble=>configure_call( double_timlo )->returning( '13:00:00' ).
double_timlo->get_local_time( ).
cl_abap_unit_assert=>assert_equals( exp = 'Good afternoon!'
act = cut->say_greetings( )
msg = 'Assert: 02 greeting should be good afternoon' ).
ENDMETHOD.
ENDCLASS.
まとめ
cl_abap_testdoubleを用いるためには、DOCのインタフェースを使ってソースコード上の依存性を排除したうえで、テストクラスからテストダブルのインスタンスを渡せるような設計にしておく必要があります。このためには、テスト容易性のために、本番コードを変える必要があります。ただ、その前提であれば、ABAP UnitのTest Doubleは、簡易に利用することができる方法だと思います。ローカルクラスなどにテスト用の依存先を実装する方法と組み合わせる方法も理解したうえで、適切に使い分けることが必要だと思います。
-
ABAPの標準でついてくるフレームワークなら、
sy-timlo
を書き換える仕組みが欲しいところですが、そのようなことはできません。テスト対象クラスのローカルクラスでテストする場合に限定すればTest Seamを使うという手もありますが、ここでは取り扱いません(注:ご指摘ありがとうございます)。 ↩