MATLAB单元测试:对函数调用进行测试

3
我有以下非常简单的实用函数:
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

原来,尽管该函数非常简单,但它存在一个问题使我遇到了问题(assert条件应为islogical(verbose) && isscalar(verbose)而不仅仅是islogical(verbose)),因此我想在其周围实施一些单元测试。
请注意,我不想测试fprintf - 我假设它没问题。那么有没有办法测试这样的内容:
  • “如果verbose是逻辑标量true,则调用fprintf
  • “如果verbose是逻辑标量false,则不会调用fprintf
  • “如果verbose是逻辑非标量,则不会调用fprintf
  • “如果verbose不是逻辑值,则不会调用fprintf
我找不到验证是否向特定函数发出了调用的方法。有什么建议吗?我能想到的唯一方法是使用自己的函数来模拟fprintf,然后将其覆盖MATLAB路径上的真实函数,在某种程度上触发一个fprintfCalled事件,由测试代码监听以确定何时被调用。这是唯一的方法吗?感觉有些过分了。
或者可能我走错了方向 - 也许我应该忘记测试所做的调用,而是直接测试vfprintf的命令行和/或文件输出。但是这感觉就像我在测试fprintf而不是vfprintf
也许我想得太多了,但我想改进我的测试实践,所以会感激任何建议。谢谢!

让我看看我是否理解正确:在您的单元测试中,您想确保正确使用了vfprintf,因此您需要知道是否在任何时候调用了fprintf。是这样吗?难道您不能检查文件是否已更改大小/被修改吗? - Ander Biguri
@AnderBiguri 有两个要点 - 首先,我想测试将输出到命令行以及文件。但更重要的是,虽然我可以这样做,但感觉我会测试fprintf而不是vfprintf。理想情况下,我希望只依赖于fprintf,并且只测试是否调用了它。(但也许你会建议这对单元测试来说是一个不好的实践,我不知道)。 - Sam Roberts
我在单元测试方面并不是专家,所以不要完全依赖我的说法。然而,我认为在进行单元测试时,你不应该修改函数,否则就是“作弊”。如果你测试文件是否已被修改(或者在命令行中是否已写入某些内容,这很容易做到),那么你正在测试vfprintf,因为它并不总是会被打印出来,这取决于verbose。因此,你并没有测试fprintf能否正确地完成所有工作,而是测试了vfprintf。对fprintf的单元测试将会更加复杂。 - Ander Biguri
1个回答

2

我认为你现在有四个选择。我喜欢第四个,但我会逐个列出它们:

  1. 在evalc内执行对vfprintf的调用,以验证打印到命令窗口的内容,或创建一个文件并将其打印到该文件中。缺点1:这种方法测试fprintf(虽然更多是学术性质,因为fprintf很少会发生重大变化或不遵守其合同)。缺点2:两种情况都与全局状态交互-要么是全局命令窗口输出(其他东西可以打印到其中),要么是文件系统。虽然这不是世界末日,但如果可以避免最好不要这样做。如果你的测试完全避免接触外部环境,那么会更好。
  2. 阴影fprintf函数。您可以通过将其放在路径之外的文件夹中,然后在该文件夹中添加自己的fprintf函数,然后在测试中使用PathFixture将其添加到路径的顶部来实现。缺点:仍然依赖于更改全局状态(路径),并且可能会更慢,因为从语言执行角度来看,路径操作是昂贵的。我不是很喜欢这个选择,但以下是它的实现方法。如果可以的话,我建议选择下面的第4种:

VerboseArgumentsHolder.m

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

VerbosePrinterSpy.m

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

* (测试文件夹)/过载/ 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. 将您的生产代码重构为具有打印接口,然后您可以添加一个特定于测试的间谍作为该接口。这是一个不错的方法,但它对您的软件结构有一个影响,可能不太容易调整,特别是如果您的代码库已经严重依赖于此实用程序。

  2. 由于您只是直接将varargin传递给fprintf,因此您可以创建一个具有fprintf方法的测试double来专门测试此功能。然后,fprintf调用将分派到您的测试特定类,该类可以简单地对输入进行监视。它可能看起来像这样:

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

希望这能帮到你!

谢谢Andy。所以我同意1看起来很混乱,3看起来很好,但是vfprintf不幸地散布在整个代码中。4很巧妙,看起来在这里完美无缺,但似乎只能因为我恰好直接通过varargin传递。如果我要追求2,最好的方法是让我的阴影fprintf报告给testCase它已经被调用了(以及它的参数)? - Sam Roberts
你需要使用持久变量或者带有Constant属性的协作者,它们可以保留一个句柄。基本上,在测试中你需要重置全局状态,然后在fprintf重载中将输入参数的记录存储在常量属性中存储的句柄中。这是一个比较巧妙的方法,但是缺点也很明显,因为只适用于特定测试的类。如果您想看一个例子,我可以添加一些代码。 - Andy Campbell
没问题 - 我明白你的建议。我会尝试第二和第四种方法,看哪个感觉最好。再次感谢,安迪。 - Sam Roberts
忍不住啦 ;-) - Andy Campbell
这可能会让你感到有趣 - 当我使用TestRunner.withNoPlugins时,我的测试现在通过了,但是当我运行TestRunner.withTextOutput('Verbosity',4)时失败了。如果你花一点时间思考,我相信你会猜到原因,然后扇自己一个耳光: )。请不要再浪费时间帮助我深入挖掘这个坑…… - Sam Roberts
哈哈,使用哪种方法?路径重载的一种?是的,使用这种方法时有时需要注意测试框架本身调用了哪些全局函数。这种路径方法的另一个缺点。祝你好运,玩得开心!无论是什么或谁在调用内置fprintf,选项4都可以正常工作 :-) - Andy Campbell

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接