Main Content

モック オブジェクトの作成

多くの場合、ユニット テストを実行するときに関心の対象となるのは、完全なシステムの一部を、依存するコンポーネントと切り離してテストすることです。システムの一部をテストするために、依存されるコンポーネントをモック オブジェクトに置き換えることができます。モック オブジェクトは、最低でも製品オブジェクトと同じインターフェイスの一部を実装しますが、多くの場合は、単純で、効率性、予測性、制御性が高い方法になります。モッキング フレームワークを使用する場合、テスト対象のコンポーネントは、共同作業の相手が "実際の" オブジェクトかモック オブジェクトかを認識しません。

Test a component using mocked-up dependencies.

たとえば、株式購入のためのアルゴリズムをテストする場合に、システム全体のテストはしないとします。株価を検索する機能を置き換えるモック オブジェクトと、トレーダーが株式を購入したことを確認する別のモック オブジェクトを使用できます。テスト対象のアルゴリズムは、モック オブジェクトで処理を行っていることを認識していないため、残りのシステムとは切り離してアルゴリズムをテストできます。

モック オブジェクトを使用して、動作を定義できます ("スタブ" と呼ばれる処理)。たとえば、あるオブジェクトがクエリに対して事前定義された応答を生成することを指定できます。また、テスト対象のコンポーネントからモック オブジェクトへ送信されたメッセージを傍受して記憶することもできます ("スパイ" と呼ばれる処理)。たとえば、特定のメソッドが呼び出されたことや、プロパティが設定されたことを確認できます。

コンポーネントを単独でテストする一般的なワークフローは次のとおりです。

  1. 依存されるコンポーネントのモックを作成します。

  2. モックの動作を定義します。たとえば、特定の入力セットで呼び出されたときにモックされたメソッドまたはプロパティが返す出力を定義します。

  3. 対象のコンポーネントをテストします。

  4. 対象のコンポーネントとモックされたコンポーネントの間の相互作用を検定します。たとえば、モックされたメソッドが特定の入力で呼び出されたことや、プロパティが設定されたことを検証します。

依存されるコンポーネント

この例では、テスト対象のコンポーネントは単純なデイトレードのアルゴリズムです。これは、他のコンポーネントから独立してテストするシステムの一部です。このデイトレードのアルゴリズムには 2 つの依存関係があります。株価データを取得するデータ サービスと、株式を購入するブローカーです。

現在の作業フォルダー内の DataService.m ファイルで、lookupPrice メソッドを含む抽象クラスを作成します。

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

量産コードでは、DataService クラスの具象実装 (BloombergDataService クラスなど) がいくつか考えられます。このクラスは Datafeed Toolbox™ を使用します。ただし、ここでは DataService クラスのモックを作成するため、ツールボックスをインストールしてトレード アルゴリズムのテストを実行する必要はありません。

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 メソッドをもち、これによって株式銘柄コードと指定した数の購入株式を受け入れ、ステータス コードを返します。ブローカー コンポーネントのモックは暗黙的なインターフェイスを使用し、スーパークラスからは派生しません。

テスト対象のコンポーネント

現在の作業フォルダーの trader.m ファイルで、単純なデイトレードのアルゴリズムを作成します。関数 trader は、入力としてデータ サービス オブジェクトを受け取り、それによって株価、株式の購入方法を定義するブローカー オブジェクト、株式銘柄コード、購入する株式の数を検索します。昨日の価格が 2 日前の価格より低い場合は、指定した数の株式を購入するようブローカーに指示します。

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

モック オブジェクトと behavior オブジェクト

モック オブジェクトは、スーパークラスによって指定されているインターフェイスの抽象メソッドおよびプロパティの実装です。モックはスーパークラスなしでも構築できます。その場合、モックは暗黙的なインターフェイスをもちます。テスト対象のコンポーネントは、モック オブジェクト メソッドを呼び出すか、モック オブジェクト プロパティにアクセスすることにより、モック オブジェクトと相互作用します。これらの相互作用に応答して、モック オブジェクトは事前定義されたアクションを実行します。

モックを作成するときは、関連する behavior オブジェクトも作成します。behavior オブジェクトは、モック オブジェクトと同じメソッドを定義し、モックの動作を制御します。behavior オブジェクトを使用してモックのアクションを定義し、相互作用を検定します。たとえば、モックされたメソッドが返す値を定義したり、プロパティにアクセスがあったことを確認するために使用します。

コマンド プロンプトで、対話型で使用する場合のモック テスト ケースを作成します。コマンド プロンプトではなくテスト クラスでモックを使用する方法については、この例で後述します。

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

動作を定義するスタブの作成

データ サービス依存関係のモックを作成し、メソッドを調べます。データ サービス モックは事前定義された値を返し、実際の株価を提供するサービスの実装を置き換えます。したがって、これはスタブ動作を示します。

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



Static methods:

lookupPrice  

DataService クラスで、lookupPrice メソッドは抽象的かつ静的です。モッキング フレームワークはこのメソッドを具象かつ静的として実装します。

データ サービス モックの動作を定義します。株式銘柄コード "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 アクションを使用すると、さらに多くの機能を使用できます。詳細については、モック オブジェクトの動作の指定を参照してください。

メッセージを傍受するスパイの作成

ブローカー依存関係のモックを作成し、メソッドを調べます。ブローカー モックは、テスト対象のコンポーネント (関数 trader) との相互作用の検証に使用するため、スパイ動作を示します。ブローカー モックは暗黙的なインターフェイスをもちます。buy メソッドは現在実装されていませんが、これを含めてモックを作成できます。

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

buy  

モックの buy メソッドを呼び出します。既定では、空が返されます。

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

     []


s2 =

     []

関数 trader はステータスの戻りコードを使用しないため、空を返すというモックの既定動作は許容されます。ブローカー モックは純粋なスパイであり、スタブ動作を実装する必要はありません。

テスト対象のコンポーネントの呼び出し

関数 trader を呼び出します。株式銘柄コードと購入する株式の数に加えて、関数 trader はデータ サービスとブローカーを入力として受け取ります。実際のデータ サービス オブジェクトとブローカー オブジェクトを渡す代わりに、モック spyBroker および stubDataService を渡します。

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

関数の相互作用の検証

ブローカー behavior オブジェクト (スパイ) を使用して、関数 trader が想定どおりに buy メソッドを呼び出すことを検証します。

TestCase.verifyCalled メソッドを使用して、関数 traderbuy メソッドに対して、FOO 株式を 100 株購入するよう指示したことを検証します。

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

指定した株数に関係なく、FOO 株式が 2 回購入されたことを検証します。verifyCalled メソッドは動作の指定に便利ですが、WasCalled 制約を使用すると、さらに多くの機能を使用できます。たとえば、モックされたメソッドが指定した回数呼び出されたことを検証できます。

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

BAR 株式 100 株を要求する buy メソッドが呼び出されなかったことを検証します。

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

BAR 株式 100 株を要求する関数 trader は呼び出されましたが、スタブにより、BAR の昨日の価格が、それより前のどの日よりも高い値を返すように定義されています。そのため、ブローカーが株式 "BAR" を購入することはありません。

関数 trader のテスト クラス

対話型のテスト ケースは、コマンド プロンプトでの実験に便利です。ただし、テスト クラス内でモックを作成して使用する方法が一般的です。現在の作業フォルダーのファイルで、この例の対話型テストを組み込む次のテスト クラスを作成します。

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]

参考

クラス

関連するトピック