CS61B sp2018笔记 | Testing and Selection Sort

  调试程序可以说是最能体现程序员水平的能力之一了,接下来我们将讨论如何写test来保证你的程序的正确性,与此同时我们将讨论一种比较算法——选择排序

1. Ad Hoc Testing

  在写sort类之前,我们先来设想并实现TestSort类。

public class TestSort {
    /** Tests the sort method of the Sort class. */
    public static void testSort() {
        String[] input = {"i", "have", "an", "egg"};
        String[] expected = {"an", "egg", "have", "i"};
        Sort.sort(input);
        for (int i = 0; i < input.length; i += 1) {
            if (!input[i].equals(expected[i])) {
                System.out.println("Mismatch in position " + i + ", expected: " + expected + ", but got: " + input[i] + ".");
                break;
            }
        }
    }

    public static void main(String[] args) {
        testSort();
    }
}

  然后,我们写一个空的sort方法,先不去实现它。

public class Sort {
    /** Sorts strings destructively. */
    public static void sort(String[] x) {        
    }
}

  于是,当我们运行testsort()方法并调用Sort.sort()方法时,控制台将会显示如下结果:

Mismatch in position 0, expected: an, but got: i.

  我们的到了一个报错信息,这证明我们写的测试类是正确的。事实上,这特别想课堂上的一些自动打分系统(autograder),你已经可以更准确客观地判断你的代码的正确性了。
  不过,写上面这样的测试类有些麻烦,你必须写很多的循环打印各种各样的信息,甚至测试类比你真正要写的类还要长,所以,我们将使用org.junit库来简化我们的工作。

2. JUnit Testing

  在写测试类时,org.junit库提供给我们许多有帮助的方法和使用的功能,比如,我们可以这样简化ad hoc test:

public static void testSort() {
    String[] input = {"i", "have", "an", "egg"};
    String[] expected = {"an", "egg", "have", "i"};
    Sort.sort(input);
    org.junit.Assert.assertArrayEquals(expected, input);
}

  This code is much simpler,我们再运行一下test类,将得到如下结果:

Exception in thread "main" arrays first differed at element [0]; expected:<[an]> but was:<[i]>
at org.junit.internal.ComparisonCriteria.arrayEquals(ComparisonCriteria.java:55)
at org.junit.Assert.internalArrayEquals(Assert.java:532)
...

  我们得到了同样的结果,只不过这样的输出格式有些丑,我们一会会使它变得简洁一些。

3. Selection Sort

  现在,我们要实现一下Sort.sort方法,选择排序包含以下三步:

  • 找到中最小的值
  • 把它移动到最前面
  • 对剩下的n-1个元素进行上两步操作
public class Sort {
    /** Sorts strings destructively. */
    public static void sort(String[] x) { 
           // find the smallest item
           // move it to the front
           // selection sort the rest (using recursion?)
    }
}

  可能你会觉得选择排序非常简单,不过我们还是要一步一步实现它,并在实现过程中故意犯一些错误,因为这节课的重点是讨论如何调试代码,而不是写出更好的选择排序。

3.1 findSmallest

  我们先来实现findSmallest方法:

    /** Returns the smallest string in x. */
    public static String findSmallest(String[] x) {
        return x[3];
    }

  显然,这是个错误答案,不过我们是想看看测试类会提示我们什么信息:

public class TestSort {
    ...
    public static void testFindSmallest() {
        String[] input = {"i", "have", "an", "egg"};
        String expected = "an";

        String actual = Sort.findSmallest(input);
        org.junit.Assert.assertEquals(expected, actual);        
    }

    public static void main(String[] args) {
        testFindSmallest(); // note: we changed this from testSort!
    }
}

Exception in thread "main" java.lang.AssertionError: expected:<[an]> but was:<[null]>
at org.junit.Assert.failNotEquals(Assert.juava:834)
at TestSort.testFindSmallest(TestSort.java:9)
at TestSort.main(TestSort.java:24)

  接下来我们实现findSmallest方法:

/** Returns the smallest string in x. 
  * @source Got help with string compares from https://goo.gl/a7yBU5. */
public static String findSmallest(String[] x) {
    String smallest = x[0];
    for (int i = 0; i < x.length; i += 1) {
        int cmp = x[i].compareTo(smallest);
        if (cmp < 0) {
            smallest = x[i];
        }
    }
    return smallest;
}

3.2 Swap

  swap方法很简单,我们可以快速实现它:

public static void swap(String[] x, int a, int b) {
    String temp = x[a];
    x[a] = x[b];
    x[b] = temp;
}

  但我们还是实验一下,如果我们像下面这样写,运行测试类,会发生什么结果:

