Чи є простіший спосіб перевірити перевірку аргументів та ініціалізацію поля в незмінному об'єкті?


20

Мій домен складається з безлічі простих незмінних класів на кшталт цього:

public class Person
{
    public string FullName { get; }
    public string NameAtBirth { get; }
    public string TaxId { get; }
    public PhoneNumber PhoneNumber { get; }
    public Address Address { get; }

    public Person(
        string fullName,
        string nameAtBirth,
        string taxId,
        PhoneNumber phoneNumber,
        Address address)
    {
        if (fullName == null)
            throw new ArgumentNullException(nameof(fullName));
        if (nameAtBirth == null)
            throw new ArgumentNullException(nameof(nameAtBirth));
        if (taxId == null)
            throw new ArgumentNullException(nameof(taxId));
        if (phoneNumber == null)
            throw new ArgumentNullException(nameof(phoneNumber));
        if (address == null)
            throw new ArgumentNullException(nameof(address));

        FullName = fullName;
        NameAtBirth = nameAtBirth;
        TaxId = taxId;
        PhoneNumber = phoneNumber;
        Address = address;
    }
}

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

Справжнім рішенням було б C # підтримувати незмінні та нерегульовані типи посилань. Але що я можу зробити, щоб покращити ситуацію тим часом? Чи варто написати всі ці тести? Було б гарною ідеєю написати генератор коду для таких класів, щоб уникнути написання тестів для кожного з них?


Ось що я зараз грунтуюся на відповідях.

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

FullName = fullName.ThrowIfNull(nameof(fullName));
NameAtBirth = nameAtBirth.ThrowIfNull(nameof(nameAtBirth));
TaxId = taxId.ThrowIfNull(nameof(taxId));
PhoneNumber = phoneNumber.ThrowIfNull(nameof(phoneNumber));
Address = address.ThrowIfNull(nameof(address));

Використовуючи наступну реалізацію Роберта Харві :

public static class ArgumentValidationExtensions
{
    public static T ThrowIfNull<T>(this T o, string paramName) where T : class
    {
        if (o == null)
            throw new ArgumentNullException(paramName);

        return o;
    }
}

Тестування перевірки невизначеною легко , використовуючи GuardClauseAssertionз AutoFixture.Idioms(спасибі за пропозицію, Esben Skov Pedersen ):

var fixture = new Fixture().Customize(new AutoMoqCustomization());
var assertion = new GuardClauseAssertion(fixture);
assertion.Verify(typeof(Address).GetConstructors());

Це можна стиснути ще більше:

typeof(Address).ShouldNotAcceptNullConstructorArguments();

Використовуючи цей метод розширення:

public static void ShouldNotAcceptNullConstructorArguments(this Type type)
{
    var fixture = new Fixture().Customize(new AutoMoqCustomization());
    var assertion = new GuardClauseAssertion(fixture);

    assertion.Verify(type.GetConstructors());
}

3
Заголовок / тег говорить про (одиничне) тестування значень поля, що передбачає тестування об'єкта після будівництва, але фрагмент коду показує вхідний аргумент / перевірку параметрів; це не однакові поняття.
Ерік Ейдт

2
FYI, ви можете написати шаблон T4, який полегшив би такий вид котлоагрегату. (Ви можете також розглянути string.IsEmpty () поза == null.)
Ерік Ейдт

1
Ви подивилися на ідіоми автофіксації? nuget.org/packages/AutoFixture.Idioms
Есбен Сков Педерсен


1
Ви також можете використовувати Fody / NullGuard , хоча, схоже, у нього немає режиму дозволу за замовчуванням.
Боб

Відповіді:


5

Я створив шаблон t4 саме для таких випадків. Щоб не писати багато табличок для котлів для змінних класів.

https://github.com/xaviergonz/T4Imumutable T4Immutable - це шаблон T4 для додатків C # .NET, який генерує код для змінних класів.

Зокрема, про ненульові тести, якщо ви користуєтесь цим:

[PreNotNullCheck, PostNotNullCheck]
public string FirstName { get; }

Конструктор буде таким:

