테스트 가능한 코드를 홍보하는 디자인 원칙은 무엇입니까? (테스트 가능한 코드 디자인과 테스트를 통한 디자인 설계)


54

내가 작업하는 대부분의 프로젝트는 나중에 개발 및 단위 테스트를 고려하여 나중에 단위 테스트를 작성하는 것을 악몽으로 만듭니다. 저의 목표는 높은 수준과 낮은 수준의 디자인 단계 자체에서 테스트를 염두에 두는 것입니다.

테스트 가능한 코드를 홍보하는 잘 정의 된 디자인 원칙이 있는지 알고 싶습니다. 최근에 이해하게 된 그러한 원칙 중 하나는 종속성 주입 및 제어 반전을 통한 종속성 반전입니다.

SOLID로 알려진 것이 있습니다. SOLID 원칙을 간접적으로 따라 쉽게 테스트 할 수있는 코드가 생성되는지 이해하고 싶습니다. 그렇지 않은 경우 테스트 가능한 코드를 홍보하는 잘 정의 된 디자인 원칙이 있습니까?

테스트 주도 개발이라는 것이 있습니다. 그러나 테스트를 통해 디자인을 유도하는 대신 디자인 단계 자체에서 테스트를 염두에두고 코드를 디자인하는 데 더 관심이 있습니다. 이것이 의미가 있기를 바랍니다.

이 주제와 관련된 또 다른 질문은 기존 제품 / 프로젝트를 리팩터링하고 각 모듈에 대해 단위 테스트 사례를 작성할 수 있도록 코드와 디자인을 변경하는 것이 괜찮은지 여부입니다.



감사합니다. 나는 단지 기사를 읽기 시작했고 이미 이해가되었다.

1
이것은 면접 질문 중 하나입니다 ( "단위 테스트가 용이하도록 코드를 어떻게 디자인합니까?"). 단위 테스트, 조롱 / 스터 빙, OOD 및 잠재적으로 TDD를 이해하고 있는지 한 손으로 보여줍니다. 안타깝게도 대답은 대개 "테스트 데이터베이스 만들기"와 같은 것입니다.
Chris Pitman 2016 년

답변:


56

예, SOLID는 쉽게 테스트 할 수있는 코드를 설계하는 매우 좋은 방법입니다. 짧은 입문서로서 :

S-단일 책임 원칙 : 객체는 정확히 한 가지 일을 수행해야하며 코드베이스에서 그 일을하는 유일한 객체 여야합니다. 예를 들어 인보이스와 같은 도메인 클래스를 선택하십시오. 송장 클래스는 시스템에서 사용되는 송장의 데이터 구조 및 비즈니스 규칙을 나타내야합니다. 코드베이스에서 송장을 나타내는 유일한 클래스 여야합니다. 이것은 메소드가 하나의 목적을 가져야하며 코드베이스 에서이 요구를 충족시키는 유일한 방법이어야한다고 말하기 위해 더 세분화 될 수 있습니다.

이 원칙을 따르면 다른 개체에서 동일한 기능을 테스트하기 위해 작성해야하는 테스트 수를 줄임으로써 디자인의 테스트 가능성을 향상시킬 수 있으며 일반적으로 더 작은 기능을 테스트하여 더 쉽게 테스트 할 수 있습니다.

O- 개방 / 폐쇄 원칙 : 클래스는 확장에 개방되어 있지만 변경하려면 폐쇄해야합니다 . 객체가 존재하고 올바르게 작동하면 이상적으로 해당 객체로 돌아가서 새로운 기능을 추가하는 변경 작업을 수행 할 필요가 없습니다. 대신, 새로운 기능을 제공하기 위해 객체를 파생 시키거나 새로운 또는 다른 종속성 구현을 연결하여 객체를 확장해야합니다. 이것은 회귀를 피합니다. 이미 다른 곳에서 사용되는 객체의 동작을 변경하지 않고도 필요할 때 언제 어디서나 새로운 기능을 도입 할 수 있습니다.

이 원칙을 준수하면 일반적으로 "모의 (mock)"를 허용하는 코드의 기능이 향상되고 새로운 동작을 예상하기 위해 테스트를 다시 작성하지 않아도됩니다. 개체에 대한 모든 기존 테스트는 확장되지 않은 구현에서 계속 작동해야하며 확장 구현을 사용하는 새로운 기능에 대한 새로운 테스트도 작동해야합니다.

