Main Content

모의 객체 생성하기

단위 테스트를 수행할 때, 전체 시스템의 일부를 해당 부분이 의존하는 컴포넌트로부터 격리해 테스트하고자 할 수 있습니다. 시스템의 일부를 테스트하기 위해, 해당 부분이 의존하고 있는 객체를 모의 객체로 대체할 수 있습니다. 많은 경우, 모의 객체는 프로덕션 객체의 인터페이스와 적어도 일부가 동일한 인터페이스를 간단하고 효율적이며 예측 및 제어가 가능한 방식으로 구현합니다. 모의 프레임워크를 사용할 때, 테스트 중인 컴포넌트는 그와 협력하는 객체가 "실제" 객체인지 모의 객체인지 인식하지 못합니다.

Test a component using mocked-up dependencies.

예를 들어, 전체 시스템을 테스트하는 것이 아니라 주식 매입 알고리즘만 테스트하려는 경우를 생각해 보겠습니다. 주가 조회 기능을 대체할 모의 객체와 거래자의 주식 매입을 검증하는 또 다른 모의 객체를 사용할 수 있습니다. 테스트 중인 알고리즘은 모의 객체에 대해 작동 중임을 알지 못하므로 시스템의 나머지 부분으로부터 격리해 알고리즘을 테스트할 수 있습니다.

모의 객체를 사용하여 동작을 정의할 수 있습니다(소위 스터빙(Stubbing)으로 불리는 과정임). 예를 들어, 객체가 쿼리에 대해 미리 정의된 응답을 생성하도록 지정할 수 있습니다. 테스트 중인 컴포넌트에서 모의 객체로 보내는 메시지를 가로채고 기억할 수도 있습니다(소위 감시(Spying)로 불리는 과정임). 예를 들어, 특정 메서드가 호출되거나 속성이 설정되었음을 검증할 수 있습니다.

컴포넌트를 격리해 테스트하는 일반적인 워크플로는 다음과 같습니다.

  1. 상위 컴포넌트의 모의 객체를 만듭니다.

  2. 모의 객체의 동작을 정의합니다. 예를 들어 모의 메서드나 속성이 일련의 특정 입력값과 함께 호출될 때 이들이 반환할 출력값을 정의합니다.

  3. 관심 있는 컴포넌트를 테스트합니다.

  4. 관심 있는 컴포넌트와 모의 컴포넌트 사이의 상호 작용을 가설 검정합니다. 예를 들어, 모의 메서드가 특정 입력값과 함께 호출되었는지, 또는 속성이 설정되었는지 검증합니다.

상위 컴포넌트

이 예제에서 테스트 중인 컴포넌트는 간단한 당일 매매 알고리즘입니다. 이는 시스템의 일부분으로서, 다른 컴포넌트와는 독립적으로 테스트하려는 부분입니다. 당일 매매 알고리즘에는 주가 데이터를 검색하는 데이터 서비스와 주식을 매입하는 브로커라는 두 가지 종속성이 존재합니다.

현재 작업 폴더에 있는 파일 DataService.mlookupPrice 메서드를 포함하는 추상 클래스를 만듭니다.

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

프로덕션 코드에는 BloombergDataService 클래스와 같이 DataService 클래스의 여러 가지 구체적인 구현이 있을 수 있습니다. 이 클래스는 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 함수는 주가를 조회하는 데이터 서비스 객체, 주식 매입 방식을 정의하는 브로커 객체, 주식 종목 코드, 매입할 주식 수를 입력값으로 받습니다. 전일 주가가 이틀 전 주가보다 낮을 경우 브로커가 지정된 개수의 주식을 매입하도록 지시합니다.

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

모의 객체와 동작 객체

모의 객체는 슈퍼클래스로 지정된 인터페이스의 추상 메서드와 속성을 구현한 것입니다. 슈퍼클래스 없이 모의 객체를 생성할 수도 있는데, 이 경우 모의 객체에 묵시적 인터페이스가 있습니다. 테스트 중인 컴포넌트는 예를 들어 모의 객체 메서드를 호출하거나 모의 객체 속성에 액세스하여 모의 객체와 상호 작용합니다. 모의 객체는 이런 상호 작용에 대한 응답으로 미리 정의된 작업을 수행합니다.

