"세터가없는"세상에서 단위 테스트


23

저는 DDD 전문가라고 생각하지 않지만 솔루션 아키텍트는 가능한 한 모범 사례를 적용하려고합니다. 나는 DDD에서 no (공개) 세터 "스타일"의 장단점에 대해 많은 논의가 있고 나는 논쟁의 양쪽을 볼 수있다. 제 문제는 모든 기술, 지식 및 경험이 풍부한 팀에서 일한다는 것입니다. 즉, 모든 개발자가 "올바른"방식으로 일을한다고 믿을 수 없습니다. 예를 들어, 도메인 객체가 객체의 내부 상태에 대한 변경이 메소드에 의해 수행되지만 공용 속성 설정자를 제공하도록 설계되면 누군가는 메소드를 호출하는 대신 속성을 설정해야합니다. 이 예제를 사용하십시오.

public class MyClass
{
    public Boolean IsPublished
    {
        get { return PublishDate != null; }
    }

    public DateTime? PublishDate { get; set; }

    public void Publish()
    {
        if (IsPublished)
            throw new InvalidOperationException("Already published.");

        PublishDate = DateTime.Today;

        Raise(new PublishedEvent());
    }
}

내 솔루션은 속성 세터를 비공개로 만드는 것인데, 우리가 객체를 수화하는 데 사용하는 ORM은 반사를 사용하여 개인 세터에 액세스 할 수 있기 때문에 가능합니다. 그러나 이것은 단위 테스트를 작성하려고 할 때 문제가됩니다. 예를 들어, 다시 게시 할 수없는 요구 사항을 확인하는 단위 테스트를 작성하려면 개체가 이미 게시되었음을 나타냅니다. 확실히 Publish를 두 번 호출 하여이 작업을 수행 할 수 있지만 테스트는 첫 번째 호출에 대해 Publish가 올바르게 구현되었다고 가정합니다. 약간 냄새가 나는 것 같습니다.

다음 코드를 사용하여 시나리오를 좀 더 실제적으로 만들어 봅시다.

public class Document
{
    public Document(String title)
    {
        if (String.IsNullOrWhiteSpace(title))
            throw new ArgumentException("title");

        Title = title;
    }

    public String ApprovedBy { get; private set; }
    public DateTime? ApprovedOn { get; private set; }
    public Boolean IsApproved { get; private set; }
    public Boolean IsPublished { get; private set; }
    public String PublishedBy { get; private set; }
    public DateTime? PublishedOn { get; private set; }
    public String Title { get; private set; }

    public void Approve(String by)
    {
        if (IsApproved)
            throw new InvalidOperationException("Already approved.");

        ApprovedBy = by;
        ApprovedOn = DateTime.Today;
        IsApproved = true;

        Raise(new ApprovedEvent(Title));
    }

    public void Publish(String by)
    {
        if (IsPublished)
            throw new InvalidOperationException("Already published.");

        if (!IsApproved)
            throw new InvalidOperationException("Cannot publish until approved.");

        PublishedBy = by;
        PublishedOn = DateTime.Today;
        IsPublished = true;

        Raise(new PublishedEvent(Title));
    }
}

다음을 확인하는 단위 테스트를 작성하고 싶습니다.

  • 문서가 승인되지 않으면 게시 할 수 없습니다
  • 문서를 다시 게시 할 수 없습니다
  • 게시되면 PublishedBy 및 PublishedOn 값이 올바르게 설정됩니다
  • 게시되면 PublishedEvent가 발생합니다.

세터에 액세스하지 않으면 테스트를 수행하는 데 필요한 상태로 개체를 넣을 수 없습니다. 세터에 대한 액세스 권한을 열면 액세스 방지 목적이 무효화됩니다.

이 문제를 어떻게 해결합니까 (d)?


내가 이것에 대해 더 많이 생각할수록, 당신의 모든 문제가 부작용을 가진 방법을 가지고 있다고 생각합니다. 또는 변경 불가능한 불변 객체입니다. DDD 세계에서이 객체의 내부 상태를 업데이트하는 대신 승인 및 게시에서 새 Document 객체를 반환해서는 안됩니까?
pdr

1
어떤 질문을 사용하고 있습니까? 나는 EF의 열렬한 팬이지만 보호자로서 setter를 선언하면 나에게 잘못된 길을 조금 문지릅니다.
Michael Brown

