Чи добре повторювати код для одиничних тестів?


11

Я написав кілька алгоритмів сортування для призначення класу, а також написав кілька тестів, щоб переконатися, що алгоритми були виконані правильно. Мої тести мають довжину лише 10 рядків, і є 3 з них, але лише 1 рядок змінюється між 3, тому багато повторюваного коду. Чи краще перефактурувати цей код на інший метод, який потім викликається з кожного тесту? Чи не мені тоді потрібно було б написати ще один тест для перевірки рефакторингу? Деякі змінні можна навіть перемістити до рівня класу. Чи повинні тестувальні класи та методи дотримуватися тих же правил, що і звичайні заняття / методи?

Ось приклад:

    [TestMethod]
    public void MergeSortAssertArrayIsSorted()
    {
        int[] a = new int[1000];
        Random rand = new Random(DateTime.Now.Millisecond);
        for(int i = 0; i < a.Length; i++)
        {
            a[i] = rand.Next(Int16.MaxValue);
        }
        int[] b = new int[1000];
        a.CopyTo(b, 0);
        List<int> temp = b.ToList();
        temp.Sort();
        b = temp.ToArray();

        MergeSort merge = new MergeSort();
        merge.mergeSort(a, 0, a.Length - 1);
        CollectionAssert.AreEqual(a, b);
    }
    [TestMethod]
    public void InsertionSortAssertArrayIsSorted()
    {
        int[] a = new int[1000];
        Random rand = new Random(DateTime.Now.Millisecond);
        for (int i = 0; i < a.Length; i++)
        {
            a[i] = rand.Next(Int16.MaxValue);
        }
        int[] b = new int[1000];
        a.CopyTo(b, 0);
        List<int> temp = b.ToList();
        temp.Sort();
        b = temp.ToArray();

        InsertionSort merge = new InsertionSort();
        merge.insertionSort(a);
        CollectionAssert.AreEqual(a, b); 
    }

Відповіді:


21

Тестовий код все ще є кодом, і його також потрібно підтримувати.

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

DRY все ще застосовується.

Чи не мені тоді потрібно було б написати ще один тест для перевірки рефакторингу?

Хотів би ти? І як ти знаєш, що тести, які ти зараз маєш, є правильними?

Ви протестуєте рефакторинг, запустивши тести. Усі вони повинні мати однакові результати.


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

6
Я не погоджуюсь. Тести не обов'язково повинні бути ДРУГИми, для них важливіше бути DAMP (Описові та значущі фрази), ніж DRY. (Взагалі, принаймні. Хоча в цьому конкретному випадку витягування повторної ініціалізації в помічник безумовно має сенс.)
Jörg W Mittag

2
Я ніколи раніше не чув DAMP, але мені подобається цей опис.
Йоахім Зауер

@ Йорг W Міттаг: Ви все ще можете бути СУХОМО і ДАМПОВАНО з тестами. Зазвичай я переробляю різні частини тесту ARRANGE-ACT-ASSERT (або GIVEN-WHEN-THEN) на допоміжні методи в тестовому кріпленні, якщо знаю, що деяка частина тесту повторюється. Зазвичай вони мають назви DAMP, такі як givenThereAreProductsSet(amount)і навіть такі прості, як actWith(param). Я встиг зробити це з вільним api мудрий (наприклад givenThereAre(2).products()) один раз, але я швидко зупинився, тому що це було схоже на надмір.
Спойк

11

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

У двох розміщених вами функціях наступні рядки абсолютно однакові, за винятком однієї різниці простору на початку forциклу:

        int[] a = new int[1000];
        Random rand = new Random(DateTime.Now.Millisecond);
        for (int i = 0; i < a.Length; i++)
        {
            a[i] = rand.Next(Int16.MaxValue);
        }
        int[] b = new int[1000];
        a.CopyTo(b, 0);
        List<int> temp = b.ToList();
        temp.Sort();
        b = temp.ToArray();

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


4

Ні, це не нормально. Натомість слід використовувати TestDataBuilder . Ви також повинні подбати про читаність ваших тестів: a? 1000? б? Якщо завтра вам доведеться попрацювати над тестовою реалізацією, тести - це чудовий спосіб ввести логіку: пишіть свої тести для колег-програмістів, а не для компілятора :)

Ось ваша реалізація тестів, "оновлена":

/**
* Data your tests will exercice on
*/
public class MyTestData(){
    final int [] values;
    public MyTestData(int sampleSize){
        values = new int[sampleSize];
        //Out of scope of your question : Random IS a depencency you should manage
        Random rand = new Random(DateTime.Now.Millisecond);
        for (int i = 0; i < a.Length; i++)
        {
            a[i] = rand.Next(Int16.MaxValue);
        }
    }
    public int [] values();
        return values;
    }

}

/**
* Data builder, with default value. 
*/
public class MyTestDataBuilder {
    //1000 is actually your sample size : emphasis on the variable name
    private int sampleSize = 1000; //default value of the sample zie
    public MyTestDataBuilder(){
        //nope
    }
    //this is method if you need to test with another sample size
    public MyTestDataBuilder withSampleSizeOf(int size){
        sampleSize=size;
    }

    //call to get an actual MyTestData instance
    public MyTestData build(){
        return new MyTestData(sampleSize);
    }
}

public class MergeSortTest { 

    /**
    * Helper method build your expected data
    */
    private int [] getExpectedData(int [] source){
        int[] expectedData =  Arrays.copyOf(source,source.length);
        Arrays.sort(expectedData);
        return expectedData;
    }
}

//revamped tests method Merge
    public void MergeSortAssertArrayIsSorted(){
        int [] actualData = new MyTestDataBuilder().build();
        int [] expected = getExpectedData(actualData);
        //Don't know what 0 is for. An option, that should have a explicit name for sure :)
        MergeSort merge = new MergeSort();
        merge.mergeSort(actualData,0,actualData.length-1); 
        CollectionAssert.AreEqual(actualData, expected);
    }

 //revamped tests method Insertion
 public void InsertionSortAssertArrayIsSorted()
    {
        int [] actualData = new MyTestDataBuilder().build();
        int [] expected = getExpectedData(actualData);
        InsertionSort merge = new InsertionSort();
        merge.insertionSort(actualData);
        CollectionAssert.AreEqual(actualData, expectedData); 
    }
//another Test, for which very small sample size matter
public void doNotCrashesWithEmptyArray()
    {
        int [] actualData = new MyTestDataBuilder().withSampleSizeOf(0).build();
        int [] expected = getExpectedData(actualData);
        //continue ...
    }
}

2

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


2

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

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

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

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