【问题标题】:Matlab: Improper index matrix reference (or outsmarting matlab)Matlab:不正确的索引矩阵参考(或胜过 matlab)
【发布时间】:2013-09-12 18:54:04
【问题描述】:

我希望能够在 Matlab 中编写类似jasmine 的测试。 所以像

expect(myfibonacci(0)).toBe(0);
expect(myfibonacci(5)).toBe(15);
expect(myfibonacci(10)).toBe(55);

我尝试了两种策略来实现:

(1)第一种策略使用结构体

expect = @(actual_value) struct('toBe', @(expected_value) assert(actual_value == expected_value));

(真正的实现不会只是调用assert)

但这不起作用:

expect(1).toBe(1); % this triggers a syntax error
??? Improper index matrix reference.

% this will work:
x = expect(1);
x.toBe(1);

(2) 我尝试的第二种策略是使用类:

classdef expect
properties (Hidden)
    actual_value
end

methods
    function obj = expect(actual_value)
        obj.actual_value = actual_value;
    end

    function obj = toBe(obj, expected_value)
        assert(obj.actual_value == expected_value);
    end
end
end

乍一看,这看起来不错: 可以在控制台运行

expect(1).toBe(1);

但是,不是在控制台中而是在脚本中运行它会给出

??? Static method or constructor invocations cannot be indexed.
Do not follow the call to the static method or constructor with
any additional indexing or dot references.

Error in ==> test at 1
expect(1).toBe(1);

有什么方法可以让这个想法在 matlab 中发挥作用吗?

