Должен ли я проверить унаследованные методы?

30

Предположим, у меня есть класс Manager, производный от базового класса Employee , и у этого Employee есть метод getEmail (), который наследуется Manager . Должен ли я проверить, что поведение метода getEmail () менеджера на самом деле такое же, как и у сотрудника?

На момент написания этих тестов поведение будет таким же, но, конечно, в какой-то момент в будущем кто-то может переопределить этот метод, изменить его поведение и, следовательно, сломать мое приложение. Тем не менее, кажется немного странным по существу проверять отсутствие вмешательства кода.

(Обратите внимание, что тестирование метода Manager :: getEmail () не улучшает покрытие кода (или даже любые другие показатели качества кода (?)) До тех пор, пока Manager :: getEmail () не будет создан / переопределен.)

(Если ответ «Да», будет полезна некоторая информация об управлении тестами, которые разделены между базовым и производным классами.)

Эквивалентная формулировка вопроса:

Если производный класс наследует метод от базового класса, как вы выражаете (тестируете), ожидаете ли вы, что унаследованный метод:

  1. Вести себя точно так же, как сейчас делает база (если поведение базы меняется, поведение производного метода не меняется);
  2. Вести все время точно так же, как и базовый (если меняется поведение базового класса, меняется и поведение производного класса); или
  3. Ведите, однако, то, что хотите (вас не волнует поведение этого метода, потому что вы никогда его не вызываете).
MJS
источник
1
Вывод Managerкласса из IMO Employeeбыл первой серьезной ошибкой.
CodesInChaos
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отдельно может иметь смысл.

Heinzi
источник
14

Я мог бы.

Если вы думаете «ну, это действительно только вызов Employee :: getEmail (), потому что я не переопределяю его, поэтому мне не нужно тестировать Manager :: getEmail ()», тогда вы на самом деле не тестируете поведение менеджера :: getEmail ().

Я бы подумал о том, что должен делать только Manager :: getEmail (), а не о том, наследуется он или нет. Если поведение Manager :: getEmail () должно возвращать то, что возвращает Employee :: getMail (), то это тест. Если поведение возвращает «pink@unicorns.com», то это тест. Неважно, реализовано ли оно по наследству или переопределено.

Имеет значение то, что, если это изменится в будущем, ваш тест поймает его, и вы знаете, что что-то либо сломалось, либо нуждается в пересмотре.

Некоторые люди могут не согласиться с кажущейся избыточностью в проверке, но я бы сказал, что вы проверяете поведение Employee :: getMail () и Manager :: getMail () как отдельные методы, независимо от того, наследуются они или нет. переопределяется. Если будущему разработчику необходимо изменить поведение Manager :: getMail (), ему также необходимо обновить тесты.

Мнения могут отличаться, хотя, я думаю, Диггер и Хайнци дали разумные основания для обратного.

seggy
источник
1
Мне нравится аргумент, что поведенческое тестирование ортогонально тому, как класс создается. Тем не менее, кажется немного забавным полностью игнорировать наследование. (А как вам управлять общими тестами, если у вас много наследства?)
mjs
1
Хорошо сказано. То, что Manager использует унаследованный метод, никоим образом не означает, что его не следует проверять. Один мой великий лидер однажды сказал: «Если он не проверен, он сломан». С этой целью, если вы выполните тесты для Employee и Manager, необходимые для регистрации кода, вы будете уверены, что разработчик, проверяющий новый код для Manager, который может изменить поведение унаследованного метода, исправит тест, чтобы отразить новое поведение , Или постучите в вашу дверь.
ураган
2
Вы действительно хотите проверить класс менеджера. Тот факт, что он является подклассом Employee и что getMail () реализован с использованием наследования, - это просто деталь реализации, которую вы должны игнорировать при создании модульных тестов. В следующем месяце вы обнаружите, что наследование Manager от Employee было плохой идеей, и вы заменили всю структуру наследования и переопределили все методы. Ваши юнит-тесты должны продолжить тестирование вашего кода без проблем.
gnasher729
5

Не проверяйте это модульно. Сделайте функциональные / приемочные испытания.

Модульные тесты должны проверять каждую реализацию, если вы не предоставляете новую реализацию, придерживайтесь принципа СУХОЙ. Если вы хотите потратить немного усилий здесь, вы можете улучшить исходный юнит-тест. Только если вы переопределите метод, вы должны написать модульный тест.

В то же время, функциональное / приемочное тестирование должно удостовериться, что в конце дня весь ваш код делает то, что он должен, и, мы надеемся, поймает любую странность из наследования.

MrFox
источник
4

Правила Роберта Мартина для TDD :

  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);
        }
    }
}

Причина, по которой я могу это сделать, - это принцип подстановки Лискова: тесты по- Employeeпрежнему должны проходить при прохождении Managerобъекта, поскольку он является производным Employee. Поэтому я должен написать свои тесты только один раз, и могу убедиться, что они работают для всех возможных реализаций интерфейса или базового класса.

Даниэль А.А. Пельсмакер
источник
Хороший вопрос в отношении принципа подстановки Лискова, вы правы в том, что если это так, производный класс пройдет все тесты базового класса. Однако LSP часто нарушается, в том числе и самим методом xUnit setUp()! И почти каждая веб-инфраструктура MVC, которая включает в себя переопределение «индексного» метода, также ломает LSP, который в основном все из них.
MJS
Это абсолютно правильный ответ - я слышал, что он называется «Абстрактным тестовым шаблоном». Из любопытства, зачем использовать вложенный класс, а не просто смешивать тесты с помощью class ManagerTests : ExployeeTests? (т. е. отражает наследование тестируемых классов.) Это в основном эстетика, чтобы помочь с просмотром результатов?
Люк Ашервуд
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было преодолено. Вы будете расследовать эту проблему и найдете причину. И все без написания отдельного тестового примера для унаследованного метода.

Следовательно, вам не нужно тестировать унаследованные методы.

В противном случае, например, в Java, вам придется тестировать все методы, унаследованные от Objectlike toString()и equals()т. Д. В каждом классе.

UOOO
источник
Вы не должны быть умными с юнит-тестами. Вы говорите: «Мне не нужно тестировать X, потому что, когда X терпит неудачу, Y терпит неудачу». Но модульные тесты основаны на предположениях об ошибках в вашем коде. С ошибками в вашем коде, почему вы думаете, что сложные отношения между тестами работают так, как вы ожидаете?
gnasher729
@ gnasher729 Я говорю: «Мне не нужно тестировать X в подклассе, потому что он уже тестировался в модульных тестах для суперкласса ». Конечно, если вы измените реализацию X, вы должны написать соответствующие тесты для него.
Uooo