public Person(string firstName) {
  // pre not null check
  if (firstName == null) throw new ArgumentNullException(nameof(firstName));

  // assignations + PostConstructor() if needed

  // post not null check
  if (this.FirstName == null) throw new NullReferenceException(nameof(this.FirstName));
}

Сказавши це, якщо ви використовуєте Анотації JetBrains для перевірки нуля, ви також можете це зробити:

[JetBrains.Annotations.NotNull, ConstructorParamNotNull]
public string FirstName { get; }

І конструктор буде таким:

public Person([JetBrains.Annotations.NotNull] string firstName) {
  // pre not null check is implied by ConstructorParamNotNull
  if (firstName == null) throw new ArgumentNullException(nameof(firstName));

  FirstName = firstName;
  // + PostConstructor() if needed

  // post not null check implied by JetBrains.Annotations.NotNull on the property
  if (this.FirstName == null) throw new NullReferenceException(nameof(this.FirstName));
}

Також є ще кілька функцій, ніж ця.


Дякую. Я тільки що спробував це, і це спрацювало чудово. Я відзначаю це як прийняту відповідь, оскільки вона вирішує всі питання в оригінальному запитанні.
Botond Balázs

16

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

internal static T ThrowIfNull<T>(this T o, string paramName) where T : class
{
    if (o == null)
        throw new ArgumentNullException(paramName);

    return o;
}

Потім ви можете написати:

public class Person
{
    public string FullName { get; }
    public string NameAtBirth { get; }
    public string TaxId { get; }
    public PhoneNumber PhoneNumber { get; }
    public Address Address { get; }

    public Person(
        string fullName,
        string nameAtBirth,
        string taxId,
        PhoneNumber phoneNumber,
        Address address)
    {
        FullName = fullName.ThrowIfNull(nameof(fullName));
        NameAtBirth = nameAtBirth.ThrowIfNull(nameof(nameAtBirth));
        TaxId = taxId.ThrowIfNull(nameof(taxId));
        PhoneNumber = phoneNumber.ThrowIfNull(nameof(fullName));
        Address = address.ThrowIfNull(nameof(address));
    }
}

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

Інші методи більш елегантні в концепції, але все більш і більш складні у виконанні, наприклад, прикрашаючи параметр з [NotNull]атрибутом, і з допомогою відображення , як це .

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


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

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

@ jpmc26: Що це означає?
Роберт Харві

1
Коли я читаю вашу відповідь, це говорить про те, що ви не тестуєте конструктор , а натомість створюєте загальний метод, який викликається для кожного параметра, і перевіряйте його. Якщо ви додасте нову пару властивостей / аргументів і забудете протестувати nullаргумент на конструктор, ви не отримаєте невдалий тест.
jpmc26

@ jpmc26: Природно. Але це також справедливо, якщо ви пишете це звичайним способом, як показано в ОП. Усі поширені методи - це переміщення throwзовні конструктора; ви насправді не переносите управління в конструкторі кудись інше. Це просто корисний метод і спосіб робити речі вільно , якщо ви вирішите.
Роберт Харві

7

За короткий термін ви не можете зробити багато чого з нудності написання таких тестів. Однак є деяка допомога з виразами кидків, яка повинна бути реалізована в рамках наступного випуску C # (v7), що, ймовірно, відбудеться в найближчі кілька місяців:

public class Person
{
    public string FullName { get; }
    public string NameAtBirth { get; }
    public string TaxId { get; }
    public PhoneNumber PhoneNumber { get; }
    public Address Address { get; }

    public Person(
        string fullName,
        string nameAtBirth,
        string taxId,
        PhoneNumber phoneNumber,
        Address address)
    {
        FullName = fullName ?? throw new ArgumentNullException(nameof(fullName));
        NameAtBirth = nameAtBirth ?? throw new ArgumentNullException(nameof(nameAtBirth));
        TaxId = taxId ?? throw new ArgumentNullException(nameof(taxId)); ;
        PhoneNumber = phoneNumber ?? throw new ArgumentNullException(nameof(phoneNumber)); ;
        Address = address ?? throw new ArgumentNullException(nameof(address)); ;
    }
}

