6

I want to be able to write jasmine-like tests in Matlab. So something like

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

There are two strategies I tried to implement this:

(1) the first strategy uses structs

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

(The real implementation will not just call assert)

However this does not work:

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

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

(2) The second strategy I tried is using a class:

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

At first glance this looks fine: You can run in the console

expect(1).toBe(1);

However, running this not in the console but in a script gives

??? 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);

Is here any way to make this idea work in matlab at all?

Machavity
  • 30,841
  • 27
  • 92
  • 100
manuels
  • 1,511
  • 3
  • 14
  • 26

5 Answers5

7

In the latest versions of MATLAB (13a/13b) there's a unit testing framework built in that looks very similar to what you're attempting. Instead of

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

you would write

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

(You could also/instead have assumeThat, assertThat, or fatalAssertThat).

If for some reason you wish to implement your own framework, note the small difference in your syntaxes - you have a dot whereas MathWorks have a comma between myfibonacci(0) and the test condition.

In MATLAB you can't index into the result of a subscripted expression like that (well, you could, but you would have to overload subsref, and that's a world of pain, trust me). So the way they've done it is to introduce the test comparison conditions as a separate package, and apply them as a separate input argument rather than as a method with the dot syntax.

Take a look at the documentation for the new unit testing framework to find out more about either the framework itself, or (if you'd prefer to roll your own) the syntaxes they have designed as a comparison to yours.

Sam Roberts
  • 23,951
  • 1
  • 40
  • 64
  • Thanks for your answer! However, I want to use this pattern not only for unit testing but for other use cases, too. – manuels Sep 09 '13 at 12:13
  • +1 good observation about how MATLAB implemented this BDD-style testing, avoiding the whole mess of using `subsref` – Amro Sep 10 '13 at 01:39
4

Your class definition works fine if you create a function instead of script

So instead of testscript.m containing

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

you need a function testfunc.m containing

function testfunc
expect(myfibonacci(0)).toBe(0);
expect(myfibonacci(5)).toBe(15);
expect(myfibonacci(10)).toBe(55);
Mohsen Nosratinia
  • 9,844
  • 1
  • 27
  • 52
3

To add to @MohsenNosratinia's remark, if you use nested-functions/closure instead of OOP classes, you get the yet another inconsistency:

function obj = expect(expr)
    obj = struct();
    obj.toBe = @toBe;
    function toBe(expected)
        assert(isequal(expr,expected))
    end
end
  1. The syntax doesn't work from the command prompt:

    >> expect(1+1).toBe(2)
    Undefined variable "expect" or class "expect". 
    
  2. Doesn't work either from a script:

    testScript.m

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

    with the same error as before:

    >> testScript
    Undefined variable "expect" or class "expect".
    Error in testScript (line 1)
    expect(1+1).toBe(2)
    
  3. But for an M-file function:

    testFcn.m

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

    it is strangely accepted:

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

    (the second assertion failed as expected, but no syntax errors!)


I consider throwing a "syntax error" to be the correct outcome here, as you should not directly index into the result of a function call. If you do, I think of it as "undefined behavior" :) (it might work, but not in all cases!)

Instead, you should first store the result in a temporary variable, then apply indexing into it:

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

or resort to ugly hacks like:

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

or even undocumented functions:

>> builtin('_paren', builtin('_dot', expect(1+1), 'toBe'), 2)
Community
  • 1
  • 1
