2009-03-03 8 views
9

C#, nUnit 및 Rhino Mocks가 적용 가능할 경우.TDD 및 DI : 종속성 주입이 번거로워 짐

TDD에서의 내 탐구는 복잡한 기능을 중심으로 테스트를 마무리하려고 시도하면서 계속됩니다. 저장 될 때 양식 내에서 종속 개체를 저장해야하는 양식을 코딩한다고 가정 해 봅시다 ... 질문, 첨부 파일 (가능한 경우) 및 "로그"항목 (예 : "blahblah가 양식을 업데이트했습니다."또는 "blahblah가 파일을 첨부했습니다."). 이 저장 기능은 저장 기능 중에 양식의 상태가 어떻게 바뀌 었는지에 따라 다양한 사람들에게 이메일을 발송합니다.

이것은 폼의 저장 함수를 모든 종속성과 함께 완벽하게 테스트하기 위해 5 개 또는 6 개의 데이터 공급자를 주입하여이 함수를 테스트하고 모든 것이 올바른 방법으로 정렬되도록해야한다는 것을 의미합니다. 조롱 된 공급자를 삽입하기 위해 양식 객체에 대해 여러 체인으로 연결된 생성자를 작성할 때이 작업은 번거로울 수 있습니다. 리팩토링 방식이나 조롱 한 데이터 제공 업체를 설정하는 더 좋은 방법 중 하나에서 뭔가를 놓치고 있다고 생각합니다.

리팩토링 방법을 자세히 검토하여이 기능을 단순화하는 방법을 알아야합니까? 관찰자 패턴 사운드는 어떻습니까? 그러면 부모 객체가 저장되고 처리 될 때 종속 객체가 감지합니다. 나는 사람들이 함수를 분리하여 함수를 분리 할 수 ​​있다고 말한 것을 알고 있습니다. 즉, 각 종속 객체의 개별 저장 함수는 테스트하지만, 폼 자체의 저장 함수는 테스트하지 않습니다. 처음?

+0

그것은 개선 제안에 도움이 될 것이다. –

답변

7

자동 기록 컨테이너를 사용하십시오. RhinoMocks 용으로 작성된 것이 있습니다.

생성자 삽입을 통해 많은 의존성이있는 클래스가 있다고 상상해보십시오.여기에 지금

private MockRepository _mocks; 
private BroadcastListViewPresenter _presenter; 
private IBroadcastListView _view; 
private IAddNewBroadcastEventBroker _addNewBroadcastEventBroker; 
private IBroadcastService _broadcastService; 
private IChannelService _channelService; 
private IDeviceService _deviceService; 
private IDialogFactory _dialogFactory; 
private IMessageBoxService _messageBoxService; 
private ITouchScreenService _touchScreenService; 
private IDeviceBroadcastFactory _deviceBroadcastFactory; 
private IFileBroadcastFactory _fileBroadcastFactory; 
private IBroadcastServiceCallback _broadcastServiceCallback; 
private IChannelServiceCallback _channelServiceCallback; 

[SetUp] 
public void SetUp() 
{ 
    _mocks = new MockRepository(); 
    _view = _mocks.DynamicMock<IBroadcastListView>(); 

    _addNewBroadcastEventBroker = _mocks.DynamicMock<IAddNewBroadcastEventBroker>(); 

    _broadcastService = _mocks.DynamicMock<IBroadcastService>(); 
    _channelService = _mocks.DynamicMock<IChannelService>(); 
    _deviceService = _mocks.DynamicMock<IDeviceService>(); 
    _dialogFactory = _mocks.DynamicMock<IDialogFactory>(); 
    _messageBoxService = _mocks.DynamicMock<IMessageBoxService>(); 
    _touchScreenService = _mocks.DynamicMock<ITouchScreenService>(); 
    _deviceBroadcastFactory = _mocks.DynamicMock<IDeviceBroadcastFactory>(); 
    _fileBroadcastFactory = _mocks.DynamicMock<IFileBroadcastFactory>(); 
    _broadcastServiceCallback = _mocks.DynamicMock<IBroadcastServiceCallback>(); 
    _channelServiceCallback = _mocks.DynamicMock<IChannelServiceCallback>(); 


    _presenter = new BroadcastListViewPresenter(
     _addNewBroadcastEventBroker, 
     _broadcastService, 
     _channelService, 
     _deviceService, 
     _dialogFactory, 
     _messageBoxService, 
     _touchScreenService, 
     _deviceBroadcastFactory, 
     _fileBroadcastFactory, 
     _broadcastServiceCallback, 
     _channelServiceCallback); 

    _presenter.View = _view; 
} 