L-Liskov 대체 원칙 : 클래스 B에 따라 A 클래스는 차이를 몰라도 X : B를 사용할 수 있어야합니다. 이것은 기본적으로 종속성으로 사용하는 모든 항목이 종속 클래스에서 볼 수있는 것과 유사한 동작을 가져야 함을 의미합니다. 간단한 예로, ConsoleWriter로 구현 된 Write (string)를 노출하는 IWriter 인터페이스가 있다고 가정합니다. 이제 파일에 대신 써야하므로 FileWriter를 만듭니다. 그렇게 할 때, FileWriter가 ConsoleWriter와 같은 방식으로 사용될 수 있는지 확인해야합니다 (의존자가 상호 작용할 수있는 유일한 방법은 Write (string)를 호출하는 것 뿐이므로). FileWriter가이를 수행해야하는 추가 정보 작업 (예 : 경로 및 파일 쓰기)은 부양 가족이 아닌 다른 곳에서 제공해야합니다.

LSP를 준수하는 디자인은 예상되는 동작을 변경하지 않고 언제라도 실제 객체 대신 "모의 된"객체를 대체 할 수 있기 때문에 테스트 가능한 코드를 작성하는 데 큰 도움이됩니다. 그러면 시스템이 연결된 실제 객체와 함께 작동합니다.

I- 인터페이스 분리 원리 : 인터페이스는 인터페이스에 의해 정의 된 역할의 기능을 제공하기 위해 가능한 한 적은 방법을 가져야합니다 . 간단히 말해, 더 작은 인터페이스가 더 작은 인터페이스보다 낫습니다. 큰 인터페이스에는 변경해야 할 이유가 더 많기 때문에 코드베이스의 다른 곳에서는 필요하지 않은 변경이 더 많이 발생하기 때문입니다.

ISP를 준수하면 테스트중인 시스템의 복잡성과 해당 SUT의 종속성이 줄어들어 테스트 가능성이 향상됩니다. 테스트중인 객체가 DoOne (), DoTwo () 및 DoThree ()를 노출하는 IDoThreeThings 인터페이스에 의존하는 경우 객체가 DoTwo 메소드 만 사용하더라도 세 가지 메소드를 모두 구현하는 객체를 조롱해야합니다. 그러나 객체가 IDoTwo에만 의존하는 경우 (DoTwo 만 노출) 해당 방법이있는 객체를 더 쉽게 조롱 할 수 있습니다.

D- 종속성 역전 원리 : 생성 과 추상화는 다른 생성에 의존해서는 안되며 추상화에 의존해서는 안된다 . 이 원리는 느슨한 결합의 신조를 직접 시행합니다. 객체는 객체가 무엇인지 알 필요가 없습니다. 대신 객체가하는 일을 신경 써야합니다. 따라서 객체 또는 메소드의 속성 및 매개 변수를 정의 할 때 인터페이스 및 / 또는 추상 기본 클래스를 사용하는 것이 구체적인 구현을 사용하는 것보다 항상 선호됩니다. 따라서 사용법을 변경하지 않고도 한 구현을 다른 구현으로 바꿀 수 있습니다 (DIP와 함께 사용되는 LSP도 따르는 경우).

다시 한 번, 테스트 할 개체에 "생산"구현 대신 종속성의 모의 구현을 주입 할 수 있기 때문에 테스트 가능성이 매우 높습니다. 생산에서. 이것이 "단독"단위 테스트의 핵심입니다.


16

SOLID로 알려진 것이 있습니다. SOLID 원칙을 간접적으로 따라 쉽게 테스트 할 수있는 코드가 생성되는지 이해하고 싶습니다.

올바르게 적용하면 그렇습니다. Jeff가 SOLID 원리를 매우 짧은 방법으로 설명 하는 블로그 게시물 이 있습니다 (podcast도 청취 할 가치가 있습니다). 더 긴 설명으로 인해 문제가 발생하는 경우 살펴볼 것을 제안합니다.