우리는 지금 랭 글링을 담당 한 프리 레인지 개발로 인해 혼합되어 있습니다. AutoMapper를 사용하여 몇 가지 Linq-to-SQL 모델 인 DataReader에서 수화하기 위해 일부 ADO.NET ) 및 일부 새로운 EF 모델.
SonOfPirate

Publish를 두 번 호출하면 전혀 냄새가 나지 않으며 그렇게하는 방법입니다.
Piotr Perak

답변:


27

테스트를 수행하는 데 필요한 상태로 개체를 넣을 수 없습니다.

테스트를 수행하는 데 필요한 상태로 오브젝트를 넣을 수없는 경우 프로덕션 코드에서 오브젝트를 상태로 넣을 수 없으므로 해당 상태 를 테스트 필요가 없습니다 . 물론,이 사건의 사실이 아니다, 당신은 할 수 있습니다 만 승인 호출 필요한 상태로 개체를 넣어.

  • 문서가 승인되지 않은 경우 게시 할 수 없습니다. 승인하기 전에 게시를 호출하면 개체 상태를 변경하지 않고 올바른 오류가 발생하는 테스트를 작성하십시오.

    void testPublishBeforeApprove() {
        doc = new Document("Doc");
        AssertRaises(doc.publish, ..., NotApprovedException);
    }
    
  • 문서를 다시 게시 할 수 없습니다. 개체를 승인 한 다음 게시를 한 번 호출하면 테스트가 성공하지만 두 번째로 개체 상태를 변경하지 않고 올바른 오류가 발생합니다.

    void testRePublish() {
        doc = new Document("Doc");
        doc.approve();
        doc.publish();
        AssertRaises(doc.publish, ..., RepublishException);
    }
    
  • 게시되면 PublishedBy 및 PublishedOn 값이 올바르게 설정됩니다. 승인을 호출하는 테스트를 작성한 후 publish를 호출하여 오브젝트 상태가 올바르게 변경되도록합니다.

    void testPublish() {
        doc = new Document("Doc");
        doc.approve();
        doc.publish();
        Assert(doc.PublishedBy, ...);
        ...
    }
    
  • 공개 될 때 PublishedEvent가 발생합니다. 이벤트 시스템에 연결하고 호출되도록 플래그를 설정하십시오.

또한 승인 테스트를 작성해야합니다.

즉, 내부 필드와 IsPublished 및 IsApproved 간의 관계를 테스트하지 마십시오. 필드를 변경하면 테스트 코드가 변경되므로 테스트는 의미가 없으므로 테스트는 매우 취약합니다. 대신 필드를 수정하더라도 테스트를 수정할 필요가없는 경우에도 이런 방법으로 퍼블릭 메서드 호출 간의 관계를 테스트해야합니다.


승인이 중단되면 여러 테스트가 중단됩니다. 더 이상 코드 단위를 테스트하지 않고 전체 구현을 테스트합니다.
pdr

나는 pdr의 관심사를 공유하기 때문에이 방향으로가는 것을 망설였다. 예, 가장 깨끗해 보이지만 개별 테스트가 실패 할 수있는 여러 가지 이유가 마음에 들지 않습니다.
SonOfPirate

4
단 한 가지 이유로 실패 할 수있는 단위 테스트를 아직 보지 못했습니다. 또한 테스트의 "상태 조작"부분을 setup()테스트 자체가 아닌 메소드에 넣을 수 있습니다.
Peter K.

12
왜에 따라되어 approve()어떻게 든 취성, 아직에 따라 setApproved(true)어떻게 든하지? approve()요구 사항의 종속성이기 때문에 테스트에서 합법적 인 종속성입니다. 의존성이 테스트에만 존재한다면, 그것은 또 다른 문제 일 것입니다.
Karl Bielefeldt

2
@ pdr, 스택 클래스를 어떻게 테스트 하시겠습니까? push()pop()방법을 독립적 으로 테스트 하시겠습니까 ?
Winston Ewert

2

또 다른 접근 방식은 인스턴스화시 내부 속성을 설정할 수있는 클래스 생성자를 만드는 것입니다.

 public Document(
  String approvedBy,
  DateTime? approvedOn,
  Boolean isApproved,
  Boolean isPublished,
  String publishedBy,
  DateTime? publishedOn,
  String title)
{
  ApprovedBy = approvedBy;
  ApprovedOn = approvedOn;
  IsApproved = isApproved;
  IsApproved = isApproved;
  PublishedBy = publishedBy;
  PublishedOn = publishedOn;
}