을 AutoMocking 컨테이너와 같은 일이 : : 여기 RhinoMocks, 아니 AutoMocking 컨테이너로를 설정하는 등의 모습입니다

private MockRepository _mocks; 
private AutoMockingContainer _container; 
private BroadcastListViewPresenter _presenter; 
private IBroadcastListView _view; 

[SetUp] 
public void SetUp() 
{ 

    _mocks = new MockRepository(); 
    _container = new AutoMockingContainer(_mocks); 
    _container.Initialize(); 

    _view = _mocks.DynamicMock<IBroadcastListView>(); 
    _presenter = _container.Create<BroadcastListViewPresenter>(); 
    _presenter.View = _view; 

} 

쉽게, 그래?

AutoMocking 컨테이너는 자동으로 생성자의 모든 의존성에 대한 모의 객체를 생성하고, 당신과 같이 테스트에 액세스 할 수 있습니다

using (_mocks.Record()) 
    { 
     _container.Get<IChannelService>().Expect(cs => cs.ChannelIsBroadcasting(channel)).Return(false); 
     _container.Get<IBroadcastService>().Expect(bs => bs.Start(8)); 
    } 

희망을. 필자의 테스트 수명은 AutoMocking 컨테이너의 출현으로 인해 훨씬 ​​쉬워졌습니다.

+0

이 접근법은 단지 복잡성을 숨기고, 당신을 덜어주지는 않습니다.여기서 루트 문제는 테스트 코드 자체가 아니라 테스트중인 코드에 있습니다. –

+0

이것은 합당한 일입니다. 한 번에 여러 서비스에 대한 기대치를 설정하는 테스트를 조심하십시오. 각 테스트는 일반적으로 한 번에 하나의 서비스에 대해서만 기대를 설정해야합니다. 나머지는 기대하지 않는 스텁을 넣는 도구가있어서 좋네요. –

5

당신이 싫다면 귀찮습니다.

조롱 방법론의 제안자는 코드가 부적절하게 작성되었다고 지적합니다. 즉,이 메서드 내에서 종속 객체를 생성하지 않아야합니다. 오히려 주입 API에는 적절한 객체를 만드는 함수가 있어야합니다.

6 개의 다른 객체를 조롱하는 경우는 사실입니다. 그러나 시스템을 단위 테스트 한 경우 해당 개체에는 이미 사용자가 사용할 수있는 조롱 기반이 있어야합니다.

마지막으로 일부 작업을 수행하는 조롱 프레임 워크를 사용하십시오.

1

DI를 수행하는 유일한 방법은 생성자 DI가 아닙니다. C#을 사용하기 때문에 생성자가 중요한 작업을하지 않으면 Property DI를 사용할 수 있습니다. 이것은 당신의 함수의 복잡성을 희생시키면서 당신의 객체의 생성자 측면에서 크게 단순화합니다. 함수는 종속 속성이 무효인지 확인하고 InvalidOperation이 null 인 경우 작업을 시작하기 전에 throw해야합니다.

+0

이 동의하지 않으면 속성 기반이 단순화되지 않고 단지 복잡성이 숨겨집니다. – eglasius

+0

글쎄, 간단히 말하자면, 한 위치에서 다른 위치로 복잡성을 옮길 수 있다는 뜻입니다. 실제로 테스트 나 시스템 측면에서 단순화 할 수 있으므로 더 작은 부분으로 더 간단한 부분을 처리 할 수 ​​있습니다. – Randolpho

