English Русский 中文 Español Deutsch Português
ユニットテストの助けを借りたコードのクオリティー向上

ユニットテストの助けを借りたコードのクオリティー向上

MetaTrader 4 | 22 4月 2016, 15:38
1 393 0
Андрей
Андрей

はじめに

MQL4 でプログラミングする間、出来上がった MQL4 プログラムいくつかに手を加え、自分独自のプログラムを何十件と作成する幅広い経験をしました。結果、MQL4 は低レベルのプログラムを作成するのにひじょうに好ましい環境である、という結論に至りました。以下にその理由を挙げています。

  1. MetaTrader 4 には組み込みデバッガがない。エラー検索はときとしてひじょうに面倒な手続きです。
  2. MQL4 には C++ やJava のような機能に対処する例外がありません。
  3. MQL4 プログラムはコードのクオリティーよりも考えをコミットするため急いで作成されることが多いものです。

そういったすべてがコードのクオリティーを低下させ、それが以下のような問題につながるのです。

  1. アドバイザー操作のエラー、誤った操作アルゴリズム(特に実アカウントでは重大です)
  2. 実行速度の低下最適化の速度のひじょうな低下
  3. エラー処理の不備エキスパートアドバイザーが動作しないことが判明する

以上ははすべて長い経験のある MQL4 プログラマ―に関することではないことを注意申し上げたいしたいと思います。スキルのあるプログラマーは高品質なコードを作成する方法を見出すものです。

私の主要な仕事がソフトウェアの品質テストに関連しているため、MQL4 プログラムの検証とデバッグに関する事柄にはなんでも興味があったのです。私が見つけたこの問題に関する記事の数はわずかなものでした。そのため、私がプログラムのクオリティー向上方法の一つをここで説明したいと思います。この話題がおもしろいと実証されたら、その他の問題について次稿で考察します。



クオリティーに関するいくつかの理論

すこしグーグル検索をすると、以下のようなものを見つけることができます。

クオリティーは生産物の特性と特徴の複合体で、この生産物に条件的なまたは予想されるニーズを満たす機能を与えるものです。

ソフトウェアにかんする限り、購入者のニーズに合い、それが担う機能を満たす場合、そのプログラムはハイクオリティであるとみなすことができます。

高品質のプログラム作成には通常2タイプの活動が必要です。

  • クオリティー保証-不具合発生を妨げるためにとられる手段
  • クオリティー管理-出来上がったプログラムの品質管理。QA が役におもに立たない場合の不具合検出コードの不具合は検出されないとほとんど排除不可能であることを理解することが必要です。そしてもちろん特に不都合なのは、顧客が不具合を見つけるような場合です。。。

クオリティー保証は複雑な問題です。それはプログラマ―の快適な作業場所から始まり、複雑なビジネス手順を実装するに至るまでの数多くのタスクにかかわっています。この問題にはまだ触れるつもりはありません。ここではクオリティー管理についてお話します。

クオリティー管理に注意を払うほど、プログラムが魔法のように動作する可能性が高まります。理論的には、クオリティー管理(または別の言い方をすると、検証)は開発の各段階で実施する必要があります。

  1. 技術仕様検証-誤った技術仕様を基にまともに動作するプログラムを作成することは不可能です。
  2. ソースコード見直し。誤り、非効率なコード、コーディングルール違反、明らかなエラーの検索。
  3. 自動モードでプログラムの個別関数検証(ユニットテスト)。
  4. マニュアルモードでの完成したプログラム検証。プログラムが正常に動作している場合の人的(テスター)チェック。
  5. 自動モードでのプログラム検証(自動羽化検証)。. ロボットがプログラムクオリティーを自分でテストするときです。夢のように聞こえますが、それが役に立つこともあります。
  6. 顧客によるプログラム検証。

などです。ひじょうに数多くの検証タイプがあります。

そしてユニットテストはもっとも興味深いものに入ります。



ユニットテストに関する理論

Google はユニットテストの定義を以下のように提供します。ユニットテストとは、プログラマ―がプログラムの残り部分におけるユーザビリティについてソースコードの個別単位(ブロック)を確認するアプリケーション検証の方法である。ユニットとは検証に適するプログラムの最小部分を言います。アプリケーション言語(MQL4 を含め)では、個別の関数がユニットと見なされます。

