Main Content

创建 Mock 对象

进行单元测试时,您经常要单独测试完整系统的一部分,将其与所依赖的组件隔离。要测试该系统的一部分,我们可以代入 mock 对象来替代所依赖的组件。mock 对象至少实现与生产对象相同的接口的一部分,但其实现方式通常很简单、高效、可预测和可控制。当您使用模拟框架时,受测组件并不清楚其协作对象是 "real" 对象还是 mock 对象。

Test a component using mocked-up dependencies.

例如,假定您想测试一种用于购买股票的算法,但不想测试整个系统。您可以使用一个 mock 对象来替换股票价格查找功能,使用另一个 mock 对象来验证交易者是否购买了股票。您所测试的算法不知道它是在操作 mock 对象,因此您可以在与系统的其余部分隔离情况下测试算法。

通过 mock 对象,您可以定义行为(称为 stubbing 的进程)。例如,可以指定某个对象生成对查询的预定义响应。此外,还可以拦截并记住从受测组件发送到 mock 对象的消息(称为 spying 的进程)。例如,可以验证是否调用了特定方法或设置了某个属性。

测试处于隔离状态的组件的典型工作流如下:

  1. 为所依赖的组件创建 mock。

  2. 定义这些 mock 的行为。例如,定义模拟方法或属性(使用一组特定的输入进行调用时)返回的输出。

  3. 测试所关注的组件。

  4. 验证所关注的组件与模拟组件之间的交互。例如,验证是否使用特定输入调用了模拟方法或是否设置了某个属性。

依赖的组件

在此示例中,受测组件是一个简单的日间交易算法。它是您要独立于其他组件单独测试的系统部分。日间交易算法有两个依赖项:检索股票价格数据的数据服务以及股票购买代理。

在您的当前工作文件夹下的文件 DataService.m 中,创建一个包含 lookupPrice 方法的抽象类。

classdef DataService
    methods (Abstract,Static)
        price = lookupPrice(ticker,date)
    end
end

在生产代码中,可能有多个属于 DataService 类的具体实现,例如 BloombergDataService 类。此类使用 Datafeed Toolbox™。但是,由于我们为 DataService 类创建 mock,因此您不需要安装该工具箱即可运行交易算法测试。

classdef BloombergDataService < DataService
    methods (Static)
        function price = lookupPrice(ticker,date)
            % This method assumes you have installed and configured the
            % Bloomberg software.
            conn = blp;
            data = history(conn,ticker,'LAST_PRICE',date-1,date);
            price = data(end);
            close(conn)
        end
    end
end

在此示例中,假定代理组件尚未开发。实现该组件后,它便会带有一个 buy 方法,用于接受股票代码和要购买的指定股数,并返回状态代码。代理组件的 mock 使用隐式接口,不从超类派生。

受测组件

在您的当前工作文件夹下的文件 trader.m 中,创建一个简单的日间交易算法。trader 函数接受以下项目作为输入:用于查找股票价格的数据服务对象、用于定义股票购买方式的代理对象、股票代码以及要购买的股数。如果昨天的价格低于两天前的价格,则指示代理购买指定的股数。

function trader(dataService,broker,ticker,numShares)
    yesterday = datetime('yesterday');
    priceYesterday = dataService.lookupPrice(ticker,yesterday);
    price2DaysAgo = dataService.lookupPrice(ticker,yesterday-days(1));
    
    if priceYesterday < price2DaysAgo
        broker.buy(ticker,numShares);
    end
end

Mock 对象和行为对象

mock 对象是超类所指定接口的抽象方法和属性的实现。您也可以在没有超类的情况下构造 mock,在这种情况下,mock 具有一个隐式接口。受测组件与 mock 对象交互,例如,通过调用 mock 对象方法或访问 mock 对象属性。mock 对象执行预定义的操作来响应这些交互。

