동일한 동작을 나타내는 여러 개체에 대한 단위 테스트를 어떻게 구성합니까?


9

많은 경우에 약간의 동작이있는 기존 클래스가있을 수 있습니다.

class Lion
{
    public void Eat(Herbivore herbivore) { ... }
}

... 그리고 단위 테스트가 있습니다 ...

[TestMethod]
public void Lion_can_eat_herbivore()
{
    var herbivore = buildHerbivoreForEating();
    var test = BuildLionForTest();
    test.Eat(herbivore);
    Assert.IsEaten(herbivore);
}

이제는 라이온과 동일하게 행동하는 Tiger 클래스를 만들어야합니다.

class Tiger
{
    public void Eat(Herbivore herbivore) { ... }
}

... 동일한 동작을 원하므로 동일한 테스트를 실행해야합니다.

interface IHerbivoreEater
{
    void Eat(Herbivore herbivore);
}

... 그리고 테스트를 리팩터링합니다.

[TestMethod]
public void Lion_can_eat_herbivore()
{
    IHerbivoreEater_can_eat_herbivore(BuildLionForTest);
}


public void IHerbivoreEater_can_eat_herbivore(Func<IHerbivoreEater> builder)
{
    var herbivore = buildHerbivoreForEating();
    var test = builder();
    test.Eat(herbivore);
    Assert.IsEaten(herbivore);
}

... 그리고 새로운 Tiger수업에 대한 또 다른 시험을 추가합니다 .

[TestMethod]
public void Tiger_can_eat_herbivore()
{
    IHerbivoreEater_can_eat_herbivore(BuildTigerForTest);
}

... 그리고 나는 내 클래스 LionTiger클래스를 리팩토링합니다 (일반적으로 상속, 때로는 구성).

class Lion : HerbivoreEater { }
class Tiger : HerbivoreEater { }

abstract class HerbivoreEater : IHerbivoreEater
{
    public void Eat(Herbivore herbivore) { ... }
}

... 그리고 모든 것이 잘됩니다. 그러나 이제 기능이 HerbivoreEater클래스에 있으므로 각 서브 클래스에서 이러한 각 동작에 대한 테스트를 수행하는 데 문제가있는 것 같습니다. 그러나 실제로 소비되고있는 서브 클래스를, 그리고 그들이 중복 행동을 공유하는 (일 만 구현 세부의 LionsTigers예를 들어, 완전히 다른 최종 용도를 가질 수있다).

동일한 코드를 여러 번 테스트하는 것은 불필요한 것처럼 보이지만 하위 클래스가 기본 클래스의 기능을 재정의 할 수 있고 재정의하는 경우가 있습니다 (예, LSP를 위반할 수 있지만 직면 할 수 있음) IHerbivoreEater는 편리한 테스트 인터페이스입니다. 최종 사용자에게는 중요하지 않을 수 있습니다). 따라서 이러한 테스트에는 가치가 있다고 생각합니다.

이 상황에서 다른 사람들은 무엇을합니까? 테스트를 기본 클래스로 옮기거나 예상되는 동작에 대해 모든 서브 클래스를 테스트합니까?

편집 :

@pdr의 답변을 바탕으로 우리는 이것을 고려해야한다고 생각합니다 IHerbivoreEater. 동작을 지정하지 않습니다. 예를 들어 :

[TestMethod]
public void Tiger_eats_herbivore_haunches_first()
{
    IHerbivoreEater_eats_herbivore_haunches_first(BuildTigerForTest);
}

[TestMethod]
public void Cheetah_eats_herbivore_haunches_first()
{
    IHerbivoreEater_eats_herbivore_haunches_first(BuildCheetahForTest);
}

[TestMethod]
public void Lion_eats_herbivore_head_first()
{
    IHerbivoreEater_eats_herbivore_head_first(BuildLionForTest);
}

논쟁을 위해 ? Animal가 포함 된 클래스 가 없어야합니다 Eat. 모든 동물은 먹고, 따라서 TigerLion클래스는 동물에서 상속 할 수있다.
머핀 맨