ほとんどの場合、ユニットテストは自動で行われます。別の言い方をすれば、異なるパラメータを持つ検証済み関数を呼ぶプログラムが作成され、このプログラムが関数により引き戻される値が真であるか否かを占めるレポートを作成します。

ユニットテストは以下の理由によりひじょうに有用です。

  1. 障害が検出される場合、ただ一つの関数が検証されることでその原因を簡単に発見することができる。全アプリケーションで障害が検出される場合、問題を起こしている関数を見つけ出すために余分な時間をいくらか使わなければならなくなります。
  2. 不具合が排除されるかどうか確認することはひじょうに簡単です。自動ユニットテストを一度実行するだけです。アプリケーション全体を再起動する必要はありません。たとえば、ほとんど再現がむつかしい明確に稀なきっかけでエラーがいくつか発生する可能性があります。ユニットテストはこの問題を排除してくれます。
  3. 何か問題になるかもしれないと懸念することなく簡単に関数コードを最適化することができるのです。ユニットテストはつねに関数が正常に動作するかどうか示してくれます。
  4. また、すぐにそれとははっきりしないが顧客のところで現れる可能性があり、検索とデバッグに長時間を要するような問題を検出します。
  5. ユニットテストが最初に作り出され関数が作成されるとき、最新のテスト駆動方法が使用されます。関数はユニットテストが通過するまで作成されます。私は初めてこの方法を C++ アプリケーションの一つで試し、それがすぐれていることが実証されました。作成の最後で関数のユーザビリティについて完全に自信を持ち、本物の喜びを感じました。そしてプログラムでそれらをさらに使用しても非の打ちどころがありませんでした。

それがどのように表記されるか見ます。平方根を求めるための関数を作成したと仮定します。
y=sqrt(x)

そして、以下のアルゴリズムに従ってそれが動作するか検証するためにもう一つ別の関数を作成します。

  • verify that sqrt(-1) == error
  • verify that sqrt(0) == 0
  • verify that sqrt(0.01) == 0.1
  • verify that sqrt(1) == 1
  • verify that sqrt(4) == 2
  • verify that sqrt(7) == 2.6....

メインの関数を作成する前に検証の関数を作成します。したがって、作成済み関数が満たす要件を決定します。それがテスト駆動方法の使用です。そして、ユニットテストが申し分のない動作を示して初めて確かにこの関数をメインのプログラムで使用することができます。

ただし疑問がまだ一つ解決されないまま残っています。検証済み関数に対して検証パラメータセットをどのように選択するのでしょうか?もちろん、可能な値はすべて使用する必要がありますが、ほとんどすべての場合、それは不可能、または労力がかかりすぎます。検証値選択に関して最新の記事を書くことができるほどです。ここでは、一般的なヒントをいくつか提供します。

  1. 正確なデータだけでなく、エラーにつながるようなデータも使用する必要があります。関数が課せられたつとめだけ果たすことのみならず、エラー処理でどのくらい優れているか確認する必要があるためです。
  2. そのため境界値を採用する必要があります。たとえば、値範囲が 0 ~100 だとすると、0 および 100 の値を使うべきです。入力データが行で構成されている場合、空白行や最長行を試行すべきです。
  3. 許可されているマークを超える値が使用されるべきなのです。前のポイントから例を見ると、値 101 と -1 が使用され、行に対しては最大値+1が使用されます。
  4. 関数のふるまい方が同様である、同等な値のサブセット(同値クラス)に対して可能な値すべての多数を破ろうとする必要があります。すべてのクラスに対して1つの値が選択されます。たとえば、sqrt(4) と sqrt(9)を両方チェックすることに意味はありません。sqrt(4) と sqrt(5)をチェックする方が関心がひかれます。その方が関数が不合理な値を返すからで、同時に前出の場合は整数値を返すからです。
  5. 関数が分岐(if, switch)を伴う場合、それらがそれぞれ確かにユニットテストで処理されるようにする必要があります。

これは明確な例にて次の章で説明します。



