상속 된 메소드를 테스트해야합니까?


30

기본 클래스 Employee 에서 파생 된 클래스 Manager 가 있고 Employee 에 Manager 가 상속 한 getEmail () 메소드가 있다고 가정하십시오 . 관리자의 getEmail () 메소드 의 동작이 실제로 직원의 동작과 동일한 지 테스트해야합니까 ?

이러한 테스트가 작성 될 때의 동작은 동일하지만, 앞으로 어느 시점에서 누군가이 방법을 무시하고 동작을 변경하여 응용 프로그램을 중단 할 수 있습니다. 그러나 중간 코드 가 없는지 본질적으로 테스트하는 것은 약간 이상 합니다.

( Manager :: getEmail () 메서드 를 테스트 해도 Manager :: getEmail () 을 만들거나 재정의 할 때까지 코드 적용 범위 (또는 실제로 다른 코드 품질 메트릭 (?))가 향상되지 않습니다 .

대답이 "예"인 경우 기본 클래스와 파생 클래스간에 공유되는 테스트 관리 방법에 대한 일부 정보가 유용합니다.

질문의 동등한 공식 :

파생 클래스가 기본 클래스에서 메서드를 상속받는 경우 상속 된 메서드가 다음을 수행 할 것으로 기대하는지 여부를 어떻게 표현 (테스트)합니까?

  1. 베이스가 지금하는 것과 정확히 같은 방식으로 동작합니다 (베이스의 동작이 바뀌더라도 파생 된 메소드의 동작은 변하지 않습니다).
  2. 모든 시간 동안베이스와 정확히 같은 방식으로 행동하십시오 (기본 클래스의 동작이 변경되면 파생 클래스의 동작도 변경됨). 또는
  3. 그러나 원하는대로 동작하십시오 (이 메소드는 호출하지 않기 때문에이 메소드의 동작에 신경 쓰지 않습니다).

1
Manager수업에서 파생 된 IMO Employee는 첫 번째 주요 실수였습니다.
코드 InChaos

4
@CodesInChaos 예, 좋지 않은 예일 수 있습니다. 그러나 상속받을 때마다 같은 문제가 적용됩니다.
mjs

1
메소드를 재정의 할 필요조차 없습니다. 기본 클래스 메서드는 재정의 된 다른 인스턴스 메서드를 호출 할 수 있으며 메서드에 대해 동일한 소스 코드를 실행하면 여전히 다른 동작이 발생합니다.
gnasher729

답변:


23

실용적인 방법을 여기에서 사용하겠습니다. 장래 누군가 누군가 Manager :: getMail을 재정의 하는 경우 새 방법에 대한 테스트 코드를 제공 하는 것은 개발자의 책임입니다.

물론 Manager::getEmail실제로 동일한 코드 경로를 가진 경우에만 유효합니다 Employee::getEmail! 메서드를 재정의하지 않더라도 다르게 동작 할 수 있습니다.

  • Employee::getEmail일부 보호 된 가상 호출 할 수 getInternalEmail있는 됩니다 에서 재정의를 Manager.
  • Employee::getEmail일부 내부 상태 (예를 들어, 일부 필드에 액세스 할 수 있습니다 _email예를 들어, 기본 구현은 다음 두 가지 구현 다를 수 있음), Employee즉 보장 할 수 _email항상 firstname.lastname@example.com있지만 Manager메일 주소를 할당에서 더 유연합니다.

이러한 경우, 메소드 자체Manager::getEmail 의 구현 이 동일 하더라도 버그는에서 만 나타날 수 있습니다 . 이 경우 별도로 테스트 하는 것이 좋습니다.Manager::getEmail


14

그렇습니다.

"잘 모르겠다면, Employee :: getEmail ()을 호출하지 않기 때문에 실제로는 Employee :: getEmail ()을 호출하는 것이므로 Manager :: getEmail ()을 테스트 할 필요가 없습니다." Manager :: getEmail ()의

상속 또는 재정의 여부와 상관없이 Manager :: getEmail () 만 수행해야 할 작업을 생각합니다. Manager :: getEmail ()의 동작이 Employee :: getMail ()이 리턴하는 모든 것을 리턴해야하는 경우, 이것이 테스트입니다. 동작이 "pink@unicorns.com"을 반환하는 것이라면 이것이 테스트입니다. 상속에 의해 구현되는지 또는 재정의되었는지는 중요하지 않습니다.

중요한 것은 미래에 변화가 생길 경우 테스트를 통해 문제가 발생하고 무언가가 깨 졌거나 재고가 필요하다는 것을 알고 있다는 것입니다.

일부 사람들은 검사에서 중복성이 보이는데 동의하지 않을 수도 있지만, 내 의견은 직원 :: getMail () 및 Manager :: getMail () 동작을 상속되는지 여부에 관계없이 고유 한 방법으로 테스트하는 것입니다. 재정의되었습니다. 향후 개발자가 Manager :: getMail ()의 동작을 변경해야하는 경우 테스트도 업데이트해야합니다.

의견은 다를 수 있지만, Digger와 Heinzi는 그 반대에 대한 합리적인 근거를 제시했다고 생각합니다.


1
나는 행동 테스트가 클래스가 구성되는 방식과 직교한다는 주장을 좋아합니다. 그러나 상속을 완전히 무시하는 것은 약간 재미있어 보입니다. (그리고 상속이 많은 경우 공유 테스트를 관리하는 방법은 무엇입니까?)
mjs

1
잘 넣어 Manager가 상속 된 메소드를 사용하고 있다고해서 절대 테스트되지 않아야한다는 의미는 아닙니다. 내 위대한 지도자는 한 번 "테스트되지 않으면 고장났습니다"라고 말했습니다. 이를 위해 코드 체크인에 필요한 Employee 및 Manager에 대한 테스트를 수행하는 경우 상속 된 메소드의 동작을 변경할 수있는 Manager의 새 코드를 체크인하는 개발자가 새 동작을 반영하도록 테스트를 수정해야합니다. . 아니면 문을 두 드리십시오.
hurricaneMitch

2
실제로 Manager 클래스를 테스트하려고합니다. 이것이 직원의 서브 클래스이고 상속에 의존하여 getMail ()이 구현된다는 사실은 단위 테스트를 작성할 때 무시해야하는 구현 세부 사항 일뿐입니다. 다음 달에는 Employee의 상속 관리자가 나쁜 아이디어임을 알게되었으며 전체 상속 구조를 바꾸고 모든 메서드를 다시 구현했습니다. 단위 테스트는 문제없이 코드를 계속 테스트해야합니다.
gnasher729

5

단위 테스트하지 마십시오. 기능 / 수락 테스트를 수행하십시오.

단위 테스트는 모든 구현을 테스트해야합니다. 새로운 구현을 제공하지 않으면 DRY 원칙을 따르십시오. 여기에 약간의 노력을 기울이고 싶다면 원래 단위 테스트를 향상시킬 수 있습니다. 메소드를 대체하는 경우에만 단위 테스트를 작성해야합니다.

동시에, 기능 / 수락 테스트는 하루가 끝날 때 모든 코드가 예상대로 작동하고 상속에서 이상한 점을 잡을 수 있는지 확인해야합니다.


4

TDD대한 Robert Martin의 규칙 은 다음과 같습니다.

  1. 실패한 단위 테스트를 통과하지 않으면 프로덕션 코드를 작성할 수 없습니다.
  2. 실패하기에 충분한 단위 테스트를 더 이상 작성할 수 없습니다. 컴파일 실패는 실패입니다.
  3. 실패한 단위 테스트를 통과하기에 충분한 양보다 더 많은 생산 코드를 작성할 수 없습니다.

이 규칙을 준수하면이 질문을 할 수 없습니다. getEmail방법을 테스트해야하는 경우 테스트 된 것입니다.


3

그렇습니다. 상속 된 메서드는 나중에 재정의 있으므로 테스트해야합니다 . 또한 상속 된 메서드 는 재정의 된 가상 메서드를 호출 하여 재정의되지 않은 상속 된 메서드의 동작을 변경할 수 있습니다.

이것을 테스트하는 방법은 다음과 같이 (아마도 추상적 인) 기본 클래스 또는 인터페이스를 테스트하기 위해 추상 클래스를 만드는 것입니다 (C #에서 NUnit 사용).

public abstract class EmployeeTests
{
    protected abstract Employee CreateInstance(string name, int age);

    [Test]
    public void GetEmail_ReturnsValidEmailAddress()
    {
        // Given
        var sut = CreateInstance("John Doe", 20);

        // When
        string email = sut.GetEmail();

        // Then
        Assert.IsTrue(Helper.IsValidEmail(email));
    }
}

그런 다음에 고유 한 테스트 클래스가 Manager있으며 직원의 테스트를 다음과 같이 통합합니다.

[TestFixture]
public class ManagerTests
{
    // Other tests.

    [TestFixture]
    public class ManagerEmployeeTests : EmployeeTests
    {
        protected override Employee CreateInstance(string name, int age);
        {
            return new Manager(name, age);
        }
    }
}

내가 그렇게 할 수있는 이유는 Liskov의 대체 원칙입니다. 객체 EmployeeManager객체를 통과 할 때 여전히 통과해야합니다 Employee. 따라서 테스트를 한 번만 작성해야하며 인터페이스 또는 기본 클래스의 가능한 모든 구현에서 작동하는지 확인할 수 있습니다.


좋은 점은 Liskov의 대체 원칙이며, 이것이 유지되면 파생 클래스가 모든 기본 클래스의 테스트를 통과한다는 것이 옳습니다. 그러나 xUnit의 setUp()메소드 자체를 포함하여 LSP가 자주 위반 됩니다! "인덱스"메서드를 재정의하는 거의 모든 웹 MVC 프레임 워크도 LSP를 중단시킵니다. 이는 기본적으로 모든 LSP입니다.
mjs

이것은 정답입니다. "Abstract Test Pattern"이라고 들었습니다. 호기심 때문에 왜 테스트를 믹싱하지 않고 중첩 클래스를 사용 class ManagerTests : ExployeeTests합니까? (즉, 테스트중인 클래스의 상속을 미러링합니다.) 결과를 보는 데 도움이되는 것이 미학입니까?
Luke Usherwood

2

관리자의 getEmail () 메소드의 동작이 실제로 직원의 동작과 동일한 지 테스트해야합니까?

내 의견으로는 반복 테스트이므로 아니오라고 말하고 직원 테스트에서 한 번 테스트하면 그 결과가 될 것입니다.

이러한 테스트가 작성 될 때의 동작은 동일하지만 물론 언젠가 누군가이 방법을 재정의하고 동작을 변경할 수 있습니다.

이 방법을 재정의하는 경우 재정의 된 동작을 확인하려면 새로운 테스트가 필요합니다. 이것이 재정의 getEmail()방법을 구현하는 사람의 일입니다 .


1

아니요, 상속 된 메소드를 테스트 할 필요는 없습니다. 이 방법에 의존하는 클래스와 테스트 사례는에서 동작이 변경되면 중단됩니다 Manager.

다음 시나리오를 생각해보십시오. 이메일 주소는 Firstname.Lastname@example.com으로 구성됩니다.

class Employee{
    String firstname, lastname;
    String getEmail() { 
        return firstname + "." + lastname + "@example.com";
    }
}

당신은 이것을 단위로 테스트했고 당신에게 잘 작동합니다 Employee. 또한 클래스를 만들었습니다 Manager.

class Manager extends Employee { /* nothing different in email generation */ }

이제 ManagerSort이메일 주소를 기준으로 목록에서 관리자를 정렬 하는 클래스 가 있습니다. 귀하의 이메일 생성은 다음과 동일하다고 가정합니다 Employee.

class ManagerSort {
    void sortManagers(Manager[] managerArrayToBeSorted)
        // sort based on email address omitted
    }
}

당신은 당신의 테스트를 작성합니다 ManagerSort:

void testManagerSort() {
    Manager[] managers = ... // build random manager list
    ManagerSort.sortManagers(managers);

    Manager[] expected = ... // expected result
    assertEquals(expected, managers); // check the result
}

모든 것이 잘 작동합니다. 이제 누군가가 와서 getEmail()메소드를 재정의합니다 .

class Manager extends Employee {
    String getEmail(){
        // managers should have their lastname and firstname order changed
        return lastname + "." + firstname + "@example.com";
    }
}

이제 어떻게됩니까? 귀하는 testManagerSort()때문에 실패 getEmail()의이 Manager무시되었다. 이 문제를 조사하고 원인을 찾을 수 있습니다. 그리고 상속 된 메소드에 대한 별도의 테스트 케이스를 작성하지 않아도됩니다.

따라서 상속 된 메소드를 테스트 할 필요가 없습니다.

그렇지 않으면, 예를 들어, 자바, 당신은에서 상속 된 모든 메소드를 테스트 할 것 Object같은 toString(), equals()모든 클래스에 등.


당신은 단위 테스트에 영리하지 않아야합니다. "X가 실패하면 Y가 실패하기 때문에 X를 테스트 할 필요가 없습니다"라고 말합니다. 그러나 단위 테스트는 코드의 버그 가정을 기반으로합니다. 코드에 버그가 있는데 왜 테스트 간의 복잡한 관계가 예상대로 작동한다고 생각하십니까?
gnasher729

@ gnasher729 저는 "서브 클래스에서 X를 테스트 할 필요가 없습니다. 이미 슈퍼 클래스의 단위 테스트에서 테스트 되었기 때문입니다 ." 물론 X 구현을 변경하면 적절한 테스트를 작성해야합니다.
Uooo
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.