2
이것은 전혀 확장되지 않습니다. 내 객체에는 객체 수명주기의 특정 시점에 값이 있거나없는 많은 속성이있을 수 있습니다. 나는 생성자가 객체가 유효한 초기 상태 또는 객체가 작동하는 데 필요한 종속성에 필요한 속성에 대한 매개 변수를 포함하고 있다는 원칙을 따릅니다. 예제에서 속성의 목적은 객체가 조작 될 때 현재 상태를 캡처하는 것입니다. 모든 속성을 가진 생성자 또는 다른 조합으로 과부하가 발생하는 것은 큰 냄새이며, 말한 것처럼 확장되지 않습니다.
SonOfPirate

이해했다. 귀하의 예제는 더 많은 속성을 언급하지 않았으며 예제의 숫자는 이것을 유효한 접근 방식으로 사용하는 것입니다. 당신이 : 당신에게 당신의 디자인에 대해 뭔가를 말하고있는 것 같다 수 없습니다 에 개체를 넣어 모든 인스턴스에 유효한 상태. 즉, 유효한 초기 상태로 만들어야하고 테스트를 위해 올바른 상태로 조작해야합니다. 그것은 Lie Ryan의 대답이가는 길임을 암시합니다 .
Peter K.

객체가 하나의 속성을 가지고 있으며이 솔루션을 절대로 변경하지 않더라도 나쁘지 않습니다. 프로덕션에서이 생성자를 사용하지 못하게하는 요인은 무엇입니까? 이 생성자를 어떻게 [TestOnly]로 표시 하시겠습니까?
Piotr Perak

생산이 왜 나쁜가요? (정말로 알고 싶습니다). 때로는 하나의 유효한 초기 객체가 아니라 생성시 객체의 정확한 상태를 재현해야 할 때가 있습니다.
Peter K.

1
따라서 객체를 유효한 초기 상태로 만드는 데 도움이되지만 수명주기 동안 객체의 동작을 테스트하려면 객체를 초기 상태에서 변경해야합니다. 내 OP는 단순히 객체의 상태를 변경하기 위해 속성을 설정할 수 없을 때 이러한 추가 상태를 테스트하는 것과 관련이 있습니다.
SonOfPirate

1

한 가지 전략은 클래스 (이 경우 Document)를 상속하고 상속 된 클래스에 대한 테스트를 작성하는 것입니다. 상속 된 클래스는 테스트에서 객체 상태를 설정할 수있는 방법을 제공합니다.

C #에서 한 가지 전략은 세터를 내부로 만든 다음 내부를 노출시켜 프로젝트를 테스트하는 것입니다.

설명 된대로 클래스 API를 사용할 수도 있습니다 ( "발행을 두 번 호출하여 확실히 수행 할 수 있습니다"). 이것은 객체의 공공 서비스를 사용하여 객체 상태를 설정하는 것입니다. 나에게 너무 냄새가 나지 않습니다. 귀하의 예의 경우, 아마도 내가 한 방식 일 것입니다.


나는 이것을 가능한 해결책으로 생각했지만 객체를 열고 캡슐화를 깨뜨리는 것처럼 느껴져 내 속성을 재정의하거나 세터를 보호 된 것으로 노출하는 것을 주저했습니다. 나는 재산을 보호하는 것이 공공 또는 내부 / 친구보다 확실히 낫다고 생각합니다. 나는이 접근법에 더 많은 생각을 할 것이다. 간단하고 효과적입니다. 때로는 이것이 최선의 방법입니다. 의견이 맞지 않으면 구체적으로 의견을 추가하십시오.
SonOfPirate

1

도메인 개체가받는 명령과 쿼리 를 절대적으로 격리 하여 테스트하기 위해 각 테스트에 예상 상태의 개체 직렬화를 제공하는 데 사용됩니다. 테스트의 정렬 섹션에서 이전에 준비한 파일에서 테스트 할 개체를로드합니다. 처음에는 바이너리 직렬화를 시작했지만 json은 관리하기가 훨씬 쉽다는 것이 입증되었습니다. 이것은 테스트에서 절대적인 격리가 실제 가치를 제공 할 때마다 잘 작동하는 것으로 판명되었습니다.

편집 (BTW 냄새입니다 순환 개체의 그래프의 경우처럼) 바로 메모, JSON 직렬화가 실패 몇 번. 그런 상황에서 나는 이진 직렬화로 구조합니다. 약간 실용적이지만 작동합니다. :-)


