Чи слід перевірити успадковані методи?


30

Припустимо , у мене є клас менеджера , отриманий з базового класу Employee , і що службовець має метод getEmail () , яка успадковується диспетчера . Чи слід перевірити, що поведінка методу getEmail () менеджера насправді така сама, як у працівника?

На час написання цих тестів поведінка буде однаковою, але, звичайно, в якийсь момент в майбутньому хтось може перекрити цей метод, змінити його поведінку і, таким чином, порушити мою заяву. Однак, по суті, здається трохи дивним тест на відсутність коду, що перемішується.

(Зверніть увагу, що метод тестування Manager :: getEmail () не покращує охоплення коду (або взагалі будь-яких інших показників якості коду (?)), Поки Manager :: getEmail () не буде створено / перекрито.)

(Якщо відповідь - "Так", корисною буде деяка інформація про те, як керувати тестами, які поділяються між базовими та похідними класами.)

Еквівалентна постановка питання:

Якщо похідний клас успадковує метод від базового класу, як ви виражаєте (тестуєте), чи очікуєте ви, що спадковий метод:

  1. Поведіть точно так само, як це робить база зараз (якщо поведінка бази змінюється, поведінка похідного методу не має);
  2. Поведіть точно так само, як і базовий за весь час (якщо поведінка базового класу змінюється, змінюється і поведінка похідного класу); або
  3. Поведіть, проте це хоче (вам не байдуже поведінка цього методу, тому що ви його ніколи не називаєте).

1
ІМО, похідне від Managerкласу, Employeeбуло першою великою помилкою.
CodesInChaos

4
@CodesInChaos Так, це може бути поганим прикладом. Але та сама проблема стосується кожного разу, коли у вас є спадщина.
міс

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

Я б.

Якщо ви думаєте "добре, це дійсно лише дзвонити співробітнику :: getEmail (), тому що я не перекриваю це, тому мені не потрібно тестувати менеджера :: getEmail ()", то ви не дуже перевіряєте поведінку менеджера :: getEmail ().

Я б подумав про те, що повинен робити тільки менеджер :: getEmail (), а не передавати його у спадок чи замінювати. Якщо поведінка менеджера :: getEmail () повинна повернути те, що повертається Employee :: getMail (), то це тест. Якщо в поведінці потрібно повернути "pink@unicorns.com", то це тест. Не має значення, чи реалізовано це у спадок чи перекрито.

Що важливо, це те, що якщо він зміниться в майбутньому, ваш тест виявить це, і ви знаєте, що щось або зламане, або його потрібно переглянути.

Деякі люди можуть не погодитись із начебто надмірністю у чеку, але моїм протидією цьому буде те, що ви тестуєте поведінку Співробітник :: getMail () та Менеджер :: getMail () як окремі методи, незалежно від того, передаються вони чи ні переосмислений Якщо майбутньому розробнику потрібно змінити поведінку Manager :: getMail (), вони також повинні оновити тести.

Думки можуть різнитися, але я думаю, що Діггер та Хайнці дали обґрунтовані обґрунтування протилежного.


1
Мені подобається аргумент, що поведінкове тестування є ортогональним способом побудови класу. Однак здається трохи смішним повністю ігнорувати спадщину. (А як вам керувати спільними тестами, якщо у вас багато спадку?)
mjs

1
Добре кажучи. Просто тому, що менеджер використовує успадкований метод, ні в якому разі не означає, що його не слід перевіряти. Один мій великий повідник одного разу сказав: "Якщо його не перевіряють, він зламається". З цією метою, якщо ви зробите тести для Співробітника та Менеджера, необхідні під час реєстрації коду, ви будете впевнені, що розробник, перевіряючи новий код для менеджера, який може змінити поведінку успадкованого методу, виправить тест, щоб відобразити нову поведінку . Або приходьте стукати у ваші двері.
ураганМіч

2
Ви дуже хочете перевірити клас менеджера. Те, що це підклас Співробітника, і що getMail () реалізується, спираючись на спадкування, - це лише детальна інформація про впровадження, яку слід ігнорувати, створюючи тести одиниць. Наступного місяця ви дізнаєтесь, що успадкування Менеджера від Співробітника було поганою ідеєю, і ви замінюєте всю структуру спадкування та повторно застосовуєте всі методи. Ваші тестові пристрої повинні продовжувати тестувати ваш код без проблем.
gnasher729

5

Не перевіряйте його. Зробіть функціональний / приймальний тест.

Блок тестів повинен перевірити кожну реалізацію, якщо ви не пропонуєте нову реалізацію, тоді дотримуйтесь принципу DRY. Якщо ви хочете витратити трохи зусиль тут, ви можете покращити оригінальний тест одиниці. Тільки якщо ви перекриєте метод, ви повинні написати тест на одиницю.

У той же час функціональне / приймальне тестування повинно переконатися, що в кінці дня весь ваш код виконує те, що належить, і, сподіваємось, вловлює будь-які дивацтва від успадкування.


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, що в основному є всіма ними.
міс

Це абсолютно правильна відповідь - я чув, що вона називається "абстрактний тестовий зразок". Чому з цікавості слід використовувати вкладений клас, а не просто змішувати тести через class ManagerTests : ExployeeTests? (тобто дзеркальне відображення успадкованості тестованих класів.) Це в основному естетика, щоб допомогти в перегляді результатів?
Люк Ушервуд,

2

Чи варто перевірити, що поведінка методу getEmail () менеджера насправді така сама, як і працівника?

Я б сказав «ні», тому що це був би повторний тест, на мою думку, я би перевірив один раз на тестах працівника, і це було б все.

Під час написання цих тестів поведінка буде однаковою, але, звичайно, в якийсь момент в майбутньому хтось може перекрити цей метод, змінити його поведінку

Якщо метод перекрито, вам знадобляться нові тести, щоб перевірити переохоплену поведінку. Це робота особи, яка реалізує переважаючий getEmail()метод.


1

Ні, вам не потрібно тестувати успадковані методи. Класи та їхні тестові випадки, які покладаються на цей метод, все одно порушаться, якщо поведінка змінилася Manager.

Подумайте про наступний сценарій: Адреса електронної пошти зібрана як Ім'я.Іменне@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()тощо у кожному класі.


Ви не повинні бути розумними з одиничними тестами. Ви кажете: "Мені не потрібно тестувати Х, бо коли X не вдається, Y не вдається". Але одиничні тести базуються на припущеннях про помилки у вашому коді. З помилками у коді, чому ви вважаєте, що складні відносини між тестами працюють так, як ви очікуєте їх роботи?
gnasher729

@ gnasher729 Я кажу: "Мені не потрібно тестувати X в підкласі, тому що він вже перевірений в одиничних тестах для суперкласу ". Звичайно, якщо ви зміните реалізацію X, вам доведеться написати відповідні тести для неї.
Uooo
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.