MATLAB 工程化编程笔记
MATLAB 被吐槽的一个重要原因就是大量 MATLAB 代码的质量一言难尽:完全脚本式编程、命名随意、结构松散、缺乏代码格式化、缺乏注释和单元测试。这类代码虽然能完成一次性的计算,却难以复现、扩展或维护,注定变成屎山代码。
本文关注如何让 MATLAB 代码变得工程化,使得 MATLAB 项目开发更有条理、更容易维护,具体包括输入参数检查、代码注释,单元测试等主题。
其实还应该加上日志系统,但是 MATLAB 并没有官方提供的,或者第三方广泛使用的日志系统。
函数参数检查
为了得到健壮的代码,非常有必要对函数的参数进行检查,除了最基础的参数个数,还需要关注参数的类型和数据范围等,下面介绍几个MATLAB提供的用于参数检查的内置函数。
assert + isXXX
最简单的做法就是基于 assert 和 isXXX
检查函数的形参是否满足要求,例如
1 | function func(u, t, f, n, b, flag, params) |
注:这里对结构体 params 的检查是要求至少含有指定字段
a, b, c,但是也允许含有更多字段,
如果需要严格检查,则必须通过 fieldnames 函数动态获取
params 的所有字段进行比较,无法在一个 assert
语句中完成。
下面是一些常用的 isXXX 函数。
关于维度和形状
| 函数 | 说明 | 正面示例 | 反面示例 |
|---|---|---|---|
isscalar(A) |
是否为标量 (1×1) |
isscalar(5) |
isscalar([1 2]) |
isvector(A) |
是否为(行或列)向量 | isvector([1 2 3]) |
isvector([1 2; 3 4]) |
isrow(A) |
是否为行向量 | isrow([1 2 3]) |
isrow([1; 2; 3]) |
iscolumn(A) |
是否为列向量 | iscolumn([1; 2; 3]) |
iscolumn([1 2 3]) |
ismatrix(A) |
是否为矩阵 | ismatrix(rand(3,4)) |
ismatrix(rand(3,4,2)) |
issparse(A) |
是否为稀疏矩阵 | issparse(speye(3)) |
issparse(eye(3)) |
isempty(A) |
是否为空 | isempty([]) |
isempty([0]) |
注:在 MATLAB 中,空数组是指至少一个维度长度等于零的数组。
关于类型
| 函数 | 说明 | 正面示例 (true) |
反面示例 (false) |
|---|---|---|---|
isnumeric(A) |
是否为数值类型 | isnumeric(3.14) |
isnumeric('3.14') |
islogical(A) |
是否为逻辑类型 | islogical(true) |
islogical(0) |
ischar(A) |
是否为字符数组 | ischar('abc') |
ischar(["abc"]) |
isstring(A) |
是否为字符串类型 | isstring("abc") |
isstring('abc') |
iscell(A) |
是否为元胞数组 | iscell({1,2}) |
iscell([1,2]) |
isstruct(A) |
是否为结构体 | isstruct(struct('a',1)) |
isstruct([1 2]) |
isobject(A) |
是否为对象 | isobject(MException) |
isobject(struct()) |
isa(A, 'class') |
是否为指定类(或者派生类)对象 | isa(3, 'double') |
isa('abc', 'double') |
注:在MATLAB中的字符数组和字符串是不一样的,很多情况下的使用都存在细微区别。
关于数值属性
| 函数 | 说明 | 正面示例 (true) |
反面示例 (false) |
|---|---|---|---|
isfinite(A) |
是否为有限数 | isfinite(3.14) |
isfinite(Inf) |
isinf(A) |
是否为无穷大 | isinf(Inf) |
isinf(3.14) |
isnan(A) |
是否为 NaN | isnan(NaN) |
isnan(3.14) |
isinteger(A) |
是否为整数类 (int32 等) | isinteger(int8(3)) |
isinteger(3) |
isfloat(A) |
是否为浮点数 | isfloat(3.14) |
isfloat(int8(3)) |
isreal(A) |
是否为实数 | isreal(3) |
isreal(1+2i) |
其它
| 函数 | 说明 | 正面示例 (true) |
反面示例 (false) |
|---|---|---|---|
isfile(name) |
是否为文件 | isfile('myfile.txt') |
isfile('myfolder') |
isfolder(name) |
是否为文件夹 | isfolder('myfolder') |
isfolder('myfile.txt') |
isfield(S, 'name') |
结构体是否包含字段 | isfield(struct('a',1), 'a') |
isfield(struct('a',1), 'b') |
iscellstr(C) |
是否为字符数组的元胞数组 | iscellstr({'a','b'}) |
iscellstr({'a', 1}) |
isStringScalar(A) |
是否为包含一个元素的字符串数组 | isStringScalar("A") |
isStringScalar(["A","B"]) |
官方文档提供了一份完整的
isXXX函数参考表:Use is* Functions to Detect State
千万不要误用isinteger函数,因为默认的绝大多数数据都是浮点数,只有专门使用int32、int64等整数类数据类型创建的数据,才会返回true。
1
2
3
4
5
6
7
8>> isinteger(1)
ans =
logical
0
>> isinteger(int32(1))
ans =
logical
1
validateattributes
validateattributes函数可以为我们检查某个具体参数的类型以及是否满足某些要求,不满足要求会抛出错误。
假设我们有一个函数func1,它接受两个输入参数a和b,我们要求:a是一个非空的标量,b是一个非空的向量。
1
2
3
4
5
6
7function result = func1(a, b)
validateattributes(a, {'numeric'}, {'nonempty', 'scalar'});
validateattributes(b, {'numeric'}, {'nonempty', 'vector'});
result = a + sum(b);
disp(['Result: ', num2str(result)]);
end
validateattributes函数可以确保a是一个非空的标量,b是一个非空的向量。如果输入参数不符合这些要求,MATLAB
会抛出错误。
validateattributes函数的基本格式为 1
validateattributes(a, classes, attributes);
其中:
- 第一个参数是要检查的函数参数
- 第二个参数是要求的类型,通常包括:
double,logical,char,struct - 第三个参数是要求参数满足的条件,可以是多个条件,例如
{'nonempty', 'vector'},也可以留空{}
关于参数的类型,MATLAB提供了class函数来获取,常见的类型包括:
double浮点数numeric数值类型(包括各种浮点数和各种整数)char字符logical布尔值struct结构体数组cell元胞数组function_handle函数句柄class_name自定义类型名称
关于参数满足的条件,常见的条件包括
- 维度检查
2d二维数组;3d三维数组row行向量;column列向量;vector行向量或列向量scalar标量'size',[d1,...dN]指定维数信息的数组'numel',N指定元素总个数为N的数组'nrows',N指定行数为N的数组;'ncols',N指定列数为N的数组square方阵(每一个维度都相等)nonempty要求数组的每一个维度都不为0nonsparse要求数组非稀疏
- 大小范围检查
'>',N所有值大于N'>=',N所有值大于等于N- ...
- 其它检查
finite数组中的元素不含有Infnonnan数组中的元素不含有Nannonnegative数组中的元素全部非负nonzero数组中的元素全部非零decreasing单调减increasing单调增
validatestring
对于字符串参数,MATLAB专门提供了检查工具validatestring,它可以限制字符串参数的所有合法取值,例如
1
2
3
4
5
6
7
8
9
10
11
12function result = selectOption(option)
option = validatestring(option, {'Option1', 'Option2', 'Option3'});
switch option
case 'Option1'
result = 'You selected Option 1';
case 'Option2'
result = 'You selected Option 2';
case 'Option3'
result = 'You selected Option 3';
end
end
但是这种限制不是严格的,正如很多MATLAB内置函数一样,可以忽略大小写进行模糊匹配,也可以只匹配到合法选项的开头部分。 如果这个参数只能匹配到唯一的合法选项,就不会报错,反之则会报错。
成功的模糊匹配例如 1
2
3
4
5
6>> validatestring('G',{'green','red'})
ans =
'green'
>> validatestring('bla',{'black','blue'})
ans =
'black'
下面的匹配则会报错 1
2
3
4
5
6
7
8>> validatestring('u',{'black','blue'})
Expected input to match one of these values:
'black', 'blue'
The input, 'u', did not match any of the valid values.
>> validatestring('ue',{'black','blue'})
Expected input to match one of these values:
'black', 'blue'
The input, 'ue', did not match any of the valid values.
inputParser
inputParser是输入参数解析器类型,和C++中常见的命令行参数解析器非常类似,只不过在这里是对函数参数进行检查。
假设我们有一个函数func2,它接受两个必需的输入参数a和b,一个可选的输入参数c,以及一个键值对参数Verbose。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27function result = func2(a, b, varargin)
% 创建一个 inputParser 对象
p = inputParser;
% 配置必需参数
p.addRequired('a', @isnumeric);
p.addRequired('b', @isnumeric);
% 配置可选参数(默认值1)
p.addOptional('c', 1, @isnumeric);
% 配置键值对参数(键值对的值默认取false)
p.addParameter('Verbose', false, @islogical);
% 解析输入参数
p.parse(a, b, varargin{:});
% 重新获取解析后的参数
a = p.Results.a;
b = p.Results.b;
c = p.Results.c;
verbose = p.Results.Verbose;
result = a + sum(b) + c;
if verbose
disp(['a: ', num2str(a)]);
disp(['b: ', num2str(b)]);
disp(['c: ', num2str(c)]);
disp(['Result: ', num2str(result)]);
end
end
在这个示例中,我们首先创建了一个inputParser对象
p,然后用它对参数进行检查:
- 使用
addRequired方法添加了必需参数a和b,并指定它们必须是数值类型。(必需参数对顺序是敏感的,在调用时必须按照声明的顺序提供。 - 使用
addOptional方法添加了可选参数c,并指定默认值为 1。 - 使用
addParameter方法添加了键值对参数Verbose,指定键的名称为Verbose,值必须是布尔类型,默认为false。 - 使用
parse方法解析输入参数。 - 从
p.Results重新获取解析后的参数。
使用例如 1
2
3
4
5
6
7
8
9
10
11% 调用 func2 并传递必需参数 a 和 b
func2(1, [2, 3]);
% 调用 func2 并传递必需参数 a 和 b 以及可选参数 c
func2(1, [2, 3], 4);
% 调用 func2 并传递必需参数 a 和 b,以及名称-值对参数 'Verbose'
func2(1, [2, 3], 'Verbose', true);
% 调用 func2 并传递必需参数 a 和 b, 可选参数 c 和名称-值对参数 'Verbose'
func2(1, [2, 3], 4, 'Verbose', true);
在较新的版本中,还支持对可选参数以键值对的形式传递,例如下面两个语句是等价的
1
2func2(1, [2, 3], 4, Verbose=true);
func2(1, [2, 3], 4, 'Verbose', true);
我们可以使用匿名函数的方式,将validateattributes函数也组合到inputParser中使用,用于判断参数的有效性,例如
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29function result = myFunction(a, b, varargin)
% 创建一个 inputParser 对象
p = inputParser;
% 设置输入参数保持顺序
p.KeepUnmatched = true;
% 必需参数
addRequired(p, 'a', @(x) validateattributes(x, {'numeric'}, {'scalar', 'nonnegative'}));
addRequired(p, 'b', @(x) validateattributes(x, {'numeric'}, {'vector'}));
% 可选参数
addOptional(p, 'c', 1, @(x) validateattributes(x, {'numeric'}, {'scalar', 'nonnegative'}));
% 名称-值对参数
addParameter(p, 'Verbose', false, @(x) validateattributes(x, {'logical'}, {'scalar'}));
% 解析输入参数
parse(p, a, b, varargin{:});
% 重新获取解析后的参数
a = p.Results.a;
b = p.Results.b;
c = p.Results.c;
verbose = p.Results.Verbose;
result = a + sum(b) + c;
if verbose
disp(['a: ', num2str(a)]);
disp(['b: ', num2str(b)]);
disp(['c: ', num2str(c)]);
disp(['Result: ', num2str(result)]);
end
end
对于 inputParser
对象,我们还可以设置它的一些属性来修改解析规则,例如:
FunctionName:默认为空。通常设置为 inputParser 所在函数的名字,这样可以在出错是给出是在哪个函数发生的;CaseSensitive:默认为 false,即对键的大小写不敏感,这样在输入键值对参数是,键的大小写不会影响解析;PartialMatching:默认为 true,即允许对键的部分匹配。例如如果定义了window参数,实际使用时只输入w,依然能够正确匹配;(如果存在歧义则会报错)
1 | p = inputParser; |
arguments
在R2019b推出的arguments块可能是MATLAB目前最推荐的参数检查语法,参考官方文档。(但是我并不觉得它最好,而且对版本要求有点高)
arguments块是一段单独的语法块,通常写在函数的开头部分,例如
1 | function myFunction(a, b, c, option) |
调用例如
1 | myFunction(5, "hello"); |
补充
对于参数检查有几个简单的原则:
- 函数文件只有主函数对外提供接口,剩下的子函数仅在文件内部可以调用,因此只有主函数需要考虑参数检查;(对于类文件来说,同理我们只需要考虑公开方法的参数检查)
- 对于某些对性能非常敏感的函数,可以仅在注释中说明,而不进行实际的参数检查。
除了参数检查,我们还可以为自定义函数提供自定义代码建议和自动填充功能,需要提供对应的
functionSignatures.json 文件,具体细节参考官方文档。
获取帮助
MATLAB 提供了高质量的帮助文档,除了基于 GUI,还可以直接在命令行中使用。
doc & docsearch
打开 MATLAB 文档浏览器(最新版已经改成使用系统浏览器),显示帮助中心对应的完整页面。
打开帮助中心首页 1
doc
打开abs对应的页面 1
doc abs
在帮助中心搜索关键字 1
docsearch abs
这两个命令都是完全基于离线版的MATLAB帮助中心,和在线的MATLAB帮助中心几乎一致(除了版本和语言可能不同),通常不支持对用户自定义代码的帮助。
help
help 命令可以在命令行中显示简要帮助信息。
help name会显示name指定的功能的帮助文本,例如函数、方法、类、工具箱、变量或命名空间。help则会显示与先前操作相关的内容。
例如查找函数的帮助信息 1
help abs
输出内容如下 1
2
3
4
5
6
7
8
9abs Absolute value.
abs(X) is the absolute value of the elements of X. When
X is complex, abs(X) is the complex modulus (magnitude) of
the elements of X.
See also sign, angle, unwrap, hypot.
Documentation for abs
Other uses of abs
对于不同的 name,help 的处理逻辑略有不同,具体来说:
- 如果 name 是变量,help 显示该变量的类的帮助文本。
- 要获取某个类的方法/属性的帮助,可以指定类名和方法名称(以句点分隔)。例如,要获取 classname 类的 methodname 方法的帮助,请键入 help classname.methodname。
- 如果 name 出现在 MATLAB 搜索路径上的多个文件夹中,help 将显示在搜索路径中找到的 name 的第一个实例的帮助文本。
- 如果 name 指定文件夹的名称或部分路径,help
函数默认将列出文件夹中每个程序文件的帮助文本的第一行。如果文件夹中含有特殊的
Contents.m文件(通常包括这个文件夹中的内容说明),会影响这里的行为。
lookfor
lookfor 会在 MATLAB 文档中所有参考页的摘要行和函数名称以及第三方和用户编写的 MATLAB 程序文件的帮助文本中搜索指定的关键字。
搜索可能比较耗时,搜索常用词得到的匹配结果通常有很多,最终会在命令行中以列表形式展示所有结果,每一行是对应的条目(指向的链接)以及对应的帮助文本的第一行(也就是使用 help 命令所展示的第一行内容)。 点击提供的链接相当于使用 help 命令查看对应条目,会在命令行中显示这一项的完整帮助信息。
例如 1
lookfor legendre
输出内容如下 1
2legendre - Associated Legendre functions
sym.legendreP - Legendre polynomials
如果将自己编写的相关代码也添加到 MATLAB 的搜索路径中,那么
lookfor 命令也会显示自己编写的代码,例如 1
2
3
4
5
6
7legendre - Associated Legendre functions
sym.legendreP - Legendre polynomials
gauss_legendre - Computation of the Nodes and Weights for the Gauss-Legendre Quadrature.
MatLegendre - : A class that provides Legendre polynomial basis functions.
MatLegendreDx - : A class that provides Legendre polynomial derivatives.
test_MatLegendre - Test suite for MatLegendre class
test_MatLegendreDx - Test suite for MatLegendreDx class
代码注释
现在我们关注如何让自己的代码注释尽可以符合 MATLAB 的规范,从而适配 MATLAB 内置的帮助系统。
虽然语言的定位类似,但是相比于Python可以使用注解进行类型提示,并结合代码分析工具进行静态检查,MATLAB其实更需要通过注释提供信息,因为有时传入的参数是列向量还是行向量都可能导致结果的差异。
函数文件添加注释
MATLAB的帮助命令将函数文件从文件开头找到的一大段注释视作帮助信息,帮助信息的第一行视作摘要。 建议在帮助信息中包括函数名称、简要说明、参数说明、返回值说明、示例代码等,在第一行的摘要中包括函数名称和简要说明。
注释通常会写在函数定义之后紧接的行,例如下面是官方文档提供的函数注释示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function c = addme(a,b)
% ADDME Add two values together.
% C = ADDME(A) adds A to itself.
%
% C = ADDME(A,B) adds A and B together.
%
% See also SUM, PLUS.
switch nargin
case 2
c = a + b;
case 1
c = a + a;
otherwise
c = 0;
end
但是并不必要,例如下面这两种写法都是可以的 1
2
3
4
5
6function func1()
% xxxxxxxxxxx
disp('hi')
end
1 | help func1 |
1 | % xxxxxxxxxxx |
1 | help func2 |
放置在代码之后的注释则不会被视作帮助信息,此时只会显示默认信息
1
2
3
4
5function func3()
disp('hi')
% xxxxxxxxxxx
end
1 | help func3 |
在帮助信息的最后一行支持如下写法 1
% See also SUM, PLUS.
此时MATLAB会尝试将SUM和PLUS链接到对应的帮助信息,否则会原样显示。
类文件添加注释
类文件的帮助信息与函数文件基本类似,参考写法如下 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45classdef myClass
% myClass Summary of myClass
% This is the first line of the description of myClass.
% Descriptions can include multiple lines of text.
%
% myClass Properties:
% a - Description of a
% b - Description of b
%
% myClass Methods:
% doThis - Description of doThis
% doThat - Description of doThat
properties
a % First property of myClass
% b - Second property of myClass
% The description for b has several
% lines of text.
b % Other comment
end
methods
function obj = myClass
end
function doThis(obj)
% doThis Do this thing
% Here is some help text for the doThis method.
%
% See also DOTHAT.
disp(obj)
end
function doThat(obj)
% doThat Do that thing
% Here is some help text for the doThat method.
%
% See also DOTHIS.
disp(obj)
end
end
end
下面对第一段帮助信息的各个部分进行解释:
- 首先是类的名称以及一行摘要;
- 然后是类的简要描述语句;
- 最后是属性列表和说明,以及方法列表和说明,如果其中包含类名称且后跟 Properties 或 Methods 以及一个冒号 (:),MATLAB 将创建指向这些属性或方法的帮助的超链接。
在属性列表中,可以在属性后面(或者上面)加上注释,这样就可以支持 help
命令对该属性获取帮助信息,例如 1
2help myClass.a
a - First property of myClass
在方法列表中,可以在方法内部加上注释,与函数注释基本相同,这样就可以支持
help 命令对该方法获取帮助信息,例如 1
2
3
4
5help myClass.doThis
doThis Do this thing
Here is some help text for the doThis method.
See also doThat.
注意:这里在类的帮助信息中展示的属性和方法最好是public的,私有的属性和方法可能无法在帮助信息中自动创建对应的链接。
单元测试
为了维护一份健壮的代码库,单元测试是必不可少的,参考官方文档。 MATLAB 提供的测试主要包括三种风格:
- 基于脚本
- 基于函数
- 基于类
基于脚本
可以通过一个脚本对指定功能进行测试,脚本名称必须以 test
开头或结尾,不区分大小写,否则测试文件可能被忽略。
每一代码节(%%)作为一个测试单元,随后的文本视作测试单元的名称,否则MATLAB会提供一个默认名称。
如果脚本中不含代码节,那么整个文件会被视作一个测试单元,测试单元的名称为脚本名称。
在测试单元中主要通过 assert 语句进行检查。
在第一个代码节之前的内容是测试的初始化代码,可以用于配置测试环境,如果这部分出现错误,会导致所有测试单元失败。
执行单元测试的流程为:在每次开始时执行初始化代码,然后跳转到某个测试单元执行,执行结束即退出,不会执行多个测试单元,无论成功或失败。
这种执行方式可以保证一个测试单元的失败不会导致整个测试中断,不同测试单元使用的变量不会相互影响。 但是由于脚本仍然是一个合法的MATLAB文件,我们还是可以直接运行整个文件,这意味着依次执行初始化代码和所有的测试单元,此时某处失败会导致整体中断,不同测试单元之间也会相互干扰。
考虑官网的例子,测试目标为 rightTri.m 1
2
3
4
5
6
7
8
9function angles = rightTri(sides)
A = atand(sides(1)/sides(2));
B = atand(sides(2)/sides(1));
hypotenuse = sides(1)/sind(A);
C = asind(hypotenuse*sind(A)/sides(1));
angles = [A B C];
end
测试脚本 rightTriTest.m 的内容如下 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39% test triangles
tri = [7 9];
triIso = [4 4];
tri306090 = [2 2*sqrt(3)];
triSkewed = [1 1500];
% preconditions
angles = rightTri(tri);
assert(angles(3) == 90,'Fundamental problem: rightTri not producing right triangle')
%% Test 1: sum of angles
angles = rightTri(tri);
assert(sum(angles) == 180)
angles = rightTri(triIso);
assert(sum(angles) == 180)
angles = rightTri(tri306090);
assert(sum(angles) == 180)
angles = rightTri(triSkewed);
assert(sum(angles) == 180)
%% Test 2: isosceles triangles
angles = rightTri(triIso);
assert(angles(1) == 45)
assert(angles(1) == angles(2))
%% Test 3: 30-60-90 triangle
angles = rightTri(tri306090);
assert(angles(1) == 30)
assert(angles(2) == 60)
assert(angles(3) == 90)
%% Test 4: Small angle approximation
angles = rightTri(triSkewed);
smallAngle = (pi/180)*angles(1); % radians
approx = sin(smallAngle);
assert(approx == smallAngle, 'Problem with small angle approximation')
需要通过 runtests 函数来执行单元测试 1
result = runtests('rightTriTest');
输出结果如下 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42Running rightTriTest
..
================================================================================
Error occurred in rightTriTest/Test3_30_60_90Triangle and it did not run to completion.
---------
Error ID:
---------
'MATLAB:assertion:failed'
--------------
Error Details:
--------------
Error using assert
Assertion failed.
Error in rightTriTest (line 31)
assert(angles(1) == 30)
================================================================================
.
================================================================================
Error occurred in rightTriTest/Test4_SmallAngleApproximation and it did not run to completion.
---------
Error ID:
---------
''
--------------
Error Details:
--------------
Error using assert
Problem with small angle approximation
Error in rightTriTest (line 39)
assert(approx == smallAngle, 'Problem with small angle approximation')
================================================================================
.
Done rightTriTest
__________
Failure Summary:
Name Failed Incomplete Reason(s)
===========================================================================
rightTriTest/Test3_30_60_90Triangle X X Errored.
---------------------------------------------------------------------------
rightTriTest/Test4_SmallAngleApproximation X X Errored.
可以进一步通过表格展示结果(这里可以看到,MATLAB根据节标题自动生成测试单元的标题)
1
2
3
4
5
6
7
8
9table(result)
ans =
4×6 table
Name Passed Failed Incomplete Duration Details
______________________________________________ ______ ______ __________ _________ ____________
{'rightTriTest/Test1_SumOfAngles' } true false false 0.020942 {1×1 struct}
{'rightTriTest/Test2_IsoscelesTriangles' } true false false 0.0032443 {1×1 struct}
{'rightTriTest/Test3_30_60_90Triangle' } false true true 0.0054824 {1×1 struct}
{'rightTriTest/Test4_SmallAngleApproximation'} false true true 0.0064785 {1×1 struct}
这里测试失败的原因是我们编写的测试不恰当,不应该对浮点数直接进行相等比较,而是应该指定容差,修正后的测试脚本
rightTriTest.m 内容如下 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42% test triangles
tri = [7 9];
triIso = [4 4];
tri306090 = [2 2*sqrt(3)];
triSkewed = [1 1500];
% Define an absolute tolerance
tol = 1e-10;
% preconditions
angles = rightTri(tri);
assert(angles(3) == 90,'Fundamental problem: rightTri not producing right triangle')
%% Test 1: sum of angles
angles = rightTri(tri);
assert(sum(angles) == 180)
angles = rightTri(triIso);
assert(sum(angles) == 180)
angles = rightTri(tri306090);
assert(sum(angles) == 180)
angles = rightTri(triSkewed);
assert(sum(angles) == 180)
%% Test 2: isosceles triangles
angles = rightTri(triIso);
assert(angles(1) == 45)
assert(angles(1) == angles(2))
%% Test 3: 30-60-90 triangle
angles = rightTri(tri306090);
assert(abs(angles(1)-30) <= tol)
assert(abs(angles(2)-60) <= tol)
assert(abs(angles(3)-90) <= tol)
%% Test 4: Small angle approximation
angles = rightTri(triSkewed);
smallAngle = (pi/180)*angles(1); % radians
approx = sin(smallAngle);
assert(abs(approx-smallAngle) <= tol, 'Problem with small angle approximation')
运行 1
result = runtests('rightTriTest');
输出内容如下 1
2
3
4Running rightTriTest
....
Done rightTriTest
__________
通过表格展示结果 1
2
3
4
5
6
7
8table(result)
ans =
4×6 table
Name Passed Failed Incomplete Duration Details
______________________________________________ ______ ______ __________ _________ ____________
{'rightTriTest/Test1_SumOfAngles' } true false false 0.014323 {1×1 struct}
{'rightTriTest/Test2_IsoscelesTriangles' } true false false 0.0045926 {1×1 struct}
{'rightTriTest/Test3_30_60_90Triangle' } true false false 0.0045378 {1×1 struct}
直接调用
result = runtests会查找当前文件夹下所有以test开头或结尾的.m文件,并执行所有测试。
基于函数
可以通过一个函数对指定功能进行测试,函数名称(以及文件名称)必须以
test 开头或结尾,不区分大小写,否则测试文件可能被忽略。
测试函数文件中的主函数的固定写法如下(除了主函数名称,其它内容尽量不要改动)
1
2
3function tests = exampleTest()
tests = functiontests(localfunctions);
end
这里使用 localfunctions
获取当前函数文件中的所有局部函数的句柄,每一个局部函数被视作一个测试单元。
测试函数文件中的局部函数的写法也相对固定:名称必须必须以
test 开头或结尾,不区分大小写,函数必须要接收一个名为
testCase 的参数,无返回值。
例如 1
2
3
4
5
6
7function testFunctionOne(testCase)
% Test specific code
end
function testFunctionTwo(testCase)
% Test specific code
end
MATLAB
允许我们在执行每一个测试单元(也就是调用对应的局部函数)的前后进行必要的操作,比如初始化一些数据,或者清理一些数据,这被称为刷新脚手架函数,包括测试前的
setup 函数和测试后的 teardown
函数,这两个特殊函数同样接收一个名为 testCase
的参数,无返回值。
1 | function setup(testCase) % do not change function name |
除此之外,还可以在所有测试前后进行必要的操作,这被称为文件脚手架函数,包括测试前的
setupOnce 函数和测试后的 teardownOnce
函数,与刷新脚手架的形式基本相同。
各种脚手架函数都是可选的,建议主要使用刷新脚手架函数,而非文件脚手架函数。
我们需要重点关注 testCase 这个对象:
- 它属于 matlab.unittest.FunctionTestCase 类型,这是一个 handle 类,因此可以用来传递数据;
- 它具有一个完全公开的结构体属性
TestData,可以在脚手架函数中通过向这个结构体添加字段以传递数据。
考虑官网的例子,测试目标为 quadraticSolver.m
1
2
3
4
5
6
7
8
9
10
11
12function r = quadraticSolver(a,b,c)
% quadraticSolver returns solutions to the
% quadratic equation a*x^2 + b*x + c = 0.
if ~isa(a,'numeric') || ~isa(b,'numeric') || ~isa(c,'numeric')
error('quadraticSolver:InputMustBeNumeric', ...
'Coefficients must be numeric.');
end
r(1) = (-b + sqrt(b^2 - 4*a*c)) / (2*a);
r(2) = (-b - sqrt(b^2 - 4*a*c)) / (2*a);
end
测试函数 quadraticSolverTest.m 的内容如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function tests = quadraticSolverTest()
tests = functiontests(localfunctions);
end
%% Test Functions
function testRealSolution(testCase)
actSolution = quadraticSolver(1,-3,2);
expSolution = [2 1];
verifyEqual(testCase,actSolution,expSolution)
end
function testImaginarySolution(testCase)
actSolution = quadraticSolver(1,2,10);
expSolution = [-1+3i -1-3i];
verifyEqual(testCase,actSolution,expSolution)
end
注意这里使用了 verifyEqual
函数,要求实际结果和预期值完全相等,我们也可以加上容差(绝对容差或相对容差)
1
2verifyEqual(testCase,1.5,2,AbsTol=1)
verifyEqual(testCase,1.5,2,RelTol=1)
与之类似的函数还有
verifyNotEqual,verifyThat 等,
完整内容可以查看验证、断言及其他鉴定一览表。
对测试函数的调用和前面的测试脚本基本相同 1
results = runtests('quadraticSolverTest');
运行结果如下 1
2
3
4Running quadraticSolverTest
..
Done quadraticSolverTest
__________
需要注意的是,与测试脚本不同,直接调用这个测试函数并不会触发任何测试。
基于类
可以通过一个自定义类对指定功能进行测试,类名称(以及文件名称)必须以
test 开头或结尾,不区分大小写,否则测试文件可能被忽略。
测试类需要直接或间接继承 matlab.unittest.TestCase 类型。
测试类的具有 Test
属性的方法被视作单元测试,除此之外,还有几个属性
TestMethodSetup和TestMethodTeardown方法在每个 Test 方法之前和之后运行。TestClassSetup和TestClassTeardown方法在测试类中的所有 Test 方法之前和之后运行。
示例代码如下 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32classdef FigurePropertiesTest < matlab.unittest.TestCase
properties
TestFigure
end
methods (TestMethodSetup)
function createFigure(testCase)
testCase.TestFigure = figure;
end
end
methods (TestMethodTeardown)
function closeFigure(testCase)
close(testCase.TestFigure)
end
end
methods (Test)
function defaultCurrentPoint(testCase)
cp = testCase.TestFigure.CurrentPoint;
testCase.verifyEqual(cp,[0 0], ...
"Default current point must be [0 0].")
end
function defaultCurrentObject(testCase)
import matlab.unittest.constraints.IsEmpty
co = testCase.TestFigure.CurrentObject;
testCase.verifyThat(co,IsEmpty, ...
"Default current object must be empty.")
end
end
end
对比下来,感觉基于脚本或函数的测试已经足够使用了,基于自定义类型的测试在形式上有点复杂了,因此简单了解一下即可。
补充
在目录中直接调用 result = runtests
会查找当前文件夹下所有以 test 开头或结尾的 .m
文件,并执行所有测试,
但是我们有时需要遍历指定目录以及它的子目录中的所有测试文件,并且最好生成测试报告,下面提供一份参考脚本。
1 | import matlab.unittest.TestRunner; |