1
@ 닉-좋은 지적이지만 다른 상황이라고 생각합니다. @pdr이 지적했듯이 Eat기본 클래스에 동작 을 넣으면 모든 하위 클래스가 동일한 Eat동작을 나타냅니다 . 그러나 나는 행동을 공유하는 비교적 관련이없는 2 개의 수업에 대해 이야기하고 있습니다. 예를 들어, 고려 Fly의 행동 BrickPerson우리가 가정 할 수있는, 비슷한 비행 동작을 전시, 그러나 반드시 그들이 공통 기본 클래스에서 파생 가지고 이해가되지 않습니다.
Scott Whitlock

답변:


6

테스트가 디자인에 대한 생각을 실제로 이끌어내는 방법을 보여주기 때문에 훌륭합니다. 설계상의 문제를 감지하고 올바른 질문을합니다.

이것을 보는 데는 두 가지 방법이 있습니다.

IHerbivoreEater는 계약입니다. 모든 IHerbivoreEater는 초식 동물을 수용하는 Eat 방법을 가져야합니다. 이제, 당신의 검사는 어떻게 먹는지 신경 쓰지 않습니다. 당신의 사자는 뭉치로 시작하고 호랑이는 목구멍에서 시작할 수 있습니다. 당신의 모든 테스트 관심사는 그것이 Eat라고 불린 후 Herbivore가 먹는다는 것입니다.

반면에, 당신이 말하는 것의 일부는 모든 IHerbivoreEaters가 정확히 같은 방식으로 (즉 기본 클래스) Herbivore를 먹는다는 것입니다. 이 경우 IHerbivoreEater 계약이 전혀 필요하지 않습니다. 아무것도 제공하지 않습니다. HerbivoreEater에서 상속받을 수도 있습니다.

또는 사자와 호랑이를 완전히 버려야합니다.

그러나 사자와 호랑이가 식습관을 제외하고 모든면에서 다른 경우 복잡한 상속 트리에 문제가 있는지 궁금해하기 시작해야합니다. Feline에서 두 클래스를 모두, KingOfItsDomain에서 Lion 클래스 만 (상어와 함께) 파생 시키려면 어떻게해야합니까? 이것이 LSP가 진정으로 들어오는 곳입니다.

공통 코드가 더 잘 캡슐화되도록 제안합니다.

public class Lion : IHerbivoreEater
{
    private IHerbivoreEatingStrategy _herbivoreEatingStrategy;
    private Lion (IHerbivoreEatingStrategy herbivoreEatingStrategy)
    {
        _herbivoreEatingStrategy = herbivoreEatingStrategy;
    }

    public Lion() : this(new StandardHerbivoreEatingStrategy())
    {
    }

    public void Eat(Herbivore herbivore)
    {
        _herbivoreEatingStrategy.Eat(herbivore);
    }
}

Tiger도 마찬가지입니다.

자, 여기 아름다운 것이 발전하고 있습니다 (의도하지 않았기 때문에 아름답습니다). 해당 개인 생성자를 테스트에 사용할 수있게하려면 가짜 IHerbivoreEatingStrategy를 전달하고 메시지가 캡슐화 된 오브젝트에 올바르게 전달되는지 테스트하십시오.

처음에 걱정하던 복잡한 테스트는 StandardHerbivoreEatingStrategy 만 테스트하면됩니다. 하나의 클래스, 하나의 테스트 세트, 걱정할 중복 코드가 없습니다.

그리고 나중에 Tigers에게 초식 동물을 다른 방식으로 먹어야한다고 말하고 싶다면이 테스트 중 어느 것도 바꾸지 않아도됩니다. 단순히 새로운 HerbivoreEatingStrategy를 작성하고 테스트하는 것입니다. 배선은 통합 테스트 레벨에서 테스트됩니다.


+1 전략 패턴은 질문을 읽은 첫 번째 질문이었습니다.
StuperUser

매우 훌륭하지만 이제는 내 질문에있는 "단위 테스트"를 "통합 테스트"로 바꾸십시오. 우리는 같은 문제로 끝나지 않습니까? IHerbivoreEater계약이지만 테스트에 필요한만큼만 계약합니다. 오리 타이핑이 실제로 도움이되는 경우 인 것 같습니다. 그냥 둘 다 동일한 테스트 로직으로 보내려고합니다. 인터페이스가 동작을 약속해야한다고 생각하지 않습니다. 테스트는 그렇게해야합니다.
Scott Whitlock

