Чи добре побудова об'єктів з нульовими параметрами в одиничних тестах?


37

Я почав писати кілька тестів для мого поточного проекту. Але я насправді не маю досвіду з цим. Я спершу хочу повністю "отримати його", тому наразі я не використовую ні свого ІоК-рамки, ні глузуючої бібліотеки.

Мені було цікаво, чи є щось не так у наданні нульових аргументів конструкторам об'єктів в одиничних тестах. Дозвольте навести приклад коду:

public class CarRadio
{...}


public class Motor
{
    public void SetSpeed(float speed){...}
}


public class Car
{
    public Car(CarRadio carRadio, Motor motor){...}
}


public class SpeedLimit
{
    public bool IsViolatedBy(Car car){...}
}

Ще один приклад автомобільного кодексу (TM), зведений лише до важливих для питання частин. Зараз я написав тест приблизно так:

public class SpeedLimitTest
{
    public void TestSpeedLimit()
    {
        Motor motor = new Motor();
        motor.SetSpeed(10f);
        Car car = new Car(null, motor);

        SpeedLimit speedLimit = new SpeedLimit();
        Assert.IsTrue(speedLimit.IsViolatedBy(car));
    }
}

Тест працює нормально. SpeedLimitпотрібен a Carз a Motor, щоб зробити свою справу. Це взагалі не цікавить CarRadio, тому я вказав нуль для цього.

Мені цікаво, чи об’єкт, що забезпечує правильну функціональність, не будуючи повністю, є порушенням SRP або запахом коду. У мене таке нудотне відчуття, що воно є, але speedLimit.IsViolatedBy(motor)не відчуває себе правильно - обмеження швидкості порушується автомобілем, а не мотором. Можливо, мені просто потрібна інша перспектива для одиничних тестів проти робочого коду, тому що весь намір полягає в тестуванні лише частини цілого.

Чи побудова об'єктів з нульовими одиницями тестів кодовим запахом?


24
Трохи поза темою, але, Motorмабуть, не повинно бути speedвзагалі. Він повинен мати throttleі обчислювати на torqueоснові струму rpmі throttle. Завдання автомобіля - використовувати Transmissionінтегрувати це в поточну швидкість, і перетворити це rpmна сигнал, щоб повернутися до Motor... Але я думаю, ви все одно не були для цього реалізмом, чи не так?
cmaster

17
Запах коду полягає в тому, що ваш конструктор знімає нуль, не нарікаючи в першу чергу. Якщо ви вважаєте, що це прийнятно (у вас можуть бути причини цього): продовжуйте, протестуйте!
Томмі

4
Розглянемо шаблон нульового об’єкта . Він часто спрощує код, роблячи його також безпечним для NPE.
Бакуріу

17
Вітаємо : ви перевірили, що за допомогою nullрадіо правильно обчислюється обмеження швидкості. Тепер ви можете створити тест для перевірки обмеження швидкості за допомогою радіо; на випадок, якщо поведінка відрізняється ...
Матьє М.

3
Не впевнений у цьому прикладі, але іноді той факт, що ви хочете лише перевірити половину класу, - це підказка, що щось слід порушити
Оуен

Відповіді:


70

У випадку з наведеним вище прикладом, доцільно, що Carможе існувати без CarRadio. У такому випадку я б сказав, що не тільки прийнятно CarRadioіноді пропускати нуль , я б сказав, що це обов'язково. Ваші тести повинні переконатися, що реалізація Carкласу є надійною та не викидає нульові винятки вказівників, коли таких немає CarRadio.

Однак візьмемо інший приклад - розглянемо а SteeringWheel. Припустимо, що це Carмає бути SteeringWheel, але тест швидкості насправді це не хвилює. У цьому випадку я не передавав би нуль, SteeringWheelоскільки це потім виштовхує Carклас у місця, де він не призначений для переходу. У цьому випадку вам краще створити якийсь тип DefaultSteeringWheel(для продовження метафори) зафіксований у прямій лінії.