ユニットテスト作成についての練習

トレーニングのゴールを設定します。タスクがエントリで配列を2つ受け取る関数を持つライブラリを作成することだとします。この関数は第1の配列から第2の配列にないエレメントを削除します。結果、第1の配列は第2の配列のサブセットとなります。

そこで関数のプロトタイプを決定します。

void CreateSubset(int & a1[], int a2[]);

関数作成に対してテスト駆動方法を試してみます。テストデータセットを決めます。以下を行うため、複数の入力データの等価クラスをマークします。

  1. 両方の配列が空である。
  2. A1 は空、A2 にはエレメントがある。
  3. A1 にはエレメントがあり、A2 は空。
  4. 両配列が同様のエレメントセットを持ち、同様のサイズである。
  5. A1 には A2 にはないエレメントがある。
  6. A1 のエレメントの一部は A2 にもあり、A2 の一部は A1 に含まれる(どちらにも交点がある)。
  7. A1 エレメントはすべて A2 にあるが、A2 のサイズの方が大きい。
  8. A1 エレメントのごく一部が A2 にある。また、エレメントは配列じゅうに散らばっている。
  9. A1 エレメントのごく一部が A2 にある。また、エレメントは配列の最初に集中している。
  10. A1 エレメントのごく一部が A2 にある。また、エレメントは配列の最後に集中している。

われわれの関数が全10ケースについてうまく動作する場合、この関数をしようするエキスパートは不完全であることで問題を抱えることはないと確証できます。ただし、何かを100%検証することは不可能で、なにか潜在的な不具合はつねに残る可能性があることを理解する必要があります。

便宜のために小規模な mql4unit ライブラリを作成しました。そしてそこにユニットテストに必要な関数を入れました。

//-------------------------------------------------------------------+

//Current test conditions are kept by the global variables
//-------------------------------------------------------------------+
int tests_passed;    //Number of successful tests
int tests_failed;    //Number of unsuccessful tests
int tests_total;     //Total number of tests

string test_name;    //Test name

//-------------------------------------------------------------------+
//The function initializes test environment for one test
//-------------------------------------------------------------------+
void UnitTestStart(string TestName)
{

   test_name = TestName;
   tests_passed = 0;
   tests_failed = 0;
   tests_total = 0;
   Print("*--------------------------------------------------*");

   Print("Starting unit test execution ", test_name);
}

//-------------------------------------------------------------------+
//the function is called at the end of the test. Brings back true if all the tests
//are successful. Otherwise - False.
//-------------------------------------------------------------------+
bool UnitTestEnd()
{
   if (tests_failed == 0)

   {
      Print("HURRAY!!! ", test_name, " PASSED. ", tests_passed, " tests are successful.");
   }
   else
   {

      Print(":((( ", test_name, " FAILED. ", tests_passed,"/",tests_total, " tests are successful.");   
   }
   Print("*--------------------------------------------------*");
}


