English Русский Español Deutsch 日本語 Português
通过"单元测试"的帮助来提高代码质量

通过"单元测试"的帮助来提高代码质量

MetaTrader 4示例 | 8 四月 2016, 12:56
1 297 0
Андрей
Андрей

简介

进行 MQL4 编程时,我对于处理部分已完成的 MQL4 程序和编写几十个自己的程序方面有相当的经验。 因此,我得出结论,MQL4 是编写低质量程序极为有利的环境。 原因如下:

  1. MetaTrader 4 没有内置的调试程序。 搜索错误有时会是个非常麻烦的过程。
  2. MQL4 没有类似 C++ 或 Java 这样的异常情况处理工具。
  3. MQL4 程序经常被匆忙编写出来,侧重于想法而非代码质量。

以上种种,导致代码质量低下,而这个事实更引发以下问题:

  1. Advisor 运行错误,操作算法错误(对真实帐户来说尤其致命)。
  2. 执行缓慢。 优化极其缓慢。
  3. 错误案例处理糟糕。 Expert advisor 会变得无法运行。

我想提醒各位注意的是,上述问题不会影响经验丰富的长期 MQL4 程序员。 熟练的程序员会想办法编写高质量代码。

尽管我的主要工作是软件质量测试,我仍对任何与 MQL4 程序测试和调试有关的材料感兴趣。 但是,我能找到关于这个问题的文章数量少之又少。 因此,我打算在此介绍改善程序质量的其中一个方法。 如果这个话题引起大家兴趣,那我会在随后的文章中谈及其他问题。



几个关于质量的理论

如果我们通过 Google 搜索一下,会发现:

质量是产品属性和特性的复合体,赋予产品满足条件需求或假定需求的可能性。

就软件而言,我们可以认定程序拥有良好质量,如果其符合客户需求并满足正确施用在它身上的功能。

编写高质量程序,通常需要两类动作:

  • 质量保证 - 为避免瑕疵形成而采取的措施。
  • 质量控制 - 对成品程序质量的控制聚焦于在 QA 帮不上忙的时候检测缺陷。 需理解的是,如果没有检测到代码缺陷的话,那该缺陷很难被消除。 当然,如果缺陷是被你客户发现的话,那情况更糟了...

质量保证是一个复杂的问题。 它涉及多个任务,从程序员舒适工作环境营造,到复杂商业流程的实施。 我们暂不讨论这个问题。 让我们先聊聊质量控制。

我们越重视质量控制,我们程序运行顺畅的可能性就越高。 理论上说,必须在开发时的每一个阶段都开展质量控制(或者测试)。

  1. 技术规格测试 - 基于错误技术规格来开发一款正常运行程序是不可能的。
  2. 源代码回顾 搜索故障、无效代码、违反编码规则、明显错误。
  3. 对程序在自动模式下的独特函数进行测试(单元测试)。
  4. 在手动模式下对整个成品程序进行测试。 人工(测试员)检查程序是否正确运作。
  5. 自动模式下的程序测试(自动化测试)。 机器人将在此时自行测试程序质量。 这听起来很不实际,但有时却奏效了。
  6. 由客户进行程序测试。

有相当多的测试类型...

“单元测试”是其中最有趣的。



有关“单元测试”的部分理论

Google 给出了以下的“单元测试”定义。 “单元测试” - 是一种应用程序验证方法,程序员通过此方法来检查独特源代码单元(块)在程序其他地方的可用性。 单元是一个程序内可用于测试的最小部分。 在应用程序语言中(包括 MQL4),一个独特函数可以被视为一个单元。

大多数情况下,“单元测试”都自动完成。 换言之,程序创建时使用不同参数调用已检查的函数,该程序随后创建一个报告,表明函数带回的值是否真实。

“单元测试”会非常有用,原因如下:

  1. 如果检测到故障,你可以轻松找到根源,因为仅有一个函数被测试。 如果在整个程序内检测到故障,你需要花费额外时间来找到引发问题的函数。
  2. 检测缺陷是否已排除非常简单。 仅需要再次运行自动“单元测试”即可。 不需要重新启动整个程序。 例如,部分非常罕见情况下出现的错误很难重组。 “单元测试”可免除这种问题。
  3. 你可以轻松地优化函数代码,而不需要担心会出现问题。 “单元测试”通常显示函数是否继续正常工作。
  4. 你还可以检测出不会立即显现的问题,但它们会在客户端出现,并且需要花费大量时间搜索和调试。
  5. 可在“单元测试”首先创建时使用现代化测试驱动方法,随后开发函数。 开发函数,知道“单元测试”通过。 我在一个 C++ 程序内首次使用了这个方法,效果不错。 我真的感到很高兴,因为我在创造这些函数后,对它们的可用性充满信心,而且他们在程序中进一步的使用也是毫无瑕疵。

让我们看看这些函数的模样。 假设,我们创造了平方根函数:
y=sqrt(x)

因此,我们应创造另一个函数以进行测试,该函数应基于以下算法工作:

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

我们可以在创造一个主函数前创造一个测试函数。 因此,我们应确定已创建函数必须符合的要求。 这就是我们对“测试驱动”方法的使用。 仅在我们的“单元测试”显示函数可以无故障工作后,我们才可以放心在主程序内使用该函数。