public static void swap(String[] x, int a, int b) {    
    x[a] = x[b];
    x[b] = x[a];
}
public class TestSort {
    ...    

    /** Test the Sort.swap method. */
    public static void testSwap() {
        String[] input = {"i", "have", "an", "egg"};
        int a = 0;
        int b = 2;
        String[] expected = {"an", "have", "i", "egg"};

        Sort.swap(input, a, b);
        org.junit.Assert.assertArrayEquals(expected, input);
    }

    public static void main(String[] args) {
        testSwap();
    }
}

Exception in thread "main" arrays first differed in element [2]; expected:<[i]> but was:<[an]>
at TestSort.testSwap(TestSort.java:36)

3.3 Revising findSmallest

 &mesp;完成上面两个方法后,我们的sort方法变成了下面这样:

/** Sorts strings destructively. */
public static void sort(String[] x) { 
       // find the smallest item
       String smallest = findSmallest(x);

       // move it to the front
       swap(x, 0, smallest);

       // selection sort the rest (using recursion?)
}

  显然,为了使用swap方法,我们的findSmallest方法要返回最小值的下标来作为参数传递给swap方法,所以我们进行一些改进:

public static int findSmallest(String[] x) {
    int smallestIndex = 0;
    for (int i = 0; i < x.length; i += 1) {
        int cmp = x[i].compareTo(x[smallestIndex]);
        if (cmp < 0) {
            smallestIndex = i;
        }
    }
    return smallestIndex;
}

  之后,最好更改测试类,来测试我们改动的正确性。

3.4 Recursive Helper Methods

  我们将使用递归的方法完成最后一步,因此需要设计一个递归的辅助方法,我们选择增加一个重载的sort方法:

/** Sorts strings destructively starting from item start. */
private static void sort(String[] x, int start) { 
   int smallestIndex = findSmallest(x);
   swap(x, start, smallestIndex);
   sort(x, start + 1);
}

  可是运行测试类,我们会遇到这样一个问题:

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 4
at Sort.swap(Sort.java:16)

  使用编译器的debug可以发现,我们没有考虑递归结束的临界条件,于是完善这个方法:

/** Sorts strings destructively starting from item start. */
private static void sort(String[] x, int start) { 
   if (start == x.length) {
       return;
   }
   int smallestIndex = findSmallest(x);
   swap(x, start, smallestIndex);
   sort(x, start + 1);
}

  运行测试类,我们有会得到一个报错:

Exception in thread "main" arrays first differed at element [0];
expected<[an]> bit was:<[have]>

  再进行断点调试,发现findSmallest方法除了问题,每次调用都是在整个数组范围进行比较,所以,我们增加一个参数指定比较范围的开始位置:

public static int findSmallest(String[] x, int start) {
    int smallestIndex = start;
    for (int i = start; i < x.length; i += 1) {
        int cmp = x[i].compareTo(x[smallestIndex]);
        if (cmp < 0) {
            smallestIndex = i;
        }
    }
    return smallestIndex;
}

  每改变一个方法都要用为这个方法设计的测试方法测试,先用自定义的testFindSmallest方法测试,通过后再用testsort方法测试,最后应该会全部通过,到此为止,我们成功实现了选择排序方法。

4. Better JUnit

  在日常调试中,我们通常会进行如下操作简化测试类的编写:

  • 把上述所有静态方法变成非静态
  • 在测试类中导包:
    import org.junit.Test
    import static org.junit.Assert.*
  • 在每个测试方法前面加标签@Test,并删除原本用于测试的main方法
  • 将冗长的调用语句org.junit.Assert.assertEquals(expected, actual);改为简单的assertEquals(expected, actual);

5. Testing Philosophy

  一般程序员测试程序的构思分为三种。
  一种是Autograder Driven Development(ADD),这样的思路一般是先写出tons of codes,然后测试后发现有问题,再一步一步的找出来,很可能错误百出,丧失了debug的信心。
  一种是Test-Driven Development(TDD),这种是把程序分为一个个小的单元,每完成一个单元就用测试类测试写的是否正确,不过这种方式会浪费时间,拖慢工作节奏。
  还有一种是Integration Test,也就是集成测试,与单元测试类似的是,这种方法也是看代码是否按预期工作,不过集成测试范围广泛,一般会涉及到外部组件,且设计难度较高,一般由专门的测试人员完成。

猜你喜欢

转载自blog.csdn.net/qq_40950957/article/details/81204569