
通过"单元测试"的帮助来提高代码质量
简介
进行 MQL4 编程时,我对于处理部分已完成的 MQL4 程序和编写几十个自己的程序方面有相当的经验。 因此,我得出结论,MQL4 是编写低质量程序极为有利的环境。 原因如下:
- MetaTrader 4 没有内置的调试程序。 搜索错误有时会是个非常麻烦的过程。
- MQL4 没有类似 C++ 或 Java 这样的异常情况处理工具。
- MQL4 程序经常被匆忙编写出来,侧重于想法而非代码质量。
以上种种,导致代码质量低下,而这个事实更引发以下问题:
- Advisor 运行错误,操作算法错误(对真实帐户来说尤其致命)。
- 执行缓慢。 优化极其缓慢。
- 错误案例处理糟糕。 Expert advisor 会变得无法运行。
我想提醒各位注意的是,上述问题不会影响经验丰富的长期 MQL4 程序员。 熟练的程序员会想办法编写高质量代码。
尽管我的主要工作是软件质量测试,我仍对任何与 MQL4 程序测试和调试有关的材料感兴趣。 但是,我能找到关于这个问题的文章数量少之又少。 因此,我打算在此介绍改善程序质量的其中一个方法。 如果这个话题引起大家兴趣,那我会在随后的文章中谈及其他问题。
几个关于质量的理论
如果我们通过 Google 搜索一下,会发现:
质量是产品属性和特性的复合体,赋予产品满足条件需求或假定需求的可能性。
就软件而言,我们可以认定程序拥有良好质量,如果其符合客户需求并满足正确施用在它身上的功能。
编写高质量程序,通常需要两类动作:
- 质量保证 - 为避免瑕疵形成而采取的措施。
- 质量控制 - 对成品程序质量的控制聚焦于在 QA 帮不上忙的时候检测缺陷。 需理解的是,如果没有检测到代码缺陷的话,那该缺陷很难被消除。 当然,如果缺陷是被你客户发现的话,那情况更糟了...
质量保证是一个复杂的问题。 它涉及多个任务,从程序员舒适工作环境营造,到复杂商业流程的实施。 我们暂不讨论这个问题。 让我们先聊聊质量控制。
我们越重视质量控制,我们程序运行顺畅的可能性就越高。 理论上说,必须在开发时的每一个阶段都开展质量控制(或者测试)。
- 技术规格测试 - 基于错误技术规格来开发一款正常运行程序是不可能的。
- 源代码回顾 搜索故障、无效代码、违反编码规则、明显错误。
- 对程序在自动模式下的独特函数进行测试(单元测试)。
- 在手动模式下对整个成品程序进行测试。 人工(测试员)检查程序是否正确运作。
- 自动模式下的程序测试(自动化测试)。 机器人将在此时自行测试程序质量。 这听起来很不实际,但有时却奏效了。
- 由客户进行程序测试。
有相当多的测试类型...
“单元测试”是其中最有趣的。
有关“单元测试”的部分理论
Google 给出了以下的“单元测试”定义。 “单元测试” - 是一种应用程序验证方法,程序员通过此方法来检查独特源代码单元(块)在程序其他地方的可用性。 单元是一个程序内可用于测试的最小部分。 在应用程序语言中(包括 MQL4),一个独特函数可以被视为一个单元。
大多数情况下,“单元测试”都自动完成。 换言之,程序创建时使用不同参数调用已检查的函数,该程序随后创建一个报告,表明函数带回的值是否真实。
“单元测试”会非常有用,原因如下:
- 如果检测到故障,你可以轻松找到根源,因为仅有一个函数被测试。 如果在整个程序内检测到故障,你需要花费额外时间来找到引发问题的函数。
- 检测缺陷是否已排除非常简单。 仅需要再次运行自动“单元测试”即可。 不需要重新启动整个程序。 例如,部分非常罕见情况下出现的错误很难重组。 “单元测试”可免除这种问题。
- 你可以轻松地优化函数代码,而不需要担心会出现问题。 “单元测试”通常显示函数是否继续正常工作。
- 你还可以检测出不会立即显现的问题,但它们会在客户端出现,并且需要花费大量时间搜索和调试。
- 可在“单元测试”首先创建时使用现代化测试驱动方法,随后开发函数。 开发函数,知道“单元测试”通过。 我在一个 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....
我们可以在创造一个主函数前创造一个测试函数。 因此,我们应确定已创建函数必须符合的要求。 这就是我们对“测试驱动”方法的使用。 仅在我们的“单元测试”显示函数可以无故障工作后,我们才可以放心在主程序内使用该函数。
但还有一个问题有待解决:我们应该如何为测试函数选择测试参数集? 当然,使用所有可能的值是有必要的,但在几乎所有情况下,这都不可能或太耗人力。 可就如何选择测试值撰写一篇新文章。 这里我将试着给出一些通用提示:
- 不仅要使用正确数据,而且要使用引发错误的数据,因为我们需要检查函数是否完成既定任务,还要检查函数处理错误的能力。
- 我们需要使用边界值。 例如,如果值范围为 0 至 100,则应使用 0 和 100 两个值。 如果输入数据包含行,应使用空白行和最长的行。
- 超出允许标记的值也应使用。 如果我们回顾上一个要点的示例,则应使用值 101 和 -1,而值 max+1 应用于行。
- 我们应尝试将所有可能的值分解为同等的子集合(相同类),这些子集合的函数行为相似。 每个类应选择一个值。 例如,检查 sqrt(4) 和 sqrt(9) 两个函数是没有意义的。 检查 sqrt(4) 和 sqrt(5) 则会更有趣,因为在这种情况中,函数将返回无理值,而在第一种情况下,则为整数值。
- 如果函数有分支(如果转换),则我们应确保每个分支都接受“单元测试”的处理。
我会试着通过明确示例在下一章节说明这个情况。
创建“单元测试”相关实践
让我们设定一个训练目标! 假设我们的任务是开发拥有在入口接受两个数组的函数的库。 函数从第一个数组中删除第二个数组中没有的元素。 因此,第一个数组是第二个数组的子集。
让我们决定函数的原型:
void CreateSubset(int & a1[], int a2[]);
我们将尝试使用“测试驱动”方法来开发函数。 让我们决定一套测试数据。 我们应标记一些输入数据等值类来达到以下目标:
- 让两个数组都为空白。
- A1 空白,A2 含有元素。
- A1 包含元素,A2 空白。
- 两个数组包含相似元素集合,大小相似。
- A1 包含 A2 内没有的元素。
- A1 内部分元素出现在 A2 内,A2 内部分元素包含在 A1 内(两者相互交叉)。
- A1 所有元素都出现在 A2 内,但 A2 更大。
- A1 元素的一小部分显示在 A2 中。 此外,元素分散在数组各处。
- A1 元素的一小部分显示在 A2 中。 此外,元素集中在数组开头处。
- 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 内实现。 很有可能无法实现。 如果你知道如何实现,请给我留言。
我们每次运行测试后,都会长舒一口气,并确保所有事情都运作正常。
一些注释
- 编写测试看起来只会浪费时间。 当我可以向你保证,用于单元测试开发的时间会给你带来超值回报。
- 当然,为所有函数开发“单元测试”并不值得。 应在函数重要性、失败概率和函数代码量之间保持平衡。 例如,不需要为只有几行的最简单函数编写测试。
- 你可以在单元测试内做任何事:打开/关闭订单,使用指标,图形对象等。你在此的操作不受限制。
最后一言。
我希望这份材料对你有所帮助。 我很乐意回答你们所有问题。 此外,我对任何关于改善本文的可能方法和撰写新文章的建议持开放态度。
我祝你们好运,并拥有完美代码!
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/1579