但还有一个问题有待解决:我们应该如何为测试函数选择测试参数集? 当然,使用所有可能的值是有必要的,但在几乎所有情况下,这都不可能或太耗人力。 可就如何选择测试值撰写一篇新文章。 这里我将试着给出一些通用提示:

  1. 不仅要使用正确数据,而且要使用引发错误的数据,因为我们需要检查函数是否完成既定任务,还要检查函数处理错误的能力。
  2. 我们需要使用边界值。 例如,如果值范围为 0 至 100,则应使用 0 和 100 两个值。 如果输入数据包含行,应使用空白行和最长的行。
  3. 超出允许标记的值也应使用。 如果我们回顾上一个要点的示例,则应使用值 101 和 -1,而值 max+1 应用于行。
  4. 我们应尝试将所有可能的值分解为同等的子集合(相同类),这些子集合的函数行为相似。 每个类应选择一个值。 例如,检查 sqrt(4) 和 sqrt(9) 两个函数是没有意义的。 检查 sqrt(4) 和 sqrt(5) 则会更有趣,因为在这种情况中,函数将返回无理值,而在第一种情况下,则为整数值。
  5. 如果函数有分支(如果转换),则我们应确保每个分支都接受“单元测试”的处理。

我会试着通过明确示例在下一章节说明这个情况。



创建“单元测试”相关实践

让我们设定一个训练目标! 假设我们的任务是开发拥有在入口接受两个数组的函数的库。 函数从第一个数组中删除第二个数组中没有的元素。 因此,第一个数组是第二个数组的子集。

让我们决定函数的原型:

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 个情况下运作顺利,则我们有绝对把握,使用此函数的 experts 将不会再受函数缺陷之苦。 但我们应明白,要 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. A small part of A1 elements is present in 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. A small part of A1 elements is present in 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. A small part of A1 elements is present in 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());
}

为执行“单元测试”,我们必须在主函数中调用测试函数,并运行脚本。

让我们执行测试。

如我们所见,结果让人失望。 这并不意外,因为函数完全没有准备好。 不过! 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++;
      }
   }
}
Let's run the test again:

函数可在任何位置运行。 函数可在 expert 内部决定,并在初始化期间运行。 处理单独模块时,一个或多个测试函数可在其中决定,并从脚本中调用。 我们可在此展开一些想象。

当然,理想的形式应具备在库编译后运行单元测试的可能性,但我不敢肯定是否可以在 MQL4 内实现。 很有可能无法实现。 如果你知道如何实现,请给我留言。

我们每次运行测试后,都会长舒一口气,并确保所有事情都运作正常。



一些注释

  1. 编写测试看起来只会浪费时间。 当我可以向你保证,用于单元测试开发的时间会给你带来超值回报。
  2. 当然,为所有函数开发“单元测试”并不值得。 应在函数重要性、失败概率和函数代码量之间保持平衡。 例如,不需要为只有几行的最简单函数编写测试。
  3. 你可以在单元测试内做任何事:打开/关闭订单,使用指标,图形对象等。你在此的操作不受限制。


最后一言。

我希望这份材料对你有所帮助。 我很乐意回答你们所有问题。 此外,我对任何关于改善本文的可能方法和撰写新文章的建议持开放态度。

我祝你们好运,并拥有完美代码!

本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/1579

附加的文件 |
mql4unit.mq4 (5.31 KB)
testscript.mq4 (4.34 KB)
在一个 Expert Advisor 内的多个 Expert Advisor 的竞争 在一个 Expert Advisor 内的多个 Expert Advisor 的竞争
使用虚拟交易,你可以创建一个自适应的 Expert Advisor,在真实市场上打开和关闭交易。 将多个策略组合到一个 Expert Advisor 内! 你的多系统 Expert Advisor 会根据虚拟交易的获利能力,自动选择进行真实市场交易的最佳策略。 这种方法可以降低亏损并增加你在市场上操作的获利能力。 进行实验并跟其他人分享你的结果吧! 我想,很多人会对你的策略组合感兴趣。
图形界面 II: 设置库的事件处理函数 (第三章) 图形界面 II: 设置库的事件处理函数 (第三章)
之前的文章中包含了用于创建主菜单构成部分类的实现. 现在, 是时候在主基础类和创建控件的类中关注事件处理函数了. 我们将特别关注根据鼠标光标的位置来管理图表的状态.
图形界面 II: 主菜单元件 (第四章)) 图形界面 II: 主菜单元件 (第四章))
这是图形界面系列第二部分的最后一章。在此,我们将探讨主菜单的创建,演示这个控件的开发以及设置库中类的处理函数以正确回应用户的操作。我们还将讨论如何把上下文菜单附加到主菜单项目中。另外,我们还会提到怎样阻止当前没有激活的元件。
通用智能交易系统:交易策略的模式(第一章) 通用智能交易系统:交易策略的模式(第一章)
任何一个智能交易系统(EA)的开发人员,无论编程技能如何,每天都面临着同样的交易目标和算法问题的困扰,即应该如何建立一个可靠的交易系统。本文介绍CStrategy交易引擎,它可以给出这些任务的解决方案,并且向用户提供一种用于描述自定义交易思想的简便机制。