【问题讨论】:

    标签: matlab unit-testing oop syntax


    【解决方案1】:

    在最新版本的 MATLAB (13a/13b) 中,内置了一个单元测试框架,看起来与您正在尝试的非常相似。而不是

    expect(myfibonacci(0)).toBe(0);
    

    你会写

    import matlab.unittest.constraints.IsEqualTo
    testCase.verifyThat(myfibonacci(0), IsEqualTo(0))
    

    (您也可以/改为使用assumeThatassertThatfatalAssertThat)。

    如果出于某种原因您希望实现自己的框架,请注意语法上的细微差别 - 您有一个点,而 MathWorks 在myfibonacci(0) 和测试条件之间有一个逗号。

    在 MATLAB 中,您不能索引到这样的下标表达式的结果(嗯,您可以,但您必须重载 subsref,这是一个痛苦的世界,相信我)。所以他们的做法是将测试比较条件作为单独的包引入,并将它们作为单独的输入参数应用,而不是作为具有点语法的方法。

    查看the documentation 以了解新的单元测试框架,以了解有关框架本身或(如果您更愿意自己动手​​)他们设计的语法与您的比较的更多信息。

    【讨论】:

    • 感谢您的回答!但是,我不仅想将此模式用于单元测试,还想用于其他用例。
    • +1 很好地观察了 MATLAB 如何实现这种 BDD 风格的测试,避免了使用 subsref 的整个混乱情况@
    【解决方案2】:

    这是一个带有重载 subsref 方法的示例实现。 我猜也可以只用一个类来完成,但这会使 subsref 重载更加丑陋。

    classdef Tester < handle
        methods
            function obj = Tester()
            end
    
            function [varargout] = subsref(this,S)
    
                if S(1).type(1) =='('
                    tv = TestValue(S(1).subs{:});
                end
    
                if numel(S) > 1
                    try
                        [varargout{1:nargout}] = builtin('subsref', tv, S(2:end));
                    catch me
                        me.throwAsCaller();
                    end
                else
                    varargout{1} = tv;
                end
    
            end
        end
    end
    

    classdef TestValue 
        properties (Hidden)
            value;
        end
        methods
            function this = TestValue(value)
                this.value = value;
            end
    
            function toBe(this, v)
                assert( isequal(this.value, v) );
            end
        end
    end
    

    结果:

    >> expect(1).toBe(1)
    >> expect(1).toBe(2)
    Error using TestValue/toBe (line 13)
    Assertion failed.
    

    【讨论】:

    • 这很好,但是如果有人想要一个 expect 对象数组会发生什么?他们可能希望myarrayofexpects(1) 返回数组的第一个元素,而不是创建类TestValue 的对象,对吧?区分这些情况会很痛苦,然后你就会陷入重载的地狱subsref...
    • 使用 matlab 使用 '()' 进行函数调用和索引,显然必须决定其中一个。在这种情况下,可能应该禁止创建Tester 对象的数组。您的评论也适用,例如函数句柄 - Mathworks 的解决方案是不允许创建函数句柄(尝试[@(x) x, @(x) x],它也为您的用例提供了建议的解决方案)。
    • @sebastian:您可以覆盖cathorzcatvertcat 方法来禁止显式创建数组对象,例如[obj,obj]。不幸的是,我知道没有办法禁止创建使用:obj(5) = Tester()(除了将构造函数完全设为私有并使用静态方法作为类工厂)。正如 SamRoberts 所说,一般规则应该是永远不要直接索引下标表达式的结果(在某些情况下,MATLAB 甚至可能崩溃,如下所示:*.com/q/17515941/97160
    • 重载subsasgn 可以解决问题:function this = subsasgn(this, idx, obj) error('Tester:subsasgn', 'Subscripted assignment not allowed for Tester objects'); end 可以解决问题。我并不是说这是一种很好的做事方式......另一方面,它以类似的方式实现,例如containers.Map - 虽然这不允许链式订阅
    • @sebastian: 啊,谢谢你的提示,虽然我不禁觉得某处有问题,但在超载时总会出现subsrefsubsasgn :) 太糟糕了containers.Map是内置的,我们看不到源代码。
    【解决方案3】:

    如果你创建一个函数而不是脚本,你的类定义就可以正常工作

    所以而不是testscript.m 包含

    expect(myfibonacci(0)).toBe(0);
    expect(myfibonacci(5)).toBe(15);
    expect(myfibonacci(10)).toBe(55);
    

    你需要一个函数 testfunc.m 包含

    function testfunc
    expect(myfibonacci(0)).toBe(0);
    expect(myfibonacci(5)).toBe(15);
    expect(myfibonacci(10)).toBe(55);
    

    【讨论】:

    • 哈哈,matlab 好高深莫测!
    【解决方案4】:

    补充@MohsenNosratinia 的评论,如果你使用嵌套函数/闭包而不是 OOP 类,你会得到另一个不一致的地方:

    function obj = expect(expr)
        obj = struct();
        obj.toBe = @toBe;
        function toBe(expected)
            assert(isequal(expr,expected))
        end
    end
    
    1. 该语法在命令提示符下不起作用:

      >> expect(1+1).toBe(2)
      Undefined variable "expect" or class "expect". 
      
    2. 在脚本中也不起作用:

      testScript.m

      expect(1+1).toBe(2)
      expect(1*1).toBe(2)
      

      和以前一样的错误:

      >> testScript
      Undefined variable "expect" or class "expect".
      Error in testScript (line 1)
      expect(1+1).toBe(2)
      
    3. 但对于 M 文件功能:

      testFcn.m

      function testFcn
          expect(1+1).toBe(2)
          expect(1*1).toBe(2)
      end
      

      它被奇怪地接受了:

      >> testFcn
      Error using expect/toBe (line 5)
      Assertion failed.
      Error in testFcn (line 3)
          expect(1*1).toBe(2) 
      

      (第二个断言按预期失败,但没有语法错误!)


    我认为在这里抛出“语法错误”是正确的结果,因为您不应该直接索引函数调用的结果。如果你这样做,我认为它是 “未定义的行为” :)(它可能有效,但并非在所有情况下都有效!)

    相反,您应该首先将结果存储在一个临时变量中,然后对其应用索引:

    >> obj = expect(1+1);
    >> obj.toBe(2);
    

    resort to ugly hacks 喜欢:

    >> feval(subsref(expect(1+1), substruct('.','toBe')), 2)
    

    甚至是未记录的函数:

    >> builtin('_paren', builtin('_dot', expect(1+1), 'toBe'), 2)
    

    【讨论】:

    • 这和@MohsenNosratinia 的观察对我来说都像是错误。错误与否,它应该在函数和脚本/命令行中工作。
    • 请注意,如果在命令行中输入x = @expect,然后输入x(1+1).toBe(2),则会收到错误Improper index matrix reference,而不是Undefined variable or class
    • 用函数句柄调用很好,我试过了,在所有三种情况下(命令、脚本、函数)都出错。仍然不是最明显的错误消息,解析器显然在这里挣扎!您必须承认“MATLAB”语法有其弱点。如果他们今天从头开始编写语言,它可能会变得更简洁,比如Julia 可能:) 一些最重要的区别:使用方括号进行数组索引,并且需要括号来调用零参数的函数.这立即消除了语法中的很多歧义..
    【解决方案5】:

    我认为重要的是要认识到 MATLAB 不是 JavaScript。类似 jasmine 的语法为此 API 使用了 JavaScript 的语义。我认为在设计任何 API 时,重要且有价值的是考虑编写它所使用的语言,并且不仅要承认其技术限制,还要承认其技术优势和已建立的惯用语。

    正如 Sam 提到的,这是 MATLAB 单元测试框架中采用的方法。例如,约束方法不会尝试在任何其他函数调用或索引操作之后立即调用函数,而是直接构造约束并命名约束以创建文字编程接口。在这种情况下,MATLAB 的一个示例优势是它不需要像 Java/C#/etc 那样在构造之前使用“new”。您实际上可以看到使用 Hamcrest matchersNUnit constraints 进行了类似的权衡,它们都没有订阅相同的方法来生成他们的识字验证,而是更喜欢按照编写它们的语言来设计他们的方法。

    此外,虽然它确实是一个单元测试框架,但它当然可以用于编写其他类型的测试,例如系统/集成。鉴于您提到您实际上是在编写测​​试,我强烈建议您使用已经可用的解决方案,而不是重新发明*。在框架的约束和其他周边特性上投入了相当多,而且绝对是生产级的。

    【讨论】:

    • AndyCampbell,我同意 - 尽管如此,MATLAB 解析事物的方式存在某种错误,正如@Amro 的回答中更全面地描述的那样。它也可能与*.com/questions/17515941/… 中描述的错误有关。请您提交一份错误报告吗?
    • @SamRoberts - 我会做的。