0

무언가를 테스트하기 어려운 경우 일반적으로 코드 품질의 증상이며 코드를 테스트 할 수 없습니다 (this podcast, IIRC 참조). 코드를 쉽게 리팩토링하여 코드를 쉽게 테스트 할 수 있도록하는 것이 좋습니다. 코드를 클래스로 분할하는 방법을 결정하는 몇 가지 방법은 SRP and OCP입니다. 보다 구체적인 지침을 보려면 문제의 코드를 볼 필요가 있습니다.

15

첫 번째로 TDD을 따르는 경우 복잡한 기능을 테스트하지 않습니다. 테스트를 통해 함수를 래핑합니다. 사실, 그건 옳지 않아. 테스트와 기능을 섞어 쓰고 거의 동시에 같은 시간에 테스트하고 함수를 약간 앞서 테스트합니다. The Three Laws of TDD을 참조하십시오.

이 세 가지 법칙을 따르고 리팩토링에 대해 열심히 다룰 때 "복잡한 기능"으로 끝나지 않습니다. 오히려 당신은 많은 테스트되고 간단한 기능으로 끝을 맺습니다.

이제 귀하의 의견을 들어보십시오. 이미 "복잡한 기능"이있는 경우 테스트를 포장하려면 다음을 수행해야합니다.

  1. DI를 통해가 아니라 명시 적으로 mock을 추가하십시오. (예 : '테스트'플래그와 같은 무서운 것, 실제 객체 대신 모의 객체를 선택하는 'if'문).
  2. 구성 요소의 기본 작동을 다루기 위해 몇 가지 테스트를 작성하십시오.
  3. 자발적으로 리팩터링하여 복잡한 기능을 여러 가지 간단한 기능으로 분해하는 동시에 가능한 한 자주 자갈을 긋는 테스트를 실행합니다.
  4. 'test'플래그를 가능한 한 높게 누르십시오. 리팩터링 할 때 데이터 소스를 작은 간단한 함수로 전달하십시오. 'test'플래그가 최상위 함수 외에는 감염되지 않도록하십시오.
  5. 재 작성 테스트. 리팩터링 할 때 가능한 한 많은 테스트를 다시 작성하여 큰 최상위 함수 대신 간단한 작은 함수를 호출하십시오. 테스트를 통해 간단한 기능을 테스트 할 수 있습니다.
  6. '테스트'플래그를 없애고 실제로 필요한 DI의 양을 결정하십시오. 테스트를 통해 하위 레벨에서 작성된 테스트를 통해 모의 객체를 삽입 할 수 있으므로 더 이상 최상위 레벨의 많은 데이터 소스를 조롱 할 필요가 없을 것입니다.

DI가 여전히 성가신 경우 모든 데이터 소스에 대한 참조를 보유하는 단일 개체를 주입하는 것이 좋습니다. 많은 것보다는 하나의 것을 주입하는 것이 항상 쉽습니다.

+0

@ Bob 삼촌. 당신은 내가 무엇을하고 있었는지 정확하게 말했습니다. – vijaysylvester

+1

제발, 아니 하나님 물건. 나는 모든 코드를 모든 의존성에 의존하게함으로써 모듈성을 파괴하는 모든 일종의 의존적 인 객체를 정리하는 데 너무 많은 시간을 보냈다. –

+0

@ Bob Bob, 고마워. 처음 2 문장이 나를 때렸다. –

5

코드가 없지만 첫 번째 반응은 테스트에 개체에 공동 작업자가 너무 많다는 것입니다. 이와 같은 경우에는 항상 거기에 더 높은 수준의 구조로 패키지되어야하는 누락 된 구조가 있음을 발견했습니다. automocking 컨테이너를 사용하는 것은 테스트에서 얻는 피드백을 그냥 놀리는 것입니다. 더 자세한 설명은 http://www.mockobjects.com/2007/04/test-smell-bloated-constructor.html을 참조하십시오.

