如何设计一个只对部分代码进行计时的测试接口?

How to design a test interface that times only some part of the code?

我想计算我的代码在许多测试 运行 中平均执行的时间。在每个测试中 运行,doWork() 执行我想要计时的工作。但我也想 checkWork() 在每个测试中 运行 不计入时间。我将有许多类似的练习# 类,所以我想通过 TestInterface 抽象测试。我目前的方法是合理的方法吗?还是有更好的设计模式/标准方法?提前致谢。

@FunctionalInterface
public interface TestInterface {
  void test(final int NUM_TESTS);
}

public class TimeTests {
  public static void test(TestInterface ti, final int NUM_TESTS, String testName) {
    DecimalFormat df = new DecimalFormat("#.####");
    long start = System.nanoTime();
    ti.test(NUM_TESTS);
    System.out.println("DEBUG: " + testName + " took "
              + df.format((System.nanoTime() - start) * 1.0 / NUM_TESTS)
              + " nanoseconds on average for " + NUM_TESTS + " tests");

  }
}

public class Exercise1 {
  private static final int NUM_TESTS = (int) Math.pow(10, 6);

  private static void mainWork(List<Integer> A) {
    // do stuff and time it
  }

  private static void checkWork(List<Integer> A) {
    // do stuff but don't count it towards the time
  }

  public static void main(String[] args) {

    TimeTests.test((NUM_TESTS_LOCAL) -> {
      for (int i = 0; i < NUM_TESTS_LOCAL; ++i) {
        List<Integer> A = new ArrayList<>();
        // add random elements to A
        mainWork(A);
        checkWork(A);
      }
    }, NUM_TESTS, "Exercise1");
  }
}

好吧,我想我已经为这个任务建立了一个像样的框架(这个词对吗?)。如果有人可以插话让我知道我的方法是否有任何好处,我将非常感激。

虽然到目前为止我的代码似乎对我的用例工作正常,但我有几个问题:

  1. public interface CloneableTestInput<T extends CloneableTestInput<T>>的接口定义中,类型模板<T extends CloneableTestInput<T>怎么不是循环定义?我不确定我是否完全理解该类型模板的含义。
  2. 有没有办法制作一个实现 CloneableTestInput<List> 的通用 CloneableList class?目前,我需要为每个 Collection 类型(例如 ArrayList、LinkedList 等)单独实现。同样,是否可以制作一个通用的 CloneableSet class 来实现 CloneableTestInput<Set>?

提前致谢:)

测试框架

第 I 部分 - 测试输入的接口

这允许 TimeTests.java 适用于通用输入类型。

public interface CloneableTestInput<T extends CloneableTestInput<T>> extends Cloneable {
  T clone();
}

public class CloneableString implements CloneableTestInput<CloneableString> {
  public String data;
  public CloneableString() {}
  public CloneableString(String input) { data = input; }

  public CloneableString clone() { return new CloneableString(String.valueOf(data)); }
}

public class CloneableArrayList extends ArrayList implements CloneableTestInput<CloneableArrayList> {
  public CloneableArrayList(ArrayList input) {
    this.addAll(input);
  }

  @Override
  public CloneableArrayList clone() {
    return new CloneableArrayList(this);
  }
}

第二部分 - 计时测试的界面

@FunctionalInterface
public interface TimeTestsInterface<outputType> {
  void test(Callable<CloneableTestInput> formInput
          , Function<CloneableTestInput, outputType> runAlgorithm
          , Function<CloneableTestInput, outputType> getKnownOutput
          , BiFunction<outputType, outputType, Boolean> checkResults
          , final int NUM_TESTS, String testName);
}

public class TimeTests<outputType> implements TimeTestsInterface<outputType> {
  public void test(Callable<CloneableTestInput> formInput
          , Function<CloneableTestInput, outputType> runAlgorithm
          , Function<CloneableTestInput, outputType> getKnownOutput
          , BiFunction<outputType, outputType, Boolean> checkResults
          , final int NUM_TESTS, String testName) {
    try {
      DecimalFormat df = new DecimalFormat("#.####");
      long total = 0, start;
      for (int i=0; i < NUM_TESTS; ++i) {
        CloneableTestInput input = formInput.call();

        CloneableTestInput orig_input = input.clone();
        start = System.nanoTime();
        outputType algorithmResult = runAlgorithm.apply(input);
        total += System.nanoTime() - start;
        outputType expectedResult = getKnownOutput.apply(orig_input);
        assert(checkResults.apply(algorithmResult, expectedResult));
      }
      System.out.println("DEBUG: " + testName + " took "
              + df.format(total * 1.0 / NUM_TESTS)
              + " nanoseconds on average for " + NUM_TESTS + " tests");
    } catch (Exception|AssertionError e) {
      System.out.println(e.toString() + " - " + e.getMessage() + " - ");
      e.printStackTrace();
    }

  }
}

用法示例

增加一个 BigInteger(使用 CloneableArrayList)

