Sam Roberts
Sam Roberts

Reputation: 24127

MATLAB unit testing a function call

I have the following very simple utility function:

function vfprintf(verbose, varargin)
% VFPRINTF Display output optionally depending on the level of verbosity.
%
% VFPRINTF(TF, ARGS) passes the arguments ARGS to the built-in MATLAB
% command |fprintf| if TF is logical true. If TF is logical false, VFPRINTF
% does nothing.

assert(islogical(verbose),...
    'utils:InvalidVerbose',...
    'VERBOSE must be logical true or false');

if verbose
    fprintf(varargin{:});
end

It turns out that even though the function is super simple, it had a issue that caused me a problem (the assert condition should have been islogical(verbose) && isscalar(verbose), not just islogical(verbose)), so I'd like to implement some unit tests around it.

Note that I don't want to test fprintf - I'm assuming that's OK. So is there a way that I can test something like:

I can't find a way to verify that a call is made out to a particular function. Any ideas? The only thing I can think of is to mock out fprintf with my own function that shadows the real one on the MATLAB path, which somehow raises an fprintfCalled event that is listened to by the testing code to tell when it's called. Is that the only approach? Seems like overkill.

Or perhaps I'm approaching this the wrong way - maybe I should forget about testing the calls that are made, and instead be testing the command-line and/or file output of vfprintf directly. But that feels like I'm then testing fprintf rather than vfprintf.

Maybe I'm overthinking things, but I'd like to improve my testing practices, so would appreciate some advice. Thanks!

Upvotes: 3

Views: 619

Answers (1)

Andy Campbell
Andy Campbell

Reputation: 2187

I think at this point you really have 4 options. I like the 4th, but I'll run through them all:

  1. Execute your calls to vfprintf within an evalc to validate what is printed to the command window or create a file and print to that file. Drawbacks 1: kind of tests fprintf (athough its more academic since there is very low chance that fprintf will change significantly or not honor its contract). Drawback 2: Both cases interact with global state - either the global command window output (which other things can print to) or the filesystem. Not the end of the world, but good to avoid if you can. It would be much nicer for your test to completely avoid touching the outside world.
  2. Shadow the fprintf function. You can do this by putting it in a folder off the path and then adding your own fprintf function in that folder, then using a PathFixture in your test to add it to the top of the path. Drawback: Still relies on changing global state (the path) and it may be slower because path manipulation is expensive from a language execution perspective. Not a big fan, but here is how this might play out. If you can I'd suggest going with #4 below over this:

VerboseArgumentsHolder.m

    classdef VerboseArgumentsHolder < handle
        properties
            Arguments = {};
        end
    end

VerbosePrinterSpy.m

    classdef VerbosePrinterSpy
        properties(Constant)
            ArgumentsHolder = VerboseArgumentsHolder;
        end
    end

* (test folder)/overloads/fprintf/fprintf.m*

   function fprintf(varargin)
   argHolder = VerbosePrinterSpy.ArgumentsHolder;
   argHolder.Arguments = varargin;
   end

vfprintfTest.m

    classdef vfprintfTest < matlab.unittest.TestCase
        methods(Test)
            function testWhenScalarTrue(testCase)
                import matlab.unittest.fixtures.PathFixture;

                testCase.applyFixture(PathFixture(...
                    fullfile((test folder),'overloads','fprintf')));

                argHolder = VerbosePrinterSpy.ArgumentsHolder;
                argHolder.Arguments = {}; % reset values since this is global and stateful.
                vfprintf(true,'dummy input');
                testCase.verifyEqual(argHolder.Arguments, 'dummy input');
            end
            function testWhenScalarFalse(testCase)

                testCase.applyFixture(PathFixture(...
                    fullfile((test folder),'overloads','fprintf')));

                argHolder = VerbosePrinterSpy.ArgumentsHolder;
                argHolder.Arguments = {}; % reset values

                vfprintf(false,'dummy input');
                testCase.verifyEmpty(argHolder.Arguments);
            end
        end
    end
  1. Restructure your production code to have an interface for printing, then you can add a test specific spy as that interface. This is a nice approach, but has an implication on your software structure that may not be all that easy to adjust to , especially if your codebase is already heavily reliant on this utility.

  2. Since you are just passing on the varargin to fprintf directly, then you can create a test double with an fprintf method to specifically test this out. Then, the fprintf call would dispatch to your testing specific class which can simply spy on the inputs. It may look something like this:

VerbosePrinterSpy.m

    classdef VerbosePrinterSpy < handle
        properties
            WasInvoked = false;
            ArgumentsUsedInPrintCall = {'Not invoked'};
        end

        methods
            function fprintf(spy, varargin)
                spy.WasInvoked = false;
                spy.ArgumentsUsedInPrintCall = varargin;
            end
        end
    end

vfprintfTest.m

    classdef vfprintfTest < matlab.unittest.TestCase
        methods(Test)
            function testWhenScalarTrue(testCase)
                spy = VerbosePrinterSpy;
                vfprintf(true, spy, 'dummy input');
                testCase.verifyTrue(spy.WasInvoked);
                testCase.verifyEqual(spy.ArgumentsUsedInPrintCall, 'dummy input');
            end
            function testWhenScalarFalse(testCase)
                spy = VerbosePrinterSpy;
                vfprintf(false, spy, 'dummy input');
                testCase.verifyFalse(spy.WasInvoked);
            end
        end
    end

Hope that helps!

Upvotes: 2

Related Questions