모의 객체를 생성할 때, 연결된 동작 객체도 생성합니다. 동작 객체는 모의 객체와 동일한 메서드를 정의하고 모의 동작을 제어합니다. 동작 객체를 사용하여 모의 동작을 정의하고 상호 작용을 가설 검정합니다. 예를 들어, 동작 객체를 사용하여 모의 메서드가 반환하는 값을 정의하거나 속성에 액세스했는지 검증합니다.

명령 프롬프트에서 대화형 방식으로 사용할 모의 테스트 케이스를 생성합니다. 명령 프롬프트 대신 테스트 클래스에서 모의 객체를 사용하는 방법은 이 예제의 뒷부분에 나와 있습니다.

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

스텁을 생성하여 동작 정의하기

데이터 서비스 종속성에 대한 모의 객체를 만들고 그 메서드를 검토합니다. 데이터 서비스 모의 객체는 미리 정의된 값을 반환하여 실제 주가를 제공하는 서비스의 구현을 대체합니다. 따라서, 이 모의 객체는 스터빙(Stubbing) 동작을 수행합니다.

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



Static methods:

lookupPrice  

DataService 클래스에서 lookupPrice 메서드는 추상 메서드이자 정적 메서드입니다. 모의 프레임워크는 이 메서드를 구체 메서드이자 정적 메서드로 구현합니다.

데이터 서비스 모의 객체의 동작을 정의합니다. 주식 종목 코드 "FOO"의 경우 이 모의 객체는 전일 주가를 $123로 반환하고, 2일 이전의 주가는 모두 $234로 정합니다. 따라서 브로커는 trader 함수에 따라 항상 "FOO" 주식을 매입합니다. 주식 종목 코드 "BAR"의 경우 이 모의 객체는 전일 주가를 $765로 반환하고, 2일 이전의 주가는 $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

testCase에서 assignOutputsWhen 메서드를 사용하면 동작을 간편하게 지정할 수 있지만, AssignOutputs 동작을 사용할 경우 더 많은 기능이 제공됩니다. 자세한 내용은 모의 객체 동작 지정하기 항목을 참조하십시오.

스파이(Spy)를 생성하여 메시지 가로채기

브로커 종속성에 대한 모의 객체를 만들고 해당 모의 객체에 대한 메서드를 검토합니다. 브로커 모의 객체는 테스트 중인 컴포넌트(trader 함수)와의 상호 작용을 검증하는 데 사용되므로 감시(Spying) 동작을 수행합니다. 브로커 모의 객체에는 묵시적 인터페이스가 있습니다. 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 함수는 상태 반환 코드를 사용하지 않으므로 빈 값을 반환하는 디폴트 모의 객체 동작은 허용 가능합니다. 브로커 모의 객체는 순수 스파이로, 스터빙(Stubbing) 동작을 구현할 필요가 없습니다.

테스트 대상 컴포넌트 호출하기

trader 함수를 호출합니다. 주식 종목 코드와 매입할 주식 수 외에, trader 함수는 데이터 서비스와 브로커를 입력값으로 받습니다. 실제 데이터 서비스 객체와 브로커 객체를 전달하는 대신, spyBroker 모의 객체와 stubDataService 모의 객체를 전달합니다.

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

함수 상호 작용 검증하기

브로커 동작 객체(스파이)를 사용하여 trader 함수가 예상대로 buy 메서드를 호출하는지 검증합니다.

TestCase.verifyCalled 메서드를 사용하여 trader 함수가 buy 메서드에 FOO 주식 100주를 매입하도록 지시했는지 검증합니다.

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.

BAR 주식 100주를 요청하는 buy 메서드가 호출되지 않았음을 검증합니다.

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

BAR 주식 100주를 요청하는 trader 함수가 호출되었지만 스텁이 BAR에 대해 2일 전보다 높은 값을 반환하도록 전일 주가를 정의했습니다. 따라서 브로커는 "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]

참고 항목

클래스

관련 항목