44
І не забудьте написати одиничний тест, щоб перевірити, що Carнеможливо побудувати без SteeringWheel...
cmaster

13
Можливо, мені щось не вистачає, але якщо це можливо і навіть звичайно створити Carбез CarRadio, чи не має сенсу створювати конструктор, який не потребує передачі, і зовсім не потрібно турбуватися про явну нуль ?
Earwig

16
Автомобільні машини, які керують авто, можуть не мати керма. Просто кажу. ☺
BlackJack

1
@James_Parsons Я забув додати, що мова йшла про клас, який не мав нульових перевірок, оскільки він використовувався лише внутрішньо і завжди отримував ненульові параметри. Інші методи Carкинули б NRE з нулем CarRadio, але відповідний код для SpeedLimitцього не буде. Що стосується питання, яке ви поставили зараз, ви будете правильні, і я б дійсно це зробив.
Р. Шмітц

2
@mtraceur Те, як усі припускали, що клас, що дозволяє побудувати з аргументом null, означає, що null є дійсним варіантом, змусив мене зрозуміти, що я, мабуть, повинен мати там нульові перевірки. Справжнє питання, яке я мав би тоді, висвітлюється безліччю хороших, цікавих, добре написаних відповідей, які я не хочу зруйнувати і які мені допомогли. Також приймаю цю відповідь зараз, бо мені не подобається залишати речі незакінченими, і це найбільше цінується (хоча всі вони були корисні).
Р. Шмітц

23

Чи побудова об'єктів з нульовими одиницями тестів кодовим запахом?

Так. Зверніть увагу на визначення C2 запаху коду : "CodeSmell - це натяк на те, що щось може бути не так, а не впевненість".

Тест працює нормально. SpeedLimit потребує Автомобіля з Мотором, щоб зробити свою справу. Це взагалі не цікавить CarRadio, тому я вказав нуль для цього.

Якщо ви намагаєтесь продемонструвати, що speedLimit.IsViolatedBy(car)не залежить від CarRadioаргументу, переданого конструктору, то, мабуть, буде зрозуміліше майбутньому технічному обслуговувачу зробити це обмеження явним шляхом введення тестового подвійного, який не дає змоги тесту, якщо він буде викликаний.

Якщо, як це більше ймовірно, ви просто намагаєтесь зберегти себе роботою по створенню того, CarRadioщо ви знаєте, що не використовується в цьому випадку, ви повинні помітити це

  • це порушення інкапсуляції (ви випускаєте CarRadioтест, який не використовується, витікає в тест)
  • ви виявили принаймні один випадок, коли ви хочете створити Carбез вказівки aCarRadio

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

public Car(IMotor motor) {...}

і тоді той конструктор може вирішити, що робити з тим, що його немаєCarRadio

public Car(IMotor motor) {
    this(null, motor);
}

або

public Car(IMotor motor) {
    this( new ICarRadio() {...} , motor);
}

або

public Car(IMotor motor) {
    this( new DefaultRadio(), motor);
}

тощо.

Йдеться про те, якщо при тестуванні SpeedLimit передавати null щось інше. Ви можете бачити, що тест називається "SpeedLimitTest", і єдиний Assert перевіряє метод SpeedLimit.

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

Це може бути підказка, що SpeedLimit.isViolatedByслід приймати рольовий інтерфейс, який реалізує автомобіль , а не вимагати цілого автомобіля.

interface IHaveSpeed {
    float speed();
}

class Car implements IHaveSpeed {...}

IHaveSpeed- справді паршиве ім’я; сподіваємось, мова вашого домену покращиться.

Якщо цей інтерфейс на місці, ваш тест для SpeedLimitможе бути набагато простішим

public void TestSpeedLimit()
{
    IHaveSpeed somethingTravelingQuickly = new IHaveSpeed {
        float speed() { return 10f; }
    }

    SpeedLimit speedLimit = new SpeedLimit();
    Assert.IsTrue(speedLimit.IsViolatedBy(somethingTravelingQuickly));
}

