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


35

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

Це дуже спрощений приклад. Насправді функції набагато складніші.

Приклад:

public class MyClass
{
     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected int FunctionB()
     {
         return new Random().Next();
     }
}

Тому для перевірки цього у нас є 2 методи.

Спосіб 1. Використовуйте функції та дії для заміни функціональності методів. Приклад:

public class MyClass
{
     public Func<int> FunctionB { get; set; }

     public MyClass()
     {
         FunctionB = FunctionBImpl;
     }

     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected int FunctionBImpl()
     {
         return new Random().Next();
     }
}

[TestClass]
public class MyClassTests
{
    private MyClass _subject;

    [TestInitialize]
    public void Initialize()
    {
        _subject = new MyClass();
    }

    [TestMethod]
    public void FunctionA_WhenNumberIsOdd_ReturnsTrue()
    {
        _subject.FunctionB = () => 1;

        var result = _subject.FunctionA();

        Assert.IsFalse(result);
    }
}

Спосіб 2: Зробіть учасників віртуальними, вивести клас та у похідному класі використовувати Функції та дії для заміни функціональності Приклад:

public class MyClass
{     
     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected virtual int FunctionB()
     {
         return new Random().Next();
     }
}

public class TestableMyClass
{
     public Func<int> FunctionBFunc { get; set; }

     public MyClass()
     {
         FunctionBFunc = base.FunctionB;
     }

     protected override int FunctionB()
     {
         return FunctionBFunc();
     }
}

[TestClass]
public class MyClassTests
{
    private TestableMyClass _subject;

    [TestInitialize]
    public void Initialize()
    {
        _subject = new TestableMyClass();
    }

    [TestMethod]
    public void FunctionA_WhenNumberIsOdd_ReturnsTrue()
    {
        _subject.FunctionBFunc = () => 1;

        var result = _subject.FunctionA();

        Assert.IsFalse(result);
    }
}

Я хочу знати, що краще, а також ЧОМУ?

Оновлення: ПРИМІТКА. ФункціяB також може бути загальнодоступною


Ваш приклад простий, але не зовсім правильний. FunctionAповертає bool, але встановлює лише локальну змінну xі нічого не повертає.
Ерік П.

1
У цьому конкретному прикладі FunctionB може бути, public staticале в іншому класі.

Для огляду коду, як очікується, ви опублікуєте фактичний код, а не спрощену його версію. Див. FAQ. Як видно, ви задаєте конкретне питання, не шукаючи огляду коду.
Вінстон Еверт

1
FunctionBрозбита по конструкції. new Random().Next()майже завжди помиляється. Вам слід ввести екземпляр Random. ( Randomце також погано розроблений клас, який може викликати кілька додаткових проблем)
CodesInChaos

на більш загальній ноті, DI через делегатів - це абсолютно добре імхо
jk.

Відповіді:


32

Відредаговано після оригінального оновлення плаката.

Відмова від відповідальності: не програміст C # (переважно Java або Ruby). Моя відповідь була б: я б це взагалі не перевіряв, і не думаю, що слід.

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

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

Дивіться пов’язану дискусію там: https://stackoverflow.com/questions/105007/should-i-test-private-methods-or-only-public-ones

Після коментаря , якщо FunctionB є загальнодоступним, я просто випробую обидва, використовуючи тестовий блок. Ви можете подумати, що тест FunctionA не є повністю "одиницею" (як це називається FunctionB), але я би не надто хвилювався цим: якщо тест FunctionB працює, але не є тестом FunctionA, це чітко означає, що проблема не в піддомен FunctionB, що досить добре для мене як дискримінатора.

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


2
Повністю згоден з відповіддю @Martin. Коли ви пишете одиничні тести для класу, ви не повинні перевіряти методи тестування . Ви тестуєте поведінку класу , що контракт (декларація, який клас повинен робити) виконується. Отже, ваші одиничні тести повинні охоплювати всі вимоги, пред'явлені до цього класу (з використанням публічних методів / властивостей), включаючи виняткові випадки

Привіт, дякую за відповідь, але це не відповіло на моє запитання. Не важливо, чи FunctionB був приватним / захищеним. Він також може бути загальнодоступним і все ще називатися з FunctionA.

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

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

2
Те, що FunctionA викликає FunctionB, є невідповідною деталлю з точки зору тестування одиниць. Якщо тести для FunctionA написані правильно, це деталізація, яку можна буде відновити пізніше, не порушуючи тестів (до тих пір, поки загальна поведінка FunctionA не залишиться незмінною). Справжня проблема полягає в тому, що пошук FunctionB випадкового числа потрібно проводити з введеним об'єктом, щоб ви могли використовувати макет під час тестування, щоб гарантувати повернення відомого числа. Це дозволяє перевірити відомі входи / виходи.
Дан Ліонс

11

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

Тож якщо я за сценарієм, де маю

class A 
{
     public B C()
     {
         D();
     }

     private E D();
     {
         // i actually want to control what this produces when I test C()
         // or this is important enough to test on its own
         // and, typically, both of the above
     }
}