Ви можете експериментувати з виразами кидків через веб -сайт Try Roslyn .


Набагато компактніше, дякую. Половина стільки рядків. Чекаємо на C # 7!
Botond Balázs

@ BotondBalázs ви можете спробувати його у попередньому перегляді VS15, якщо хочете
user1306322

7

Я здивований, що ніхто ще не згадав про NullGuard.Fody . Він доступний через NuGet і автоматично вбудовує ці нульові перевірки в ІЛ під час компіляції.

Тож ваш код конструктора просто був би

public Person(
    string fullName,
    string nameAtBirth,
    string taxId,
    PhoneNumber phoneNumber,
    Address address)
{
    FullName = fullName;
    NameAtBirth = nameAtBirth;
    TaxId = taxId;
    PhoneNumber = phoneNumber;
    Address = address;
}

і NullGuard додасть ці нульові перевірки для вас, перетворивши його в саме те, що ви написали.

Але врахуйте, що NullGuard є відмовою з , тобто, це додасть ці перевірки нуля для кожного методу і аргументу конструктора, власність добувача і сеттерів і навіть перевірити метод значень, що повертаються , якщо явно не дозволяє нульове значення з [AllowNull]атрибутом.


Дякую, це виглядає як дуже приємне загальне рішення. Хоча дуже прикро, що це модифікує мовну поведінку за замовчуванням. Мені б хотілося це набагато більше, якби він працював, додаючи [NotNull]атрибут замість того, щоб не дозволяти null бути типовим.
Ботонд Балас

@ BotondBalázs: PostSharp має це разом з іншими атрибутами перевірки параметрів. На відміну від Fody, це не безкоштовно. Крім того, згенерований код запустить DLL-файли PostSharp, тому вам потрібно включити ті, які є вашим продуктом. Fody запустить свій код лише під час компіляції і не вимагатиме виконання.
Роман Райнер

@ BotondBalázs: Я також спочатку вважав дивно, що NullGuard застосовується за замовчуванням, але це має сенс справді. У будь-якій розумній базі коду, що дозволяє null, має бути набагато рідше, ніж перевірка на null. Якщо ви дійсно хочете десь дозволити нуль, підхід до відмови змушує вас бути дуже ретельним із цим.
Роман Reiner

5
Так, це розумно, але це порушує Принцип найменшого здивування . Якщо новий програміст не знає, що проект використовує NullGuard, вони чекають великого сюрпризу! До речі, я теж не розумію голосування, це дуже корисна відповідь.
Botond Balázs

1
@ BotondBalázs / Roman, схоже, у NullGuard з'явився новий явний режим, який змінює спосіб його роботи за замовчуванням
Kyle W

5

Чи варто написати всі ці тести?

Ні, напевно, ні. Яка ймовірність того, що ти це зіб'єш? Яка ймовірність, що якась семантика зміниться з-під вас? Який вплив, якщо хтось зробить це?

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

Було б гарною ідеєю написати генератор коду для таких класів, щоб уникнути написання тестів для кожного з них?

Можливо? Таку річ можна було легко зробити за допомогою роздумів. Щось слід враховувати - це генерування коду для реального коду, тому у вас немає N класів, які можуть мати людські помилки. Профілактика помилок> виявлення помилок


Дякую. Можливо, я не досить зрозумів у своєму питанні, але я мав намір написати генератор, який генерує незмінні класи, а не тести для них
Botond Balázs

2
Я також декілька разів бачив замість того, що if...throwвони матимуть ThrowHelper.ArgumentNull(myArg, "myArg"), тому логіка все ще є, і ви не зациклюєтесь на покритті коду. Де ArgumentNullметод буде робити if...throw.
Матвій

2

Чи варто написати всі ці тести?

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

Наприклад, у вас можуть бути тести для класу Factory, які мають твердження на основі цих властивостей (наприклад, створений примірник із належним чином призначеним Nameвластивістю).