좋은 질문, 좋은 답변입니다. 개인 생성자가 필요하지 않습니다. IoC 컨테이너를 사용하여 IHerbivoreEatingStrategy를 StandardHerbivoreEatingStrategy에 연결할 수 있습니다.
azheglov

@ScottWhitlock : "인터페이스가 동작을 약속해야한다고 생각하지 않습니다. 테스트는 그렇게해야합니다." 그것이 바로 내가 말하는 것입니다. 그것이 행동을 약속한다면, 그것을 제거하고 (기본) 클래스를 사용해야합니다. 테스트에는 전혀 필요하지 않습니다.
pdr

@ azheglov : 동의하지만 내 대답은 이미 충분히 길었다 :)
pdr

1

동일한 코드를 여러 번 테스트하는 것은 불필요한 것처럼 보이지만 하위 클래스가 기본 클래스의 기능을 무시하고 무시할 수있는 경우가 있습니다.

몇 가지 테스트를 생략하기 위해 화이트 박스 지식을 사용하는 것이 적절한 지 묻습니다. 블랙 박스의 관점에서, Lion그리고 Tiger다른 클래스입니다. 따라서 코드에 익숙하지 않은 사람이 코드를 테스트하지만 구현 지식이 풍부한 사람은 단순히 한 동물을 테스트하면 도망 칠 수 있다는 것을 알고 있습니다.

단위 테스트를 개발하는 이유 중 하나 나중에 리팩터링 할 수 있지만 동일한 블랙 박스 인터페이스를 유지하기 위해서 입니다. 단위 테스트는 클래스가 고객과의 계약을 계속 충족 시키거나 최소한 계약 변경 방법을주의 깊게 인식하고 생각하도록 도와줍니다. 당신은 자신을 알고 Lion있거나 나중에 언젠가 Tiger재정의 할 수 있습니다 Eat. 이것이 원격으로 가능하다면, 당신이 지원하는 각 동물이 먹을 수있는 간단한 단위 테스트 테스트 :

[TestMethod]
public void Tiger_can_eat_herbivore()
{
    IHerbivoreEater_can_eat_herbivore(BuildTigerForTest);
}

수행하기에 충분하고 충분해야하며 개체가 계약을 충족하지 못하는시기를 감지 할 수 있습니다.


이 질문이 실제로 블랙 박스 대 화이트 박스 테스트의 선호로 귀결되는지 궁금합니다. 나는 블랙 박스 캠프에 기대기 때문에 아마도 내가 시험해 보는 이유 일 것이다. 지적 해 주셔서 감사합니다.
Scott Whitlock

1

당신은 올바르게하고 있습니다. 단위 테스트는 새 코드를 한 번만 사용하는 동작을 테스트하는 것으로 생각하십시오. 이것은 프로덕션 코드에서와 동일한 호출입니다.

이 상황에서는 완전히 정확합니다. Lion 또는 Tiger 사용자는 HerbivoreEaters이며 해당 메소드에 대해 실제로 실행되는 코드가 기본 클래스에서 공통적임을 신경 쓰지 않아도됩니다 (적어도). 마찬가지로 HerbivoreEater 초록 사용자 (구체적인 Lion 또는 Tiger에서 제공)는 사용자가 가지고있는 것을 신경 쓰지 않습니다. 그들이 관심을 갖는 것은 그들의 사자, 호랑이 또는 알 수없는 HerbivoreEater 구현으로 초식 동물을 올바르게 먹을 수 있다는 것입니다.

여러분이 기본적으로 테스트하는 것은 사자가 의도 한대로 먹고 호랑이가 의도 한대로 먹는 것입니다. 둘 다 정확히 같은 방식으로 먹는다는 것이 항상 사실이 아니기 때문에 둘 다를 테스트하는 것이 중요합니다. 두 가지를 모두 테스트함으로써, 변경하고 싶지 않은 것이 변경되지 않았는지 확인하십시오. 이것들은 둘 다 정의 된 HerbivoreEaters이기 때문에, 적어도 Cheetah를 추가 할 때까지 모든 HerbivoreEaters가 의도 한대로 먹을 것인지 테스트했습니다. 귀하의 테스트는 코드를 완벽하게 다루고 적절하게 연습합니다 (허비 보어를 먹는 허 비어에 터의 결과에 대한 모든 예상 된 주장을하는 경우).

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.