Тоді я йду на рефактор.

class A 
{
     ICollaborator collaborator;

     public A(ICollaborator collaborator)
     {
         this.collaborator = collaborator;
     }

     public B C()
     {
         collaborator.D();
     }
}

Тепер у мене є сценарій, коли D () незалежно перевіряється та є повністю замінюваним.

Як засіб організації, мій співробітник може не жити на одному рівні простору імен. Наприклад, якщо він Aзнаходиться у FooCorp.BLL, то мій співавтор може бути ще одним глибоким шаром, як у FooCorp.BLL.Collaborators (або будь-яке ім'я). Моя співпраця може бути видно лише у складі через internalмодифікатор доступу, який я також піддавав би моїм проектам тестування блоків за допомогою InternalsVisibleToатрибута зборки. Таким чином, ви можете зберігати чистий API, що стосується абонентів, створюючи код, який можна перевірити.


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

Вам доведеться вирішити, чи має сенс названий делегат або інтерфейс має сенс, і я не вирішу для вас. Особисто я не проти однокласних (публічних) методів. Чим менше, тим краще, оскільки вони стають все легшими для розуміння.
Ентоні Пеграм

0

Додаючи до того, що вказує Мартін,

Якщо ваш метод приватний / захищений - не перевіряйте його. Він є внутрішнім для класу і не має доступу до нього поза класом.

В обох підходах, які ви згадуєте, я маю ці проблеми -

Метод 1 - Це фактично змінює клас поведінки тесту в тесті.

Спосіб 2. Це фактично не перевіряє виробничий код, натомість тестує іншу реалізацію.

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

Я очікую реалістичний сценарій, коли ми можемо налаштувати MyClass таким чином, щоб ми знали, що поверне FunctionB. Тоді наш очікуваний результат відомий, ми можемо викликати FunctionA і стверджувати про фактичний результат.


3
protectedмайже те саме, що public. Тільки privateі internalє деталями реалізації.
CodesInChaos

@codeinchaos - мені тут цікаво. Для тесту захищені методи є "приватними", якщо ви не змінюєте атрибути збірки. Тільки похідні типи мають доступ до захищених членів. За винятком віртуальних, я не бачу, чому до тесту слід захищатись подібними до публічних. Ви можете, будь ласка, детальніше розглянути?
Шрікант Венугопалан

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

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

0

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

Плюси

  1. Дозволяє просту структуру коду, коли використання шаблонів коду лише для тестування одиниць може додати додаткову складність.
  2. Дозволяє запечатувати класи та для усунення віртуальних методів, необхідних популярним глузуючим фреймворкам, таким як Moq. Герметизація класів та усунення віртуальних методів робить їх кандидатами для вбудовування та інших оптимізацій компілятора. ( https://msdn.microsoft.com/en-us/library/ff647802.aspx )
  3. Спрощує тестабельність, оскільки заміна реалізації функції Func / Action в тесті Unit є простою, як просто призначення нового значення Func / Action
  4. Також дозволяє перевірити, якщо статичний Func був викликаний з іншого методу, оскільки статичні методи не можуть бути глумитими.
  5. Легко переробляти існуючі методи до функцій / дій, оскільки синтаксис виклику методу на сайті виклику залишається тим самим. (Часи, коли ви не можете переробити метод у функцію / дія, див. Мінуси)

Мінуси

  1. Функції / дії не можна використовувати, якщо ваш клас може бути похідний, оскільки функції функцій / дій не мають шляхів успадкування, таких як Методи
  2. Неможливо використовувати параметри за замовчуванням. Створення Func з параметрами за замовчуванням тягне за собою створення нового делегата, який може зробити код заплутаним залежно від випадку використання
  3. Неможливо використовувати синтаксис названого параметра для виклику таких методів, як щось (firstName: "S", lastName: "K")
  4. Найбільша проблема полягає в тому, що ви не маєте доступу до посилання "це" у функціях та діях, і тому будь-які залежності від класу повинні бути явно передані як параметри. Це добре, оскільки ви знаєте всі залежності, але погано, якщо у вас є багато властивостей, від яких буде залежати ваш Func. Ваш пробіг буде залежати від Вашого випадку використання.

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

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

public class MyClass
{
     public Func<int> FunctionB = () => new Random().Next();

     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }
}

Сподіваюся, це допомагає!


-1

Використання Mock можливо. Nuget: https://www.nuget.org/packages/moq/

І повірте мені його досить просто і має сенс.

public class SomeClass
{
    public SomeClass(int a) { }

    public void A()
    {
        B();
    }

    public virtual void B()
    {

    }
}

[TestFixture]
public class Test
{
    [Test]
    public void Test_A_Calls_B()
    {
        var mockedObject = new Mock<SomeClass>(5); // You can also specify constructor arguments.
        //You can also setup what a function can return.
        var obj = mockedObject.Object;
        obj.A();

        Mock.Get(obj).Verify(x=>x.B(),Times.AtLeastOnce);//This test passes
    }
}

Макет потребує методу віртуального, щоб перекрити.

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