4

이 문맥에서 나는 보통 "당신의 객체가 너무 많은 의존성을 가지고 있음을 나타냅니다"또는 "당신의 객체가 너무 많은 공동 작업자를 가지고 있음을 나타냅니다"라는 문장을 따라 진술을 발견합니다. 물론 MVC 컨트롤러 나 폼은 그 의무를 수행하기 위해 다양한 서비스와 객체를 호출하게 될 것입니다. 그것은 결국 응용 프로그램의 최상위 계층에 있습니다. 이러한 종속성 중 일부를 더 높은 수준의 객체 (예 : ShippingMethodRepository 및 TransitTimeCalculator가 ShippingRateFinder로 결합 됨)로 함께 스모크 할 수 있지만 이는 특히 최상위 수준의 프레젠테이션 지향 객체의 경우에만 진행됩니다. 그것은 조롱하는 객체가 하나도 적지 만, 실제로 종속성을 제거하지 않고 간접적 인 계층을 통해 실제 종속성을 모호하게 만들었습니다.

하나의 모호한 조언은 객체를 삽입하고 의존성을 변경하는 인터페이스를 만드는 경우 (코드를 변경하면서 실제로 새 MessageBoxService에 드롭하려고합니까?) 정말 그렇습니다. ?), 그럼 신경 쓰지 마라. 이러한 종속성은 객체의 예상되는 동작의 일부이며 통합 테스트는 실제 비즈니스 가치가있는 곳이기 때문에 함께 테스트해야합니다.

다른 불경스런 충고는 일반적으로 MVC 컨트롤러 나 Windows Forms를 테스트 할 때 거의 유용하지 않다는 것입니다. 매번 누군가 HttpContext를 조롱하고 쿠키가 설정되었는지 확인하기 위해 테스트 할 때마다 나는 비명을 지르고 싶다. AccountController가 쿠키를 설정하면 누가 신경 쓰나요? 나는하지 않는다. 쿠키는 컨트롤러를 블랙 박스로 취급하는 것과 아무 관련이 없습니다. 통합 테스트는 기능을 테스트하는 데 필요합니다 (즉, 통합 테스트에서 Login() 후에 PrivilegedArea()에 대한 호출이 실패했습니다). 이렇게하면 로그인 쿠키의 형식이 변경되는 경우에 쓸모없는 단위 테스트가 무효화되는 것을 방지 할 수 있습니다.

개체 모델에 대한 단위 테스트를 저장하고, 프레젠테이션 계층에 대한 통합 테스트를 저장하고 가능한 경우 모의 개체를 피하십시오. 특정 종속성을 조롱하는 것이 어렵다면, 실용 주의적으로해야 할 때입니다. 단원 테스트를 수행하지 말고 대신 통합 테스트를 작성하여 시간 낭비를 막으십시오.

3

간단히 대답하면 을 테스트하려고 시도하는 코드가 너무 많이입니다. Single Responsibility Principle을 고수하면 도움이 될 것 같습니다.

저장 단추 방법 에는 다른 개체에 개체를 위임하기위한 최상위 수준 호출 만 포함해야합니다. 그런 다음 이러한 객체를 인터페이스를 통해 추상화 할 수 있습니다. 그런 다음 저장 단추 메서드를 테스트하면 조롱 된 개체과의 상호 작용 만 테스트합니다.

다음 단계는 이러한 하위 수준의 클래스에 테스트를 작성하는 것이지만 이러한 테스트는 격리되어 테스트하기 때문에 쉽지 않습니다. 복잡한 테스트 설정 코드가 필요한 경우 이는 잘못된 디자인 (또는 나쁜 테스트 방법)을 나타내는 좋은 지표입니다.

추천 도서 : 당신은 당신의 코드를 표시 할 경우

  1. Clean Code: A Handbook of Agile Software Craftsmanship
  2. Google's guide to writing testable code