TDD_in_Matlab中进行测试驱动开发(TDD)
什么是TDD
TDD(Test-Driven Development,测试驱动开发)是一种软件开发过程,它强调在编写代码之前先编写测试用例。TDD的流程通常包括以下几个步骤:
- 编写测试用例:首先,开发人员编写针对代码的测试用例。这些测试用例通常使用单元测试框架(如JUnit、NUnit等)来编写。
- 运行测试:然后,运行测试用例,确保它们失败。
- 编写代码:接下来,开发人员编写代码来使测试用例通过。
- 重构:最后,开发人员重构代码,确保它符合设计原则,并且易于维护。
flowchart O(开发需求) --> B[测试用例] B --> A{运行测试} A --->|通过| D(提交代码) A -->|未通过| C[重构代码] C --> A
Matlab对TDD的支持
Matlab有一套完整的单元测试框架,可以方便地进行TDD。一般而言,最基础的单元测试框架需要包含以下两个部分:
- 测试用例编写
- 断言工具
- 测试数据管理
- 测试用例的组织
- 测试用例运行
- 自动化发现测试用例
- 测试用例的执行
- 测试结果的收集
- 测试报告的生成
测试用例的编写
在Matlab中,定义测试用例可以有三个方式:基于脚本、基于函数、基于类。
- 基于脚本:每个测试单元写作脚本的一节,用
%%
分隔。 - 基于函数:每个测试单元写作一个文件的局部函数。
- 基于类:每个测试单元写作一个类的测试方法。
当然,脚本测试只能使用框架的基础断言工具,函数和类测试可以采用更高级的测试工具。我们首先从最简单的脚本测试开始。
测试用例的运行
对于脚本测试,一般使用runtests
函数直接运行测试脚本文件。
如果显式地定义测试组件(testsuite
或者matlab.unittest.TestSuite
类),可以访问其他更加复杂的功能。当然,部分高级的功能还需要使用基于函数和类的测试。
如果还需要做一些比较复杂的事情,还可以使用TestRunner
类来实现。
实用基于函数和类的测试,可以在运行时进行测试的组织和过滤,并可以定义测试的运行顺序、结果收集、测试报告等。
脚本测试工具
下面我们很随机的找一个需求来完成开发。我们假设,需要实现一个表达式的类,用于进行Genetic Programming。
我们的需求是什么样的呢?
表达式:
- 值,这个值在表达式中保持不变,C1,C2,C3,…
- 变量,这个变量在表达式中可以变化,X1,X2,X3,…
- 函数,函数需要输入N个参数,每个参数同样是表达式
这个递归的定义,可以很自然地用一个类来表示。
编写测试用例
1% 测试表达式类
2
3%% Test 1: Constant
4c1 = Expression("Constant", "c1", 1);
5assert(isa(c1, 'handle'));
6assert(c1.isConstant());
7assert(~c1.isVariable());
8assert(~c1.isFunction());
9assert(c1.value == 1);
10assert(strcmp(c1.name, "c1"));
11assert(isempty(c1.operands));
12
13
14%% Test 2: Variable
15x1 = Expression("Variable", "x1");
16assert(isa(x1, 'handle'));
17assert(~x1.isConstant());
18assert(x1.isVariable());
19assert(~x1.isFunction());
20assert(strcmp(x1.name, "x1"));
21assert(isempty(x1.value));
22assert(isempty(x1.operands));
23
24
25
26%% test 3: Function
27f1 = Expression("Function", "plus", 2);
28assert(isa(f1, 'handle'));
29assert(f1.isFunction());
30assert(~f1.isConstant());
31assert(~f1.isVariable());
32assert(strcmp(f1.name, "plus"));
33assert(f1.noperands == 2);
34
35%% Test 4: Function with operands
36c1 = Expression("Constant", "c1", 1);
37x1 = Expression("Variable", "x1");
38f2 = Expression("Function", "plus", 2, x1, c1);
39assert(f2.noperands == 2);
40assert(f2.operands{1} == x1);
41assert(f2.operands{2} == c1);
42
43
44
45%% Test 4: find all variables
46c1 = Expression("Constant", "c1", 1);
47x1 = Expression("Variable", "x1");
48f1 = Expression("Function", "plus", 2, x1, c1);
49vars = f1.findvars();
50assert(isequal(vars, x1));
51
52%% Test 5: find all constants
53c1 = Expression("Constant", "c1", 1);
54x1 = Expression("Variable", "x1");
55f1 = Expression("Function", "plus", 2, x1, c1);
56consts = f1.findconstants();
57assert(isequal(consts, c1));
58
59%% Test 6: find all functions
60c1 = Expression("Constant", "c1", 1);
61x1 = Expression("Variable", "x1");
62f1 = Expression("Function", "plus", 2, x1, c1);
63funcs = f1.findfunctions();
64assert(isequal(funcs, f1));
我们先不要搞太复杂,只实现常量、变量和函数。
运行测试
我们先来运行一下测试用例,看看是否通过。
1table(runtests("testExpr"))
结果如下:
Name Passed Failed Incomplete Duration Details
___________________________________ ______ ______ __________ _________ ____________
{'testExpr/Test1_Constant' } false true true 0.012455 {1x1 struct}
{'testExpr/Test2_Variable' } false true true 0.0035648 {1x1 struct}
{'testExpr/Test3_Function' } false true true 0.0034582 {1x1 struct}
{'testExpr/Test4_FindAllVariables'} false true true 0.0073568 {1x1 struct}
{'testExpr/Test5_FindAllConstants'} false true true 0.0041104 {1x1 struct}
{'testExpr/Test6_FindAllFunctions'} false true true 0.0039236 {1x1 struct}
实现表达式类
1classdef Expression < handle
2 properties
3 type
4 name
5 value
6 operands
7 noperands
8 end
9
10 methods
11 function obj = Expression(type, name, varargin)
12 obj.type = type;
13 obj.name = name;
14 switch type
15 case "Constant"
16 obj.value = varargin{1};
17 obj.noperands = 0;
18 obj.operands = [];
19 case "Variable"
20 obj.value = [];
21 obj.noperands = 0;
22 obj.operands = [];
23 case "Function"
24 obj.value = str2func(name);
25 obj.noperands = varargin{1};
26 obj.operands = varargin{2:end};
27 end
28 end
29 end
30
我们再运行一下测试用例,看看是否通过。
1table(runtests("testExpr"))
依然是没有通过,我们再检查一下代码。我们现针对第一个测试用例,实现一些代码。
1 methods % constant
2 function isConstant = isConstant(obj)
3 isConstant = strcmp(obj.type, "Constant");
4 end
5
6 function isVariable = isVariable(obj)
7 isVariable = strcmp(obj.type, "Variable");
8 end
9
10 function isFunction = isFunction(obj)
11 isFunction = strcmp(obj.type, "Function");
12 end
13
14 end
15end
我们再运行一下测试用例,看看是否通过。
1runtests("testExpr/Test1_Constant")
这些可以通过了。
Running testExpr
.
Done testExpr
__________
ans =
TestResult with properties:
Name: 'testExpr/Test1_Constant'
Passed: 1
Failed: 0
Incomplete: 0
接下来,我们看看针对变量的测试:
1runtests("testExpr/Test2_Variable")
这些也可以通过了。
Running testExpr
.
Done testExpr
__________
ans =
TestResult with properties:
Name: 'testExpr/Test2_Variable'
Passed: 1
Failed: 0
Incomplete: 0
Duration: 0.0118
Details: [1x1 struct]
Totals:
1 Passed, 0 Failed, 0 Incomplete.
0.011789 seconds testing time.
接下来,就是针对函数的部分。
1runtests("testExpr/Test3_Function")
失败的部分,很清楚:
================================================================================
Error occurred in testExpr/Test3_Function and it did not run to completion.
---------
Error ID:
---------
'MATLAB:TooManyOutputsDueToMissingBraces'
--------------
Error Details:
--------------
Unable to perform assignment with 0 elements on the right-hand side.
Error in Expression (line 26)
obj.operands = varargin{2:end};
Error in testExpr (line 27)
f1 = Expression("Function", "plus", 2);
================================================================================
改来改去,终于通过了前几个测试。此时类定义如下:
1classdef Expression < handle
2 properties
3 type
4 name
5 value
6 operands
7 noperands
8 end
9
10 methods
11 function obj = Expression(type, name, varargin)
12 obj.type = type;
13 obj.name = name;
14 switch type
15 case "Constant"
16 obj.value = varargin{1};
17 obj.noperands = 0;
18 obj.operands = {};
19 case "Variable"
20 obj.value = [];
21 obj.noperands = 0;
22 obj.operands = {};
23 case "Function"
24 obj.value = str2func(name);
25 n = numel(varargin);
26 switch n
27 case 0
28 % like e, pi, etc.
29 obj.noperands = 0;
30 obj.operands = {};
31 case 1
32 obj.noperands = varargin{1};
33 obj.operands = {};
34 otherwise
35 obj.noperands = varargin{1};
36 [obj.operands{1:n-1}] = varargin{2:end};
37 end
38 end
39 end
40 end
41
42 methods % constant
43 function isConstant = isConstant(obj)
44 isConstant = strcmp(obj.type, "Constant");
45 end
46
47 function isVariable = isVariable(obj)
48 isVariable = strcmp(obj.type, "Variable");
49 end
50
51 function isFunction = isFunction(obj)
52 isFunction = strcmp(obj.type, "Function");
53 end
54
55 end
测试结果:
Name Passed Failed Incomplete Duration Details
_______________________________________ ______ ______ __________ _________ ____________
{'testExpr/Test1_Constant' } true false false 0.0065065 {1x1 struct}
{'testExpr/Test2_Variable' } true false false 0.0041009 {1x1 struct}
{'testExpr/Test3_Function' } true false false 0.0035713 {1x1 struct}
{'testExpr/Test4_FunctionWithOperands'} true false false 0.002952 {1x1 struct}
{'testExpr/Test4_FindAllVariables' } false true true 0.0033039 {1x1 struct}
{'testExpr/Test5_FindAllConstants' } false true true 0.0035309 {1x1 struct}
{'testExpr/Test6_FindAllFunctions' } false true true 0.0033208 {1x1 struct}
后面的几个测试随手写上,就可以通过了。
1classdef Expression < handle
2 properties
3 type
4 name
5 value
6 operands
7 noperands
8 end
9
10 methods
11 function obj = Expression(type, name, varargin)
12 obj.type = type;
13 obj.name = name;
14 switch type
15 case "Constant"
16 obj.value = varargin{1};
17 obj.noperands = 0;
18 obj.operands = {};
19 case "Variable"
20 obj.value = [];
21 obj.noperands = 0;
22 obj.operands = {};
23 case "Function"
24 obj.value = str2func(name);
25 n = numel(varargin);
26 switch n
27 case 0
28 % like e, pi, etc.
29 obj.noperands = 0;
30 obj.operands = {};
31 case 1
32 obj.noperands = varargin{1};
33 obj.operands = {};
34 otherwise
35 obj.noperands = varargin{1};
36 [obj.operands{1:n-1}] = varargin{2:end};
37 end
38 end
39 end
40 end
41
42 methods % constant
43 function isConstant = isConstant(obj)
44 isConstant = strcmp(obj.type, "Constant");
45 end
46
47 function isVariable = isVariable(obj)
48 isVariable = strcmp(obj.type, "Variable");
49 end
50
51 function isFunction = isFunction(obj)
52 isFunction = strcmp(obj.type, "Function");
53 end
54
55 end
56
57 methods % find elements
58 function vars = findvars(obj)
59 vars = [];
60 if obj.isVariable()
61 vars = obj;
62 elseif obj.isFunction() && ~isempty(obj.operands)
63 for i = 1:numel(obj.operands)
64 op_vars = obj.operands{i}.findvars();
65 if ~isempty(op_vars)
66 if isempty(vars)
67 vars = op_vars;
68 else
69 vars = [vars, op_vars];
70 end
71 end
72 end
73 end
74 end
75
76 function consts = findconstants(obj)
77 consts = [];
78 if obj.isConstant()
79 consts = obj;
80 elseif obj.isFunction() && ~isempty(obj.operands)
81 for i = 1:numel(obj.operands)
82 op_consts = obj.operands{i}.findconstants();
83 if ~isempty(op_consts)
84 if isempty(consts)
85 consts = op_consts;
86 else
87 consts = [consts, op_consts];
88 end
89 end
90 end
91 end
92 end
93
94 function funcs = findfunctions(obj)
95 funcs = [];
96 if obj.isFunction()
97 funcs = obj;
98 if ~isempty(obj.operands)
99 for i = 1:numel(obj.operands)
100 op_funcs = obj.operands{i}.findfunctions();
101 if ~isempty(op_funcs)
102 funcs = [funcs, op_funcs];
103 end
104 end
105 end
106 end
107 end
108 end
109end
测试table(runtests("testExpr"))
,结果如下:
Name Passed Failed Incomplete Duration Details
_______________________________________ ______ ______ __________ _________ ____________
{'testExpr/Test1_Constant' } true false false 0.0056642 {1x1 struct}
{'testExpr/Test2_Variable' } true false false 0.002396 {1x1 struct}
{'testExpr/test3_Function' } true false false 0.0025321 {1x1 struct}
{'testExpr/Test4_FunctionWithOperands'} true false false 0.0025485 {1x1 struct}
{'testExpr/Test4_FindAllVariables' } true false false 0.0043304 {1x1 struct}
{'testExpr/Test5_FindAllConstants' } true false false 0.0036644 {1x1 struct}
{'testExpr/Test6_FindAllFunctions' } true false false 0.0034711 {1x1 struct}
总结
利用脚本来完成测试,每一个%%
分隔的测试单元,就是一个测试用例。测试用例的名字大概就是从%% test 1: constant
变成,testExpr/Test1_Constant
。名称里面的数字和:
都可以去掉。可以通过runtests
函数来运行测试所有测试(文件名作为参数)或者单个测试(测试用例名作为参数)。
文章标签
|-->matlab |-->TDD |-->测试驱动开发 |-->runtests |-->unittest
- 本站总访问量:次
- 本站总访客数:人
- 可通过邮件联系作者:Email大福
- 也可以访问技术博客:大福是小强
- 也可以在知乎搞抽象:知乎-大福
- Comments, requests, and/or opinions go to: Github Repository