当您创建 mock 时,还会创建一个关联的行为对象。行为对象与 mock 对象定义的方法相同,用于控制 mock 行为。使用行为对象可定义 mock 操作并验证交互。例如,使用它来定义模拟方法返回的值或验证是否已访问某个属性。

在命令提示符下,创建一个 mock 测试用例以供交互使用。此示例后面将介绍在测试类中而非在命令提示符下使用 mock。

import matlab.mock.TestCase
testCase = TestCase.forInteractiveUse;

创建 Stub 以定义行为

为数据服务依赖项创建一个 mock 并检验其方法。数据服务 mock 会返回预定义的值,并替换用于提供实际股票价格的服务的实现。因此,它展示了上桩行为。

[stubDataService,dataServiceBehavior] = createMock(testCase,?DataService);
methods(stubDataService)
Methods for class matlab.mock.classes.DataServiceMock:



Static methods:

lookupPrice  

DataService 类中,lookupPrice 方法是抽象和静态的。模拟框架会将该方法作为具体和静态的方法进行实现。

定义数据服务 mock 的行为。对于股票代码 "FOO",它会将昨天的价格返回为 $123,而昨天之前的任何价格都为 $234。因此,根据 trader 函数的要求,代理将始终购买股票 "FOO"。对于股票代码 "BAR",它会将昨天的价格返回为 $765,而昨天之前的任何价格都为 $543。因此,代理将从不会购买股票 "BAR"

import matlab.unittest.constraints.IsLessThan
yesterday = datetime('yesterday');

testCase.assignOutputsWhen(dataServiceBehavior.lookupPrice(...
    "FOO",yesterday),123);
testCase.assignOutputsWhen(dataServiceBehavior.lookupPrice(...
    "FOO",IsLessThan(yesterday)),234);

testCase.assignOutputsWhen(dataServiceBehavior.lookupPrice(...
    "BAR",yesterday),765);
testCase.assignOutputsWhen(dataServiceBehavior.lookupPrice(...
    "BAR",IsLessThan(yesterday)),543);

您现在可以调用所模拟的 lookupPrice 方法。

p1 = stubDataService.lookupPrice("FOO",yesterday)
p2 = stubDataService.lookupPrice("BAR",yesterday-days(5))
p1 =

   123


p2 =

   543

虽然 testCaseassignOutputsWhen 方法便于指定行为,但如果您使用 AssignOutputs 操作,则有更多功能可供使用。有关详细信息,请参阅指定 Mock 对象行为

创建 Spy 以拦截消息

为代理依赖项创建一个 mock 并检验其方法。由于代理 mock 用于验证与受测组件(trader 函数)的交互,因此它可展示出 spying 行为。代理 mock 具有一个隐式接口。虽然 buy 方法当前未实现,但您可以创建一个具有该方法的 mock。

[spyBroker,brokerBehavior] = createMock(testCase,'AddedMethods',{'buy'});
methods(spyBroker)
Methods for class matlab.mock.classes.Mock:

buy  

调用该 mock 的 buy 方法。默认情况下,它会返回空值。

s1 = spyBroker.buy
s2 = spyBroker.buy("inputs",[13 42])
s1 =

     []


s2 =

     []

由于 trader 函数不会使用状态返回代码,因此返回空值这一默认 mock 行为是可接受的。代理 mock 是一个纯粹的 spy,不需要实现任何上桩行为。

调用受测组件

调用 trader 函数。除了股票代码和要购买的股数之外,trader 函数还接受数据服务和代理作为输入。传入 spyBrokerstubDataService mock,而不是传入实际的数据服务和代理对象。

trader(stubDataService,spyBroker,"FOO",100)
trader(stubDataService,spyBroker,"FOO",75)
trader(stubDataService,spyBroker,"BAR",100)

验证函数交互

使用代理行为对象 (spy) 来验证 trader 函数是否按预期调用 buy 方法。

