可以为单元测试重复代码吗?


11

我为类分配编写了一些排序算法,并且还编写了一些测试以确保算法正确实现。我的测试只有10行,其中有3行,但是3行之间只有1行更改,因此有很多重复的代码。将此代码重构为另一个可以在每次测试中调用的方法是否更好?然后,我不需要编写另一个测试来测试重构吗?有些变量甚至可以上移到类级别。测试类和方法是否应该遵循与常规类/方法相同的规则?

这是一个例子:

    [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); 
    }

Answers:


21

测试代码仍然是代码,还需要维护。

如果需要更改复制的逻辑,通常需要在复制逻辑的每个位置执行此操作。

DRY仍然适用。

然后,我不需要编写另一个测试来测试重构吗?

你会?您如何知道当前进行的测试是正确的?

您可以通过运行测试来测试重构。它们都应具有相同的结果。


对了 测试就是代码-编写好的代码的所有相同原理仍然适用!通过运行测试来测试重构,但是要确保覆盖范围足够,并且在测试中遇到了多个边界条件(例如,正常条件与故障条件)。
Michael

6
我不同意。测试不一定必须是DRY,对他们来说,DAMP(描述性和有意义的短语)比DRY更重要。(至少,通常来说。至少在这种情况下,将重复的初始化拉进助手中确实很有意义。)
JörgW Mittag 2012年

2
我以前从未听过DAMP,但是我喜欢这种描述。
约阿希姆·绍尔

@JörgW Mittag:通过测试,您仍然可以保持DRY和DAMP。如果我知道某些测试重复,我通常将测试的不同ARRANGE-ACT-ASSERT(或GIVEN-WHEN-THEN)部分重构为测试夹具中的辅助方法。它们通常具有DAMP名称,例如givenThereAreProductsSet(amount)和甚至简单actWith(param)。我设法用流利的api明智的方法(例如givenThereAre(2).products())来完成此操作,但是我很快停了下来,因为它感觉像是一种过大的杀伤力。
Spoike 2012年

11

正如Oded所说,测试代码仍然需要维护。我要补充一点,测试代码中的重复使维护人员更难理解测试的结构并添加新的测试。

在发布的两个函数中,以下几行是完全相同的,除了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。您还应该注意测试的可读性: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

甚至比生产代码更多,测试代码还需要针对可读性和可维护性进行优化,因为它必须与要测试的代码一起维护,并且必须作为文档的一部分进行读取。考虑复制的代码如何使测试代码的维护更加困难,以及如何成为不编写所有测试代码的诱因。另外,请不要忘记,当您编写一个函数来对测试进行DRY操作时,它也应该接受测试。


2

复制测试代码是一个容易陷入的陷阱。当然这很方便,但是如果您开始重构实现代码并且所有测试都需要更改,那会发生什么呢?与执行代码重复一样,您将面临同样的风险,因为您很可能还需要在许多地方更改测试代码。所有这些加起来浪费了大量时间,并且需要处理越来越多的故障点,这意味着维护软件的成本不必要地高昂,从而降低了软件的整体业务价值从事于。

还考虑在测试中易于执行的操作在实施中将变得易于执行。当您时间紧迫且压力重重时,人们往往会依赖于习得的行为模式,并且通常会尝试去做当时看来最容易的事情。因此,如果您发现剪切并粘贴了大量测试代码,则很可能在实现代码中也是如此,这是您在职业生涯的早期就避免的习惯,以节省大量时间当您发现自己必须维护所编写的旧代码,并且公司不一定负担得起重写的麻烦时,就会遇到麻烦。

正如其他人所说,您使用DRY主体,并寻找机会将任何可能的重复重构为辅助方法和辅助类,是的,您甚至应该在测试中执行此操作,以最大程度地重用代码并保存以后您将面临维护困难。您甚至可能发现自己正在缓慢地开发可以反复使用的测试API,甚至在多个项目中也可以重复使用-当然,这就是最近几年对我的影响。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.