내 경험상 SOLID의 2 가지 원칙은 테스트 가능한 코드를 디자인하는 데 중요한 역할을합니다.

  • 인터페이스 분리 원리 -적은 범용 인터페이스 대신 많은 클라이언트 별 인터페이스를 선호해야합니다. 이것은 단일 책임 원칙 과 함께 사용되며 기능 / 작업 중심 클래스를 설계하는 데 도움이됩니다. 기능 / 작업 중심 클래스는 더 일반적인 테스트에 비해 훨씬 쉬우 며 (종종 일반적인 "관리자""컨텍스트" )에 비해 테스트가 훨씬 쉽습니다. , 복잡성 감소, 세밀하고 명확한 테스트. 간단히 말해 작은 구성 요소는 간단한 테스트로 이어집니다.
  • 종속성 반전 원리 -구현이 아닌 계약에 의한 설계. 복잡한 객체를 테스트 하고 설정하기 위해 전체 종속성 그래프가 필요하지는 않지만 인터페이스를 조롱하고 완료 할 수 있다는 사실을 인식하면 가장 도움이됩니다 .

테스트 가능성을 설계 할 때이 두 가지가 가장 도움이 될 것입니다. 나머지 것들도 영향을 미치지 만 크게 말하지는 않습니다.

(...) 기존 제품 / 프로젝트를 리팩터링하고 각 모듈에 대해 단위 테스트 케이스를 작성할 수 있도록 코드 및 디자인을 변경해도 괜찮습니까?

기존의 단위 테스트가 없으면 문제를 묻는 것입니다. 단위 테스트는 코드가 작동 한다는 것을 보증 합니다 . 적절한 테스트 범위가있는 경우 주요 변경 사항이 즉시 발견됩니다.

이제 단위 테스트추가 하기 위해 기존 코드변경 하려는 경우 아직 테스트가 없지만 코드를 이미 변경 한 틈이 생깁니다 . 당연히, 당신은 당신의 변화가 어떻게되었는지에 대한 단서가 없을 수도 있습니다. 이것은 피하고 싶은 상황입니다.

어쨌든 단위 테스트는 테스트하기 어려운 코드에 대해서도 작성할 가치가 있습니다. 코드 가 작동 하지만 단위 테스트가 아닌 경우 테스트를 작성한 다음 변경 사항 도입 하는 것이 적절한 해결책입니다 . 그러나보다 쉽게 ​​테스트 할 수 있도록 테스트 된 코드를 변경하는 것은 경영진이 비용을 지출하고 싶지 않은 일입니다 (비즈니스 가치가 거의 없거나 전혀 들지 않을 것입니다).


iaw 높은 응집력 및 낮은 커플 링
jk.

8

첫 번째 질문 :

SOLID는 실제로 나아가는 길입니다. SOLID 약어의 가장 중요한 두 가지 측면은 테스트 가능성과 관련하여 S (Single Responsibility)와 D (Dependency Injection)입니다.

단일 책임 : 수업은 실제로 한 가지 일만해야합니다. 파일을 생성하고 일부 입력을 구문 분석하여 파일에 쓰는 클래스는 이미 세 가지 작업을 수행합니다. 수업이 한 가지만 수행하는 경우 정확히 무엇을 기대해야하는지 알기 때문에 테스트 사례를 디자인하는 것은 매우 쉬워야합니다.

DI (Dependency Injection) : 테스트 환경을 제어 할 수 있습니다. 코드 내에 잘못된 객체를 만드는 대신 클래스 생성자 또는 메서드 호출을 통해 객체를 삽입합니다. 단위 테스트를 수행 할 때 실제 클래스를 스텁 또는 모의 객체로 바꾸면 완전히 제어 할 수 있습니다.

두 번째 질문 : 코드를 리팩토링하기 전에 코드의 기능을 문서화하는 테스트를 작성하는 것이 이상적입니다. 이러한 방식으로 리팩토링이 원래 코드와 동일한 결과를 재현한다는 것을 문서화 할 수 있습니다. 그러나 문제는 기능 코드를 테스트하기 어렵다는 것입니다. 이것은 고전적인 상황입니다! 내 조언은 : 단위 테스트 전에 리팩토링에 대해 신중하게 생각하십시오. 가능하다면; 작업 코드에 대한 테스트를 작성한 다음 코드를 리팩터링 한 다음 테스트를 리팩터링하십시오. 시간이 걸리 겠지만 리팩토링 된 코드가 이전 코드와 동일하다는 것이 더 확실합니다. 그렇게 말하면서 나는 많은 시간을 포기했다. 클래스는 너무 추악하고 혼란 스러울 수 있으므로 재 작성이 테스트 가능하게 만드는 유일한 방법입니다.