//-------------------------------------------------------------------+
//The function executes the test for two arrays of int type
//Brings back true, if the arrays are equal
//-------------------------------------------------------------------+
bool TestIntArray(int actual[], int expected[]){

   tests_total++;
   //Comparing arrays' sizes
   if (ArraySize(actual) != ArraySize(expected))
   {
      Print("Test #", tests_total," ERROR. Array size ", ArraySize(actual), " instead of ", ArraySize(expected));

      tests_failed++;
      return(false);      
   }
   //Then comparing element by element
   for (int i=0; i<ArraySize(actual);i++)

   {
      if (actual[i]!=expected[i]){
         Print("Test #", tests_total," ERROR. Element value #",i,"=", actual, " instead of ", expected);
         tests_failed++;

         return(false);
      }
   }
   //If all the elements are equal, the test is passed
   Print("Test #", tests_total," OK: Passed!");  

   tests_passed++;
   return(true);
}
Let's create "mytests" test script with an empty body of our function. Create test function in it and describe all unit tests in it.
bool Test()
{
   UnitTestStart("CreateSubset function testing");
   Print("1. Both arrays are empty.");

   int a1_1[], a1_2[];
   int result_1[]; //Waiting for an empty array as a result of the function execution
   CreateSubset(a1_1, a1_2);
   TestIntArray(a1_1, result_1);
   
   Print("2. A1 is empty, A2 contains the elements");

   int a2_1[], a2_2[] = {1,2,3};
   int result_2[]; //Waiting for an empty array as a result of the function execution
   CreateSubset(a2_1, a2_2);

   TestIntArray(a2_1, result_2);

   Print("3. A1 contains the elements, A2 is empty");
   int a3_1[] = {1,2,3}, a3_2[];

   int result_3[]; //Waiting for an empty array as a result of the function execution
   CreateSubset(a3_1, a3_2);
   TestIntArray(a3_1, result_3);

   Print("4. Both contain similar set of the elements and have similar size");
   int a4_1[] = {1,2,3}, a4_2[] = {1,2,3};

   int result_4[] = {1,2,3}; //Waiting for an unchanged array as a result of the function execution
   CreateSubset(a4_1, a4_2);
   TestIntArray(a4_1, result_4);

   Print("5. A1 contains the elements that are not present in A2");

   int a5_1[] = {4,5,6}, a5_2[] = {1,2,3};
   int result_5[]; //Waiting for an empty array as a result of the function execution

   CreateSubset(a5_1, a5_2);
   TestIntArray(a5_1, result_5);
   
   Print("6. Part of the elements in A1 are present in A2, A2 part is contained in A1 (both multitudes have an intersection)");
   int a6_1[] = {1,2,3,4,5,6,7,8,9,10}, a6_2[] = {3,5,7,9,11,13,15};

   int result_6[] = {3,5,7,9}; //Waiting for arrays intersection as a result of the function execution
   CreateSubset(a6_1, a6_2);
   TestIntArray(a6_1, result_6);

   
   Print("7. All A1 elements are present in A2, but A2 size is bigger");
   int a7_1[] = {3,4,5}, a7_2[] = {1,2,3,4,5,6,7,8,9,10};

   int result_7[] = {3,4,5}; //Waiting for arrays intersection as a result of the function execution
   CreateSubset(a7_1, a7_2);
   TestIntArray(a7_1, result_7);
   

   Print("8. A1 エレメントのごく一部が A2 にある。Besides, the elements are scattered all over an array.");
   int a8_1[] = {1,2,3,4,5,6,7,8,9,10}, a8_2[] = {2,5,9};

   int result_8[] = {2,5,9}; //Waiting for arrays intersection as a result of the function execution
   CreateSubset(a8_1, a8_2);
   TestIntArray(a8_1, result_8);
   

   Print("9. A1 エレメントのごく一部が A2 にある。Besides, the elements are concentrated at an array leader.");
   int a9_1[] = {1,2,3,4,5,6,7,8,9,10}, a9_2[] = {1,2,3};

   int result_9[] = {1,2,3}; //Waiting for arrays intersection as a result of the function execution
   CreateSubset(a9_1, a9_2);
   TestIntArray(a9_1, result_9);

   Print("10. A1 エレメントのごく一部が A2 にある。Besides, the elements are concentrated at an array's end.");

   int a10_1[] = {1,2,3,4,5,6,7,8,9,10}, a10_2[] = {8,9,10};

   int result_10[] = {8,9,10}; //Waiting for arrays intersection as a result of the function execution
   CreateSubset(a10_1, a10_2);
   TestIntArray(a10_1, result_10);
   

   return (UnitTestEnd());
}

ユニットテストを実行するために、主要関数で Test 関数を呼び出しスクリプトを実行する必要があります。

われわれのテストを実行します。

見てのとおり、結果は期待したものではありません。それは驚くことではありません。というのも関数がまったく準備できていないからです。ただ、それでも!10件中4件のテストが問題なく通過しています。それは、理論的に関数が空であることを見のがしている可能性があることを意味します。いくつかの場合には正常に動作したであろうからです。

言い換えれば、誤った関数が正常に動作する入力データのサブセットがある可能性があるのです。プログラマーがうまくいくようなテストデータのみを採用していたら、空の関数はあっさり顧客のもとに届いてしまいます。

次に CreateSubset 関数自体を作成します。ここではこの関数の効率性と見栄えについては語りません。