У вашому останньому прикладі я припускаю, що вам доведеться створити макет-клас, який реалізує IHaveSpeedінтерфейс, оскільки (принаймні, в c #) ви не зможете інстанціювати сам інтерфейс.
Райан Серл

1
Це правильно - ефективне написання буде залежати від того, який аромат Blub ви використовуєте для своїх тестів.
VoiceOfUnreason

18

Чи побудова об'єктів з нульовими одиницями тестів кодовим запахом?

Ні

Зовсім ні. Навпаки, якщо NULLє значення, яке доступне в якості аргументу (деякі мови можуть мати конструкції, що забороняють передачу нульового вказівника), слід перевірити його.

Інше питання - що відбувається, коли ти проходиш NULL. Але саме в цьому полягає одиничний тест: переконайтеся, що визначений результат відбувається при кожному можливому введенні. І NULL це потенційний внесок, тому у вас повинен бути визначений результат, і ви повинні мати тест на це.

Тож якщо ваш Автомобіль повинен бути сконструйований без радіо, вам слід написати тест на це. Якщо це не так і передбачається виняток, вам слід написати тест.

Так чи інакше, так, ви повинні написати тест на NULL. Це було б запахом коду, якби ви його не опустили. Це означатиме, що у вас є неперевірена гілка коду, яку ви просто припустили, що магічно не буде помилок.


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


Схоже, ви пишете Carтут тестування . Хоча я нічого не бачу, я вважаю неправильним твердження, питання стосується тестування SpeedLimit.
Р. Шмітц

@ R.Schmitz Не впевнений, що ти маєш на увазі. Я цитував питання, це стосується переходу NULLдо конструктора Car.
nvoigt

1
Йдеться про те, якщо під час тестуванняSpeedLimit передавати null щось інше . Ви можете бачити, що тест називається "SpeedLimitTest", і єдине Assert- це перевірка методу SpeedLimit. Тестування Car- наприклад, "якщо ваш Автомобіль може бути сконструйований без радіо, ви повинні написати тест на це" - відбудеться в іншому тесті.
Р. Шмітц

7

Я особисто не вагаюся вводити нулі. Насправді, кожного разу, коли я ввожу залежності в конструктор, однією з перших речей, яку я роблю, є підняття ArgumentNullException, якщо ці ін'єкції є недійсними. Таким чином я можу спокійно використовувати їх у своєму класі, без десятків недійсних перевірок, рознесених навколо. Ось дуже поширена закономірність. Зверніть увагу на використання лише для читання, щоб ці залежності, встановлені в конструкторі, не могли бути встановлені на нуль (або що-небудь інше) після:

class Car
{
   private readonly ICarRadio _carRadio;
   private readonly IMotor _motor;

   public Car(ICarRadio carRadio, IMotor motor)
   {
      if (carRadio == null)
         throw new ArgumentNullException(nameof(carRadio));

      if (motor == null)
         throw new ArgumentNullException(nameof(motor));

      _carRadio = carRadio;
      _motor = motor;
   }
}

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

Сказавши це, це стає проблемою, коли ви зробите дві речі:

  1. Програма на інтерфейси замість класів.
  2. Використовуйте глузуючий фреймворк (як Moq для C #), щоб вводити знущаються залежності замість нулів.

Отже, для вашого прикладу у вас є:

interface ICarRadio
{
   bool Tune(float frequency);
}

interface Motor
{
   bool SetSpeed(float speed);
}

...
var car = new Car(new Moq<ICarRadio>().Object, new Moq<IMotor>().Object);

Детальніше про використання Moq можна ознайомитись тут .

Звичайно, якщо ви хочете зрозуміти це, не спершу знущаючись, ви все одно можете це зробити, якщо ви використовуєте інтерфейси. Просто створіть манекен CarRadio і Motor наступним чином та вставте їх замість цього:

class DummyCarRadio : ICarRadio
{
   ...
}
class DummyMotor : IMotor
{
   ...
}

...
var car = new Car(new DummyCarRadio(), new DummyMotor())

3
Це відповідь, яку я написав би
Ліат

1
Я б навіть зайшов, щоб сказати, що манекени також повинні виконати свій договір. Якби IMotorбув getSpeed()метод, DummyMotorпотрібно було б також повернути змінену швидкість і після успішного setSpeed.
ComFreek

1

Це не просто гаразд, це добре відома техніка розбиття залежності для отримання застарілого коду в тестову заправку. (Див. "Ефективна робота зі застарілим кодексом" Майкла Пірса.)

Пахне? Добре...

Тест не пахне. Тест робить свою справу. Він оголошує, що "цей об'єкт може бути недійсним, і тестовий код все ще працює правильно".

Запах в реалізації. Заявляючи аргумент конструктора, ви заявляєте, що "цьому класу потрібна ця річ, щоб правильно функціонувати". Очевидно, що це неправда. Все неправдиве трохи смердюче.


1

Давайте зробимо крок назад до перших принципів.

Тест одиниці тестує деякий аспект одного класу.

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

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

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

Якщо все це здасться нам значним, ви фактично вже використовуєте манекен, передаючи NULL в конструктор Авто, оскільки це значення, яке потенційно не буде використане.

Що має стати зрозумілим досить швидко, - це те, що ви потенційно запускаєте свій код з великою кількістю обробки NULL. Якщо ви замінюєте параметри класу інтерфейсами, ви також прокладаєте шлях для глузування та DI тощо, що робить їх набагато простішими в обробці.


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

3
трактування "одиниці" як евфемізму для "класу", на жаль, є дуже поширеним способом мислення, але насправді все це робить (а) заохочення тестів "сміття, сміття", що може повністю підірвати обгрунтованість цілого тесту пакети та (b) застосовують межі, які повинні вільно змінюватися, що може калічити продуктивність. Я б хотів, щоб більше людей слухали їхній біль і оскаржували цю задуму.
Мураха P

3
Такої плутанини немає. Тестові одиниці поведінки, а не одиниці коду. Немає нічого про концепцію одиничного тестування - за винятком широко поширеного культавого розуму у тесті програмного тестування - що означає, що тестовий пристрій поєднаний із чітко вираженою концепцією OOP класу. І дуже згубна помилка, яка теж є.
Мураха П

1
Я точно не впевнений, що ти маєш на увазі - я стверджую точну протилежність тому, що ти маєш на увазі. Заглушіть / знудьтеся над жорсткими межами (якщо вам це абсолютно доведеться) і протестуйте поведінкову одиницю. Через загальнодоступний API. Що це за API, залежить від того, що ви будуєте, але слід повинен бути мінімальним. Додавання абстракції, поліморфізму чи коду реструктуризації не повинно спричиняти збій тестів - "одиниця на клас" суперечить цьому і в першу чергу повністю підриває значення тестів.
Мураха P

1
RobbieDee - я кажу про точне протилежне. Подумайте, що ви, можливо, є одним із прихильників поширених помилок, перегляньте те, що ви вважаєте "одиницею", і, можливо, заглибиться трохи глибше, про що насправді говорив Кент Бек, коли говорив про TDD. Те, що зараз більшість людей називає TDD, - це жахливий вантаж. youtube.com/watch?v=EZ05e7EMOLM
Ant Ant

1

З огляду на ваш дизайн, я б запропонував: Car складається з набору Features. Особливість може бути CarRadio, або EntertainmentSystemякий може складатися з речей , як CarRadio, і DvdPlayerт.д.

Отже, як мінімум, вам доведеться побудувати а Carз набором Features. EntertainmentSystemі CarRadioбудуть реалізаторами інтерфейсу (Java, C # тощо), public [I|i]nterface Featureі це буде екземпляром композиційного шаблону дизайну.

Тому ви завжди побудували a CarFeatures тест з, і одиничний тест ніколи б не створив Carбез жодного Features. Хоча ви можете розглянути знущання над тим, BasicTestCarFeatureSetщо має мінімальний набір функціональних можливостей, необхідний для вашого найпростішого тесту.

Просто деякі думки.

Джон

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.