setter가없고 설정을 위해 public 메소드를 호출하지 않으려면 어떻게 예상 상태에서 오브젝트를 준비합니까?
Piotr Perak

나는 그것을 위해 작은 도구를 작성했습니다. 리플렉션에 의해 클래스를로드하여 공용 생성자 (일반적으로 식별자 만 사용)를 사용하여 새 엔티티를 생성하고 Action <TEntity> 배열을 호출하여 각 작업 후 스냅 샷을 저장합니다 (조치 색인을 기반으로 한 일반적인 이름으로) 엔터티 이름). 이 도구는 엔티티 코드의 각 리팩토링에서 수동으로 실행되며 스냅 샷은 DCVS에 의해 추적됩니다. 분명히 각 액션은 엔티티의 공개 명령을 호출하지만, 이것은 실제로 단위 테스트 라는 테스트 실행에서 수행됩니다 .
Giacomo Tesio

나는 그것이 어떻게 변화하는지 이해하지 못한다. 여전히 sut (테스트중인 시스템)에서 공개 메소드를 호출하면 테스트에서 해당 메소드를 호출하는 것과 다르지 않습니다.
Piotr Perak

스냅 샷이 생성되면 파일에 저장됩니다. 각 테스트는 엔터티의 시작 상태를 얻는 데 필요한 작업 순서가 아니라 상태 자체 (스냅 샷에서로드 됨)에 따라 다릅니다. 테스트중인 메소드 자체는 다른 메소드의 변경 사항과 분리됩니다.
Giacomo Tesio

테스트를 위해 직렬화 된 상태를 준비하는 데 사용 된 공개 메소드를 변경했지만 직렬화 된 오브젝트를 재생성하기 위해 도구를 실행하는 것을 잊어 버린 경우 어떻게해야합니까? 코드에 오류가 있어도 테스트는 여전히 녹색입니다. 여전히 나는 이것이 아무것도 변하지 않는다고 말합니다. 여전히 공개 메소드를 실행하므로 테스트 할 오브젝트를 설정하십시오. 그러나 테스트가 실행되기 오래 전에 실행합니다.
Piotr Perak

-7

당신은 말합니다

가능할 때마다 모범 사례를 적용하려고합니다

오브젝트를 수화하는 데 사용하는 ORM은 리플렉션을 사용하므로 개인 설정기에 액세스 할 수 있습니다.

클래스에서 액세스 제어를 우회하기 위해 리플렉션을 사용하는 것이 "모범 사례"라고 설명하는 것이 아니라고 생각해야합니다. 너무 느려질 것입니다.


개인적으로, 나는 당신의 단위 테스트 프레임 워크를 긁어 내고 수업에 무언가를 갈 것입니다-어쨌든 전체 클래스를 테스트하는 관점에서 테스트를 작성하는 것 같습니다. 과거에는 테스트가 필요한 까다로운 구성 요소의 경우 어설 션 및 설정 코드를 클래스 자체에 포함 시켰습니다 (모든 클래스에서 test () 메서드를 사용하는 일반적인 디자인 패턴 이었음). 단순히 객체를 인스턴스화하고 리플렉션 해킹과 같은 불쾌 함없이 원하는대로 설정할 수있는 테스트 메소드를 호출합니다.

코드 팽창이 걱정된다면 #ifdefs에서 테스트 메소드를 랩핑하여 디버그 코드에서만 사용 가능하도록 만드십시오 (아마도 모범 사례 자체).


4
-1 : 테스트 프레임 워크를 긁어 내고 클래스 내부의 테스트 방법으로 돌아가는 것은 단위 테스트의 어두운 시대로 거슬러 올라갑니다.
Robert Johnson

9
나에게 -1은 없지만 프로덕션 환경에 테스트 코드를 포함시키는 것은 일반적으로 Bad Thing (TM) 입니다.
Peter K.

OP는 무엇을합니까? 전용 세터로 조이는 것을 고집하십니까?! 마시는 독을 선택하는 것과 같습니다. OP에 대한 제안은 단위 테스트를 프로덕션이 아닌 디버그 코드에 넣는 것이 었습니다. 내 경험상 단위 테스트를 다른 프로젝트에 넣는 것은 어쨌든 프로젝트가 원본과 밀접하게 연결되어 있음을 의미하므로 개발자 PoV와는 별 차이가 없습니다.
gbjbaanb
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.