void CreateSubset(int & a1[], int a2[]){
   int i=0;

   while(i<ArraySize(a1)){
      bool b_exist = false;
      for (int j=0; j<ArraySize(a2);j++){

         if (a1[i] == a2[j]) b_exist = true;
      }
      if (!b_exist){
         for (j=i; j<ArraySize(a1)-1;j++){
            a1[j] = a1[j+1];   

         }
         ArrayResize(a1, ArraySize(a1)-1);
      }else{
         i++;
      }
   }
}
再度検証を実行します。

関数はどこからでも実行可能です。エキスパート内部で決定され、初期化中実行されます。個別のモジュールが処理される場合、1つまたは複数のテスト関数がその内部で決定され、スクリプトから呼ばれます。ここで少し空想します。

もちろん、理想的なバリアントとしては、ライブラリをコンパイルした直後にユニットテストを実行する機能を持つことでしょうが、これを MQL4 で行うことが可能かどうかまだわかっていません。できない可能性が高いと思われます。する方法をご存じなら、ご一報ください。

テスト実行後は毎回、安堵のため息をつき、すべてがあるべきように動作していることを確認することができます。



コメントを少々

  1. テストを書くことは時間の浪費のように思えます。ですが、ユニットテスト作成に費やす時間はそれ以上の利益になることを保証します。
  2. もちろん、全関数に対してユニットテストを作成する価値はありません。. 関数の重要性、不具合確率、関数内のコード量の間でバランスをとることです。たとえば、数行の簡単な関数に対してユニットテストを書く必要はありません。
  3. ユニットテストではなんでも好きなことができます。注文のオープン/クローズ、インディケータ使用、グラフィックオブジェクト使用等です。ここではみなさんの行いは制限されることがありません。


そして最後です。

本稿で取り上げた内容がみなさんのお役に立つことを願っています。ご質問には心をこめて回答いたします。また、本稿を改善し新しい記事を書くためのご提案は歓迎します。

不備なしコードの作成を!グッドラック!

MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/1579

添付されたファイル |
mql4unit.mq4 (5.31 KB)
testscript.mq4 (4.34 KB)
グラフィカルインタフェース  II:ライブラリのイベントハンドラの設定(チャプター3) グラフィカルインタフェース II:ライブラリのイベントハンドラの設定(チャプター3)
以前の記事には、メインメニューの構成部分を作成するためのクラスの実装が含まれています。ここで、主要な基本クラスと作成されたコントロールのクラスでイベントハンドラを細かく見ることにします。また、マウスカーソルの位置に応じたチャートの状態の管理にも特別な注意が払われます。
Expert Advisor 内での Expert Advisor コンテスト Expert Advisor 内での Expert Advisor コンテスト
仮想取引によって、適応型 Expert Advisor を作成することができます。それは実マーケットでトレードをオン、オフするものです。1件の Expert Advisor に複数の戦略を組み合わせます。マルチシステム Expert Advisor は自動でトレーディング戦略を選択します。それは実マーケットで仮想取引の収益性をもとにトレードを行うには最良のものです。この種の方法により、ドローダウンを減少し、マーケットでの作業の収益性を上げることができるのです。実験を行い、結果を他の人と共有してください。多くの人がみなさんの戦略ポートフォリオを知りたがっていると思います。
グラフィカルインタフェース  II:メインメニュー要素(チャプターー4) グラフィカルインタフェース II:メインメニュー要素(チャプターー4)
これは、グラフィカルインターフェイスに関するシリーズの第二部の最終章です。ここでは、メインメニューの作成を検討します。このコントロールの開発とユーザの行動に正確な反応するライブラリクラスのハンドラ設定が実証されます。また、メインメニューの項目にコンテキストメニューを取り付ける方法についても説明します。そのに加えて、現在非アクティブな要素のブロックも言及されます。
フィルターの魔法 フィルターの魔法
ほとんどの自動売買システム開発者はなんらかのトレードシグナルフィルター機能を利用しています。本稿では、帯域通過と Expert Advisor 用の個別フィルターの作成と実装を探り、自動売買システムの特性を向上させます。