Якщо ці класи піддаються відкритому API, який використовується третьою частиною / кінцевим користувачем (@EJoshua дякую за те, що помітили), то тести на очікувані ArgumentNullExceptionможуть бути корисними.

Дочекавшись C # 7, ви можете використовувати метод розширення

public MyClass(string name)
{
    name.ThrowArgumentNullExceptionIfNull(nameof(name));
}

public static void ThrowArgumentNullExceptionIfNull(this object value, string paramName)
{
    if(value == null)
        throw new ArgumentNullException(paramName);
}

Для тестування ви можете використовувати один параметризований метод тестування, який використовує відображення для створення nullпосилання на кожен параметр та затвердження очікуваного винятку.


1
Це переконливий аргумент щодо не тестування ініціалізації властивостей.
Botond Balázs

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

2

Ви завжди можете написати такий метод, як:

// Just to illustrate how to call this
private static void SomeMethod(string a, string b, string c, string d)
    {
        ValidateArguments(a, b, c, d);
        // ...
    }

    // This is the one to use as a utility function
    private static void ValidateArguments(params object[] args)
    {
        for (int i = 0; i < args.Length; i++)
        {
            if (args[i] == null)
            {
                StackTrace trace = new StackTrace();
                // Get the method that called us
                MethodBase info = trace.GetFrame(1).GetMethod();

                // Get information on the parameter that is null so we can add its name to the exception
                ParameterInfo param = info.GetParameters()[i];

                // Raise the exception on behalf of the caller
                throw new ArgumentNullException(param.Name);
            }
        }
    }

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

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

// Note that we already know based on the previous condition that args[i] is not null
else if (args[i].GetType() == typeof(string))
            {
                string argCast = arg as string;

                if (!argCast.Trim().Any())
                {
                    ParameterInfo param = GetParameterInfo(i);

                    throw new ArgumentException(param.Name + " is empty or consists only of whitespace");
                }
            }

Метод GetParameterInfo, на який я посилаюся, в основному робить відображення (так що мені не доведеться постійно вводити одне і те ж, що було б порушенням принципу DRY ):

private static ParameterInfo GetParameterInfo(int index)
    {
        StackTrace trace = new StackTrace();

        // Note that we have to go 2 methods back to get the ValidateArguments method's caller
        MethodBase info = trace.GetFrame(2).GetMethod();

        // Get information on the parameter that is null so we can add its name to the exception
        ParameterInfo param = info.GetParameters()[index];

        return param;
    }

1
Чому це заборонено? Це не надто погано
Gaspa79

0

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

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

Я пропоную використовувати API перевірки Java та екстерналізувати процес перевірки. Я пропоную мати чотири обов'язки (класи):

  1. Об'єкт пропозиції з валідацією анотованих параметрів Java з валідаційним методом, який повертає набір ConstraintViolations. Одна перевага полягає в тому, що ви можете обходити ці об’єкти без твердження, що є дійсним. Ви можете затримати перевірку, доки вона не буде необхідною, не намагаючись ініціювати об'єкт домену. Об'єкт перевірки може використовуватися в різних шарах, оскільки це POJO, що не має спеціальних знань про рівень. Це може бути частиною вашого громадського API.

  2. Завод об'єкта домену, який відповідає за створення дійсних об'єктів. Ви передаєте об'єкт пропозиції, і фабрика викличе перевірку та створить об’єкт домену, якщо все добре.

  3. Сам об’єкт домену, який повинен бути здебільшого недоступний для інстанції для інших розробників. Для тестування пропоную цей клас мати в пакеті.

  4. Інтерфейс публічного домену, щоб приховати конкретний об’єкт домену та зробити жорстке неправильне використання.

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

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


1
Я з повагою не згоден з вашим останнім моментом. Якщо не потрібно вводити одну і ту ж котельну плиту в 100 разів, це допоможе зменшити ймовірність помилки. Уявіть, якби це була Java, а не C #. Я повинен був би визначити поле резервного копіювання та метод отримання для кожної властивості. Код став би абсолютно нечитабельним, і що важливо, затьмарилась би незграбна інфраструктура.
Botond Balázs
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.