PHPUnitManual:10.1
- 10.1 Stubs
실제 오브젝트를 대체하여, 설정한 값을 (옵션으로) 반환하는 테스트 더블을 stub 라고 합니다. stub 를 사용하면, "SUT 가 의존하는 실제 컴포넌트를 대체하여, SUT 의 입력을 간접적으로 제어 가능해집니다. 이를 사용하여 SUT 가 다른 어떤 것도 실행하지 않도록 강제할 수 있습니다."
예10.2 "메소드에 고정값을 반환하는 stub" 는 stub 메소드의 생성과 반환값의 설정 방법의 예입니다. 먼저, PHPUnit_Framework_TestCase 의 getMock() 메소드를 사용하여 SomeClass 오브젝트의 stub 를 생성합니다 (예10.1 "stub 로 만들 클래스"). 다음으로, PHPUnit 이 제공하는 Fluent Interface 를 사용하여 stub 의 동작을 지정합니다. 즉, 여러 개의 일회용 오브젝트를 생성하여 연결하는 등의 동작은 필요하지 않습니다. 대신에 예에 나온 것처럼 메소드의 호출을 연결합니다. 이쪽이 보다 읽기 편한 "흐르는 듯한(Fluent)" 코드가 됩니다.
예10.1 stub 으로 만들 클래스
<?php
class SomeClass
{
public function doSomething()
{
// Do something.
}
}
?>
예10.2 메소드에 고정값을 반환하는 stub
<?php
require_once 'SomeClass.php';
class StubTest extends PHPUnit_Framework_TestCase
{
public function testStub()
{
// Create a stub for the SomeClass class.
$stub = $this->getMock('SomeClass');
// Configure the stub.
$stub->expects($this->any())
->method('doSomething')
->will($this->returnValue('foo'));
// Calling $stub->doSomething() will now return
// 'foo'.
$this->assertEquals('foo', $stub->doSomething());
}
}
?>
PHPUnit 은 getMock() 메소드가 사용되었을 때 자동적으로 백그라운드에서, 원하는 대로 동작하는 새로운 PHP 클래스를 생성합니다. 생성된 테스트 더블 클래스의 설정은 getMock() 메소드의 옵션 인수를 통해 이루어집니다.
- 기본설정에서는, 지정 클래스의 모든 메소드가 단순히 NULL 을 반환하는 테스트 더블로 됩니다. 반환값을 변경하기 위한 방법 중의 한 예로 will($this->returnValue()) 를 사용할 수 있습니다.
- 옵션으로 2번째 인수를 지정하면, 해당 배열 속에 포함되는 이름의 메소드만이 테스트 더블로 대체되고, 나머지 메소드들은 그대로 사용됩니다. 인수에 NULL 이 사용된 경우, 대체되는 메소드는 없습니다.
- 옵션으로 3번째 인수를 지정하여, 대상 클래스의 constructor 에 넘기는 인수의 배열을 넘길 수 있습니다 (기본 설정에서는 constructor 는 더미 구현으로 대체되지 않습니다).
- 옵션의 4번째 인수를 지정하여, 생성되는 테스트 더블 클래스의 클래스 이름을 지정할 수 있습니다.
- 옵션의 5번째 인수를 지정하여, 대상 클래스의 constructor 를 호출하지 않도록 할 수 있습니다.
- 옵션의 6번째 인수를 지정하여, 대상 클래스의 clone constructor 를 호출하지 않도록 할 수 있습니다.
- 옵션의 7번째 인수를 지정하여, 테스트 더블 클래스를 작성할 때, __autoload() 를 무효화할 수 있습니다.
생성된 테스트 더블 클래스의 설정을 mock builder API 를 통해 지정할 수도 있습니다. 예10.3 "mock builder API 를 사용하여 생성된 테스트 더블 클래스를 변경" 에 예가 나와 있습니다. mock builder 에서 사용 가능한 메소드는 다음과 같습니다.
- setMethods(array $methods) 를 mock builder 오브젝트에서 호출하여 테스트 더블로 대체할 메소드를 지정할 수 있습니다. 그 외의 메소드들의 동작은 변하지 않습니다. setMethods(NULL) 은 어떤 메소드도 대체하지 않습니다.
- setConstructorArgs(array $args) 를 호출하여 배열을 넘기면, 이 배열을 대상 클래스의 constructor 에 넘길 수 있습니다 (기본 설정의 더미 구현에서는 constructor 는 대체되지 않습니다).
- setMockClassName($name) 는 생성되는 테스트 더블 클래스의 클래스 이름을 지정할 수 있습니다.
- disableOriginalConstructor() 는 대상 클래스의 constructor 를 무효화할 수 있습니다.
- disableOriginalClone() 는 대상 클래스의 clone constructor 를 무효화할 수 있습니다.
- disableAutoload() 는 테스트 더블 클래스의 생성 시에 __autoload() 를 무효화할 수 있습니다.
예10.3 mock builder API 를 사용하여 생성된 테스트 더블 클래스를 변경
<?php
require_once 'SomeClass.php';
class StubTest extends PHPUnit_Framework_TestCase
{
public function testStub()
{
// Create a stub for the SomeClass class.
$stub = $this->getMockBuilder('SomeClass')
->disableOriginalConstructor()
->getMock();
// Configure the stub.
$stub->expects($this->any())
->method('doSomething')
->will($this->returnValue('foo'));
// Calling $stub->doSomething() will now return
// 'foo'.
$this->assertEquals('foo', $stub->doSomething());
}
}
?>
때로는 메소드를 호출할 때, 인수 중 하나를 (그대로) stub 메소드 호출의 반환값으로 하고 있은 경우가 있습니다. 예10.4 "메소드에 인수 중 하나를 반환시키는 stub" 는 returnArgument() 를 returnValue() 대신으로 사용하여 이를 구현한 예입니다.
예10.4 메소드에 인수 중 하나를 반환시키는 stub
<?php
require_once 'SomeClass.php';
class StubTest extends PHPUnit_Framework_TestCase
{
public function testReturnArgumentStub()
{
// Create a stub for the SomeClass class.
$stub = $this->getMock('SomeClass');
// Configure the stub.
$stub->expects($this->any())
->method('doSomething')
->will($this->returnArgument(0));
// $stub->doSomething('foo') returns 'foo'
$this->assertEquals('foo', $stub->doSomething('foo'));
// $stub->doSomething('bar') returns 'bar'
$this->assertEquals('bar', $stub->doSomething('bar'));
}
}
?>
흐르는 듯한 인터페이스를 테스트할 경우, stub 메소드가 오브젝트 자신에의 참조 (reference) 를 반환하는 것이 유용합니다. 예10.5 "stub 오브젝트에의 참조를 반환하는 메소드의 stub" 가 그 예입니다.
예10.5: 참조를 stub 객체로 반환하는 메서드 호출 떼어보기
<?php
require_once 'SomeClass.php';
class StubTest extends PHPUnit_Framework_TestCase
{
public function testReturnSelf()
{
// Create a stub for the SomeClass class.
$stub = $this->getMock('SomeClass');
// Configure the stub.
$stub->expects($this->any())
->method('doSomething')
->will($this->returnSelf());
// $stub->doSomething() returns $stub
$this->assertSame($stub, $stub->doSomething());
}
}
?>
stub 메소드의 호출 결과로 정의된 인수 리스트에 맞게 다른 값을 반환해야만 할 때도 있습니다. returnValueMap() 를 사용하여 map 를 만들어 인수와 관련맺고, 반환값에 대응시킬 수 있습니다. 예10.6 "메소드에 map 의 값을 반환시키는 stub" 가 그 예입니다.
예10.6 메소드에 map 의 값을 반환시키는 stub
<?php
require_once 'SomeClass.php';
class StubTest extends PHPUnit_Framework_TestCase
{
public function testReturnValueMapStub()
{
// Create a stub for the SomeClass class.
$stub = $this->getMock('SomeClass');
// Create a map of arguments to return values.
$map = array(
array('a', 'b', 'c', 'd'),
array('e', 'f', 'g', 'h')
);
// Configure the stub.
$stub->expects($this->any())
->method('doSomething')
->will($this->returnValueMap($map));
// $stub->doSomething() returns different values depending on
// the provided arguments.
$this->assertEquals('d', $stub->doSomething('a', 'b', 'c'));
$this->assertEquals('h', $stub->doSomething('e', 'f', 'g'));
}
}
?>
stub 메소드의 호출 결과로, 고정값 (returnValue() 참조) 이나 (불변의) 인수 (returnArgument() 참조) 가 아니라 계산한 결과값을 반환시키고자 하는 경우, returnCallback() 를 사용할 수 있습니다. 이 메소드는 stub 메소드로부터 callback 함수나 메소드의 결과를 반환시킵니다. 예10.7 "메소드에 callback 의 값을 반환시키는 stub" 를 참조하세요.
예10.7 메소드에 callback 의 값을 반환시키는 stub
<?php
require_once 'SomeClass.php';
class StubTest extends PHPUnit_Framework_TestCase
{
public function testReturnCallbackStub()
{
// Create a stub for the SomeClass class.
$stub = $this->getMock('SomeClass');
// Configure the stub.
$stub->expects($this->any())
->method('doSomething')
->will($this->returnCallback('str_rot13'));
// $stub->doSomething($argument) returns str_rot13($argument)
$this->assertEquals('fbzrguvat', $stub->doSomething('something'));
}
}
?>
보다 단순한 방법으로, 원하는 반환값의 리스트를 지정할 수도 있습니다. 이 경우에는 onConsecutiveCalls() 메소드를 사용합니다. 예10.8 "리스트로 지정한 값을 순서대로 method 에서 반환시키는 stub" 가 예입니다.
예10.8 리스트로 지정한 값을 순서대로 method 에서 반환시키는 stub
<?php
require_once 'SomeClass.php';
class StubTest extends PHPUnit_Framework_TestCase
{
public function testOnConsecutiveCallsStub()
{
// Create a stub for the SomeClass class.
$stub = $this->getMock('SomeClass');
// Configure the stub.
$stub->expects($this->any())
->method('doSomething')
->will($this->onConsecutiveCalls(2, 3, 5, 7));
// $stub->doSomething() returns a different value each time
$this->assertEquals(2, $stub->doSomething());
$this->assertEquals(3, $stub->doSomething());
$this->assertEquals(5, $stub->doSomething());
}
}
?>
stub 메소드에서, 값을 반환하는 대신에 예외를 발생시킬 수도 있습니다. 예10.9 "메소드에 예외를 throw 시키는 stub" 는 throwException() 를 사용하여 예외를 발생시키는 예입니다.
예10.9 메소드에 예외를 throw 시키는 stub
<?php
require_once 'SomeClass.php';
class StubTest extends PHPUnit_Framework_TestCase
{
public function testThrowExceptionStub()
{
// Create a stub for the SomeClass class.
$stub = $this->getMock('SomeClass');
// Configure the stub.
$stub->expects($this->any())
->method('doSomething')
->will($this->throwException(new Exception));
// $stub->doSomething() throws Exception
$stub->doSomething();
}
}
?>
stub 를 사용하여 설계를 보다 좋게 개선시킬 수도 있습니다. 다양한 곳에서 사용되는 리소스를 단일 창구 (façade) 를 경유하도록 하여, 간단히 stub 로 대체시킬 수도 있습니다. 예를 들어, 데이터베이스 억세스 코드를 곳곳에 구현하는 것이 아니라, IDatabase 인터페이스를 구현한 단일 Database 오브젝트를 사용하도록 합니다. 결과적으로, IDatabase 를 구현한 stub 를 작성하여 테스트에 사용 가능해집니다. 동시에 테스트 시에 stub 데이터베이스를 사용할 것인가 아니면 진짜 데이터베이스를 사용할 것인가를 선택할 수 있습니다. 즉, 개발 시점에서는 로컬 환경에서 테스트하고, 종합 테스트 시점에서 진짜 데이터베이스를 사용하여 테스트하는 것도 가능합니다.
stub 로 만들어야만 하는 기능들은, 대부분 동일 오브젝트 안에서 밀접하게 결합되어 있습니다. 이 기능을 하나로 결합된 인터페이스로 묶는 것을 통해, 시스템의 다른 부분과의 결합을 약화시킬 수 있습니다.