Amro
  • 123,847
  • 25
  • 243
  • 454
  • Both this and @MohsenNosratinia's observation feel like bugs to me. Error or not, it should work the same within functions and scripts/command line. – Sam Roberts Sep 10 '13 at 08:51
  • 2
    Note that if, at the command line, you type `x = @expect` and then `x(1+1).toBe(2)`, you get the error `Improper index matrix reference`, not `Undefined variable or class`. – Sam Roberts Sep 10 '13 at 08:55
  • 1
    Good call with function handles, I tried it and it errors in all three cases (command,script,function). Still not the most obvious error messages, the parser is clearly struggling here! You gotta admit the "MATLAB" syntax has its weaknesses. If they were to write the language from scratch today, it could be made a lot cleaner, something like [Julia](http://julialang.org/) maybe :) Some of the most important differences: using square brackets for array indexing, and requiring parentheses to call functions with zero arguments. This immediately clears a lot ambiguity in the syntax.. – Amro Sep 10 '13 at 16:05
2

Here's an examplary implementation with an overloaded subsref method. It could also be done with only one class I guess, but that would make the subsref-overloading even uglier.

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

And

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

Results in:

>> expect(1).toBe(1)
>> expect(1).toBe(2)
Error using TestValue/toBe (line 13)
Assertion failed.
sebastian
  • 9,526
  • 26
  • 54
  • 1
    This is nice, but what happens if someone wanted to have an array of `expect` objects? They would probably want `myarrayofexpects(1)` to return the first element of the array, rather than to create an object of class `TestValue`, right? Distinguishing those cases is going to be a pain, and then you're down the path to the hell of overloading `subsref`... – Sam Roberts Sep 09 '13 at 11:13
  • 1
    With matlab using '()' for function-calling as well as for indexing, one obviously has to decide for one or the other. In this case one should probably forbid an array-creation of `Tester` objects. Your comment would also apply e.g. to function handles - Mathworks' solution here is to simply not allow the creation of function handles (try `[@(x) x, @(x) x]`, which also gives a proposed solution for your use case). – sebastian Sep 09 '13 at 11:21
  • @sebastian: you could override `cat`, `horzcat`, and `vertcat` methods to forbid explicit array-creation of objects such as `[obj,obj]`. Unfortunately there is no way that I know of to forbid creation using: `obj(5) = Tester()` (other than making the constructor private altogether and use a static method as class factory). As SamRoberts said, the general rule should be to never directly index into the result of a subscripted expression (MATLAB could even crash in some cases as shown here: http://stackoverflow.com/q/17515941/97160) – Amro Sep 10 '13 at 01:52
  • Overloading `subsasgn` does the trick: `function this = subsasgn(this, idx, obj) error('Tester:subsasgn', 'Subscripted assignment not allowed for Tester objects'); end` does the trick. I'm not saying this is a nice way to do things though... On the other hand it's implemented in a similar way for e.g. a `containers.Map` - though this doesn't allow for chained subscriptions – sebastian Sep 10 '13 at 08:23
  • @sebastian: ah thanks for the tip, although I can't help but feel there is a catch somewhere, there always is when overloading `subsref` and `subsasgn` :) Too bad `containers.Map` is built-in and we cant see the source code. – Amro Sep 10 '13 at 16:13
2

I think the important thing to recognize is that MATLAB is not JavaScript. The jasmine-like syntax utilizes the semantics of JavaScript for this API. I think when designing any API it is important and valuable to think about the language it is written in and subscribe not only to its technical limitations, but also to its technical strengths and its established idioms.

As Sam alluded to this is the approach taken in the MATLAB Unit Test Framework. For example, the constraints approach does not attempt to invoke a function immediately after any other function call or indexing operation, but rather constructs the constraint directly and names the constraint so as to create a literate programming interface. One example strength of MATLAB in this case is that it doesn't require a "new" before construction like Java/C#/etc do. You can actually see similar tradeoffs being done with Hamcrest matchers and NUnit constraints, neither of which subscribe to the same approach to producing their literate verifications, preferring instead to design their approaches after the languages to which they are written in.

Also, while it is indeed a unit test framework, it certainly can be used to write other types of tests like system/integration. Given you mentioned you are actually writing tests, I would highly recommend utilizing the solution that is already available rather than reinventing the wheel. There is quite a lot of investment in the constraints and the other surrounding features of the framework, and it is definitely production grade.

Andy Campbell
  • 2,177
  • 13
  • 15
  • 1
    AndyCampbell, I agree - nevertheless, there's some sort of bug here in the way MATLAB is parsing things, as described more fully in @Amro's answer. It may be related also to the bug described in http://stackoverflow.com/questions/17515941/indexed-object-dot-notation-method-gives-scalar-property. Could you put in a bug report please? – Sam Roberts Sep 11 '13 at 08:57
  • @SamRoberts - That I will do. – Andy Campbell Sep 11 '13 at 16:15