/**
 * Problem 6.2 from EPI
 * Given an array A of digits encodiing a decimal number D,
 * with MSD at A[0]. Update A to hold D + 1.
 */
public class PlusOne {
  private static final int NUM_TESTS = (int) Math.pow(10, 5);
  private static final int ARR_LENGTH = (int) Math.pow(10, 2);

  private static ArrayList<Integer> plusOne(ArrayList<Integer> A) {
    int n = A.size() - 1;
    A.set(n, A.get(n) + 1);
    for (int i = n; i > 0 && A.get(i) == 10; --i) {
      A.set(i, 0);
      A.set(i-1, A.get(i-1) + 1);
    }
    if (A.get(0) == 10) {
      // Need additional digit up front as MSD
      A.set(0,0);
      A.add(0,1);
    }
    return A;
  }

  private static ArrayList<Integer> randArray(int len) {
    ArrayList<Integer> A = new ArrayList<>();
    if (len == 0) return A;
    Random rgen = new Random();
    A.add(rgen.nextInt(9) + 1);
    --len;
    while (len != 0) {
      A.add(rgen.nextInt(10));
      --len;
    }
    return A;
  }

  public static void main(String[] args) {
    Callable<CloneableTestInput> formInput = () -> new CloneableArrayList(randArray(ARR_LENGTH));
    Function<CloneableTestInput, ArrayList<Integer>> runAlgorithm =
        (input) -> plusOne((ArrayList<Integer>) input);
    Function<CloneableTestInput, ArrayList<Integer>> getKnownOutput = 
        (orig_input) -> {
      BigInteger B = new BigInteger(Joiner.on("").join((ArrayList<Integer>) orig_input));
      B = B.add(BigInteger.valueOf(1));
      ArrayList<Integer> expectedOutput = new ArrayList<>();
      while (B.compareTo(BigInteger.valueOf(0)) > 0) {
        expectedOutput.add(0, B.mod(BigInteger.valueOf(10)).intValue());
        B = B.divide(BigInteger.valueOf(10));
      }
      return expectedOutput;
    };
    BiFunction<ArrayList<Integer>, ArrayList<Integer>, Boolean> checkResults = List::equals;
    TimeTests<ArrayList<Integer>> algTimer = new TimeTests<>();
    algTimer.test(formInput, runAlgorithm, getKnownOutput, checkResults, NUM_TESTS, "PlusOne");
  }
}

String可以重排成回文吗? (使用 CloneableString)

public class CanStringBePalindrome {
  private static final int INPUT_STRING_LENGTH = (int) Math.pow(10, 2);
  private static final int NUM_TESTS = (int) Math.pow(10, 6);

  private static boolean canFormPalindromeHash(final String s) {
    Map<Character, Integer> charFreqs = new HashMap<>();
    for (int i = 0; i < s.length(); ++i) {
      char c = s.charAt(i);
      if (!charFreqs.containsKey(c))
        charFreqs.put(c, 1);
      else
        charFreqs.put(c, charFreqs.get(c) + 1);
    }

    int oddFreqCount = 0;
    for (Map.Entry<Character, Integer> entry : charFreqs.entrySet()) {
      if ((entry.getValue() % 2) != 0 && (++oddFreqCount > 1))
        return false;
    }
    return true;
  }


  private static boolean canFormPalindromeSorting(final String s) {
    // TODO : find faster/simpler way of getting frequency counts
    char[] a = s.toCharArray();
    Arrays.sort(a);
    int oddFreqCount = 0;
    int numCurrChar =1;

    for (int i = 1; i < a.length && oddFreqCount <= 1; ++i) {
      if(a[i] != a[i-1]) {
        if ((numCurrChar & 1) != 0)
          ++oddFreqCount;
        numCurrChar = 1;
      } else
        ++numCurrChar;
    }
    if ((numCurrChar & 1) != 0)
      ++oddFreqCount;
    return oddFreqCount <= 1;
  }

  private static String randString(int len) {
    StringBuilder sb = new StringBuilder();
    Random rgen = new Random();
    while (len-- > 0)
      sb.append((char)(rgen.nextInt(26) + 'A'));
    return sb.toString();
  }

  public static void main(String[] args) {
    Callable<CloneableTestInput> formInput = () -> new CloneableString(randString(INPUT_STRING_LENGTH));
    Function<CloneableTestInput, Boolean > runAlgorithm =
            (input) -> canFormPalindromeHash(((CloneableString)input).data);
    Function<CloneableTestInput, Boolean> getKnownOutput =
            (orig_input) -> canFormPalindromeSorting(((CloneableString)orig_input).data);
    BiFunction<Boolean, Boolean, Boolean> checkResults = Boolean::equals;
    TimeTests<Boolean> algTimer = new TimeTests<>();
    algTimer.test(formInput, runAlgorithm, getKnownOutput, checkResults
            , NUM_TESTS, "CanStringBePalindrome");
  }
}