<-- Home |--matlab

TDD_in_Matlab中进行测试驱动开发(TDD)

什么是TDD

TDD(Test-Driven Development,测试驱动开发)是一种软件开发过程,它强调在编写代码之前先编写测试用例。TDD的流程通常包括以下几个步骤:

  1. 编写测试用例:首先,开发人员编写针对代码的测试用例。这些测试用例通常使用单元测试框架(如JUnit、NUnit等)来编写。
  2. 运行测试:然后,运行测试用例,确保它们失败。
  3. 编写代码:接下来,开发人员编写代码来使测试用例通过。
  4. 重构:最后,开发人员重构代码,确保它符合设计原则,并且易于维护。
flowchart
    O(开发需求) --> B[测试用例]
    B --> A{运行测试}
    A --->|通过| D(提交代码)
    A -->|未通过| C[重构代码]
    C --> A

Matlab对TDD的支持

Matlab有一套完整的单元测试框架,可以方便地进行TDD。一般而言,最基础的单元测试框架需要包含以下两个部分:

  • 测试用例编写
    • 断言工具
    • 测试数据管理
    • 测试用例的组织
  • 测试用例运行
    • 自动化发现测试用例
    • 测试用例的执行
    • 测试结果的收集
    • 测试报告的生成

测试用例的编写

在Matlab中,定义测试用例可以有三个方式:基于脚本、基于函数、基于类。

matlab_tdd

  • 基于脚本:每个测试单元写作脚本的一节,用%%分隔。
  • 基于函数:每个测试单元写作一个文件的局部函数。
  • 基于类:每个测试单元写作一个类的测试方法。

当然,脚本测试只能使用框架的基础断言工具,函数和类测试可以采用更高级的测试工具。我们首先从最简单的脚本测试开始。

测试用例的运行

对于脚本测试,一般使用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


GitHub