4

느슨한 결합을 달성하는 데 중점을 둔 다른 답변 외에도 복잡한 논리를 테스트하는 것에 대해 이야기하고 싶습니다.

한 번은 논리가 복잡하고 조건이 많고 필드의 역할을 이해하기 어려운 클래스를 단위 테스트해야했습니다.

이 코드를 상태 머신 을 나타내는 많은 작은 클래스로 교체했습니다 . 이전 클래스의 여러 상태가 명시 적이기 때문에 논리를 따르기가 훨씬 간단 해졌습니다. 각 주 클래스는 다른 클래스와 독립적이므로 쉽게 테스트 할 수있었습니다.

상태가 명시 적이라는 사실은 코드의 가능한 모든 경로 (상태 전환)를 열거하고 각 상태에 대한 단위 테스트를 작성하는 것이 더 쉬워졌습니다.

물론 모든 복잡한 논리를 상태 머신으로 모델링 할 수있는 것은 아닙니다.


3

SOLID는 제 경험상 SOLID의 네 가지 측면이 실제로 단위 테스트와 잘 작동하는 훌륭한 출발입니다.

  • 단일 책임 원칙 -각 수업은 한 가지 일만합니다. 값 계산, 파일 열기, 문자열 구문 분석 등 무엇이든 가능합니다. 따라서 의사 결정 지점뿐만 아니라 입력 및 출력의 양도 매우 작아야합니다. 테스트를 쉽게 작성할 수 있습니다.
  • Liskov 대체 원칙 -코드의 바람직한 속성 (예상 결과)을 변경하지 않고 스텁 및 모의 객체를 대체 할 수 있어야합니다.
  • 인터페이스 분리 원칙 -인터페이스별로 접점을 분리하면 Moq와 같은 조롱 프레임 워크를 사용하여 스텁 및 모의 객체를 매우 쉽게 만들 수 있습니다. 구체적인 클래스에 의존하는 대신 인터페이스를 구현하는 것에 의존하고 있습니다.
  • 의존성 주입 원리 -테스트하려는 메소드의 생성자, 속성 또는 매개 변수를 통해 스텁과 모의를 코드에 주입 할 수 있습니다.

또한 다른 패턴, 특히 팩토리 패턴을 살펴볼 것입니다. 인터페이스를 구현하는 구체적인 클래스가 있다고 가정 해 봅시다. 구체적 클래스를 인스턴스화하는 팩토리를 작성하지만 대신 인터페이스를 리턴합니다.

public interface ISomeInterface
{
    int GetValue();
}  

public class SomeClass : ISomeInterface
{
    public int GetValue()
    {
         return 1;
    }
}

public interface ISomeOtherInterface
{
    bool IsSuccess();
}

public class SomeOtherClass : ISomeOtherInterface
{
     private ISomeInterface m_SomeInterface;

     public SomeOtherClass(ISomeInterface someInterface)
     {
          m_SomeInterface = someInterface;
     }

     public bool IsSuccess()
     {
          return m_SomeInterface.GetValue() == 1;
     }
}

public class SomeFactory
{
     public virtual ISomeInterface GetSomeInterface()
     {
          return new SomeClass();
     }

     public virtual ISomeOtherInterface GetSomeOtherInterface()
     {
          ISomeInterface someInterface = GetSomeInterface();

          return new SomeOtherClass(someInterface);
     }
}

테스트에서 Moq 또는 다른 모의 프레임 워크를 사용하여 해당 가상 메서드를 재정의하고 디자인의 인터페이스를 반환 할 수 있습니다. 그러나 구현 코드에 관한 한 팩토리는 변경되지 않았습니다. 이 방법으로 많은 구현 세부 사항을 숨길 수 있습니다. 구현 코드는 인터페이스의 구축 방법에 신경 쓰지 않으며 인터페이스가 다시 필요한 것입니다.

이 부분을 조금 더 확장하고 싶다면 The Art of Unit Testing을 읽는 것이 좋습니다 . 이 원칙을 사용하는 방법에 대한 훌륭한 예를 제공하며 매우 빨리 읽습니다.


1
이를 "주입"원칙이 아닌 의존성 "반전"원칙이라고합니다.
Mathias Lykkegaard Lorenzen
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.