使用 TestCase.verifyCalled 方法来验证 trader 函数是否指示 buy 方法购买 100 股 FOO 股票。

import matlab.mock.constraints.WasCalled;
testCase.verifyCalled(brokerBehavior.buy("FOO",100))
Verification passed.

验证 FOO 股票是否被购买了两次,而不管指定的股数如何。虽然 verifyCalled 方法便于指定行为,但如果您使用 WasCalled 约束,则有更多功能可供使用。例如,您可以验证模拟方法是否被调用了指定的次数。

import matlab.unittest.constraints.IsAnything
testCase.verifyThat(brokerBehavior.buy("FOO",IsAnything), ...
    WasCalled('WithCount',2))
Verification passed.

验证是否未调用 buy 方法来请求购买 100 股 BAR 股票。

testCase.verifyNotCalled(brokerBehavior.buy("BAR",100))
Verification passed.

尽管调用了 trader 函数来请求购买 100 股 BAR 股票,但该桩件为 BAR 定义了昨天的价格,以返回一个高于昨天之前的所有日期的值。因此,代理将从不会购买股票 "BAR"

trader 函数的测试类

交互测试用例便于在命令提示符下进行试验。但是,通常会在测试类中创建和使用 mock。在您的当前工作文件夹下的文件中,创建以下包含此示例中的交互测试的测试类。

classdef TraderTest < matlab.mock.TestCase
    methods(Test)
        function buysStockWhenDrops(testCase)
            import matlab.unittest.constraints.IsLessThan
            import matlab.unittest.constraints.IsAnything
            import matlab.mock.constraints.WasCalled
            yesterday = datetime('yesterday');
            
            % Create mocks
            [stubDataService,dataServiceBehavior] = createMock(testCase,...
                ?DataService);
            [spyBroker,brokerBehavior] = createMock(testCase,...
                'AddedMethods',{'buy'});
            
            % Set up behavior
            testCase.assignOutputsWhen(dataServiceBehavior.lookupPrice(...
                "FOO",yesterday),123);
            testCase.assignOutputsWhen(dataServiceBehavior.lookupPrice(...
                "FOO",IsLessThan(yesterday)),234);
            
            % Call function under test
            trader(stubDataService,spyBroker,"FOO",100)
            trader(stubDataService,spyBroker,"FOO",75)
            
            % Verify interactions
            testCase.verifyCalled(brokerBehavior.buy("FOO",100))
            testCase.verifyThat(brokerBehavior.buy("FOO",IsAnything),...
                WasCalled('WithCount',2))
        end
        function doesNotBuyStockWhenIncreases(testCase)
            import matlab.unittest.constraints.IsLessThan
            yesterday = datetime('yesterday');
            
            % Create mocks
            [stubDataService,dataServiceBehavior] = createMock(testCase,...
                ?DataService);
            [spyBroker,brokerBehavior] = createMock(testCase, ...
                'AddedMethods',{'buy'});
            
            % Set up behavior
            testCase.assignOutputsWhen(dataServiceBehavior.lookupPrice(...
                "BAR",yesterday),765);
            testCase.assignOutputsWhen(dataServiceBehavior.lookupPrice(...
                "BAR",IsLessThan(yesterday)),543);
            
            % Call function under test
            trader(stubDataService,spyBroker,"BAR",100)
            
            % Verify interactions
            testCase.verifyNotCalled(brokerBehavior.buy("BAR",100))
        end
    end
end

运行测试并查看结果表。

results = runtests('TraderTest');
table(results)
Running TraderTest
..
Done TraderTest
__________


ans =

  2×6 table

                      Name                       Passed    Failed    Incomplete    Duration      Details   
    _________________________________________    ______    ______    __________    ________    ____________

    'TraderTest/buysStockWhenDrops'              true      false       false        0.24223    [1×1 struct]
    'TraderTest/doesNotBuyStockWhenIncreases'    true      false       false       0.073614    [1×1 struct]