如何更好地测试具有公共队列的多线程程序?

时间:2013-02-24 13:30:45

标签: java multithreading testing queue

当需要测试此程序时,哪种方式应该更好 首先askUserPathAndWord()要求用户输入pathwhatFind。我们有两个主题:

  • 第一个线程扫描文件夹,如果它在队列中找到可读文件put()
  • 队列中的第二个线程take(),并在其中查找whatFind 文件。如果搜索成功,则输出以控制该文件的路径 以及这个词的频率。

这是与多线程工作的集成依赖性。哪种变体能更好地测试这个程序--Sunit of EasyMock?我阅读了一些关于EasyMock的教程,但我不知道在哪些情况下最好使用它。

代码:

class FolderScan implements Runnable {

    private String path;
    private BlockingQueue<File> queue;
    private CountDownLatch latch;
    private File endOfWorkFile;

    FolderScan(String path, BlockingQueue<File> queue, CountDownLatch latch,
            File endOfWorkFile) {
        this.path = path;
        this.queue = queue;
        this.latch = latch;
        this.endOfWorkFile = endOfWorkFile;
    }

    public FolderScan() {
    }

    @Override
    public void run() {
        findFiles(path);
        queue.add(endOfWorkFile);
        latch.countDown();
    }

    private void findFiles(String path) {

        try {
            File root = new File(path);
            File[] list = root.listFiles();
            for (File currentFile : list) {
                String s = currentFile.getName().toLowerCase();
                if (currentFile.isDirectory()) {
                    findFiles(currentFile.getAbsolutePath());
                } else {
                    if (s.matches("^.*?\\.(txt|pdf|doc|docx|html|htm|xml|djvu|rar|rtf)$")) {
                        queue.put(currentFile);
                    }
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

public class FileScan implements Runnable {

    private String whatFind;
    private BlockingQueue<File> queue;
    private CountDownLatch latch;
    private File endOfWorkFile;

    public FileScan(String whatFind, BlockingQueue<File> queue,
            CountDownLatch latch, File endOfWorkFile) {
        this.whatFind = whatFind;
        this.queue = queue;
        this.latch = latch;
        this.endOfWorkFile = endOfWorkFile;
    }

    public FileScan() {
    }

    @Override
    public void run() {

        while (true) {
            try {
                File file;
                file = queue.take();

                if (file == endOfWorkFile) {
                    break;
                }

                scan(file);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        latch.countDown();
    }

    private void scan(File file) {
        Scanner scanner = null;
        int matches = 0;

        try {
            scanner = new Scanner(file);
        } catch (FileNotFoundException e) {
            System.out.println("File Not Found.");
            e.printStackTrace();
        }

        while (scanner.hasNext())
            if (scanner.next().equals(whatFind)) {
                matches++;
            }

        if (matches > 0) {
            String myStr = String.format(
                    "File: %s - and the number of matches " + "is: %d",
                    file.getAbsolutePath(), matches);
            System.out.println(myStr);
        }
    }

    public void askUserPathAndWord() {

        BufferedReader bufferedReader = new BufferedReader(
                new InputStreamReader(System.in));
        String path;
        String whatFind;
        BlockingQueue<File> queue = new LinkedBlockingQueue<File>();

        try {
            System.out.println("Please, enter a Path and Word"
                    + "(which you want to find):");
            System.out.println("Please enter a Path:");
            path = bufferedReader.readLine();
            System.out.println("Please enter a Word:");
            whatFind = bufferedReader.readLine();

            if (path != null && whatFind != null) {

                File endOfWorkFile = new File("GameOver.tmp");
                CountDownLatch latch = new CountDownLatch(2);

                FolderScan folderScan = new FolderScan(path, queue, latch,
                        endOfWorkFile);
                FileScan fileScan = new FileScan(whatFind, queue, latch,
                        endOfWorkFile);

                Executor executor = Executors.newCachedThreadPool();
                executor.execute(folderScan);
                executor.execute(fileScan);

                latch.await();
                System.out.println("Thank you!");
            } else {
                System.out.println("You did not enter anything");
            }

        } catch (IOException | RuntimeException e) {
            System.out.println("Wrong input!");
            e.printStackTrace();
        } catch (InterruptedException e) {
            System.out.println("Interrupted.");
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {  
        new FileScan().askUserPathAndWord();
    }
}

问题:

  • 在这种情况下哪种测试最佳?
  • 如何更好地测试两种变体中的合同义务?
  • 如果答案是EasyMock,我们应该如何正确地做到这一点?
  • 如果Junit,我们如何测试void方法?

2 个答案:

答案 0 :(得分:2)

JUnit还是EasyMock? - 答案是,两者都有!一个是单元测试框架,另一个是允许模拟对象,这使您可以编写更好的测试。因此,使用JUnit plus 任何模拟框架(建议EasyMock,我使用Mockito或者第三个名为jMock)是个好主意。< / p>

哪种测试具有最佳覆盖率? - 模拟允许您关注要进行单元测试的代码区域,因此您最终将比以前测试更多代码。模拟执行繁重操作的代码(例如写入数据库或从文件系统读取 - 与您的操作一样)特别有用。因此,将EasyMock与JUnit结合使用可以比单独的JUnit更好地覆盖(和更好的测试)。

如何更好地测试两种变体中的合同义务? - 确保所有公共方法都经过测试。然后,在每次测试结束时彻底断言并验证您的期望。

如果答案是EasyMock,我们应该如何正确地执行此操作? - 使用verify方法检查测试期间是否正确调用了模拟对象。

如果是Junit,我们如何测试void方法? - 你需要以其他方式断言结果是预期的。也许您将对象作为参数传递给void方法,或者可能有另一种方法来获取结果(例如getter)。

祝你好运!

答案 1 :(得分:2)

在考虑测试此程序时,请记住单元测试是什么。从Wikipedia开始,“单元测试是一种方法,通过该方法测试各个源代码单元......以确定它们是否适合使用”。

请记住,您正在深入了解一下,因为测试多线程程序很困难。您应尽可能地从测试功能中删除线程交互的复杂性。

看看你的程序,你已经有了很好的封装和关注点分离,所以你很顺利。我的策略是独立于线程测试两个*Scan对象。为此,请记住他们的角色是什么。我会说以下内容:

  • FolderScan遍历目录结构,查找特定类型的文件,并将通过过滤器的文件放入队列。当它耗尽目录结构时,它会将特定文件放入队列中,倒计时锁存并退出。
  • FileScan使用文件队列,对它们执行操作,并将输出打印到控制台。当它命中特定文件时,它会倒计时并退出。

由于你已经有了大概或多或少的工作代码,当你对其进行改进测试时(而不是在编写代码时编写它们,这是更好的)你想要尽可能少地更改源代码以获得通过测试。之后你可能希望重构代码,你的测试会让你有信心这样做。

FolderScan测试

首先,为FolderScan创建一个新的JUnit测试。您可以编写多个测试,但是从高级别开始,每个测试都应该使用一些文件填充目录结构。我至少会测试每一个案例:

  1. 包含传递过滤器的文件的单个文件夹。
  2. 包含未通过过滤器的文件的单个文件夹。
  3. 一个嵌套文件夹,每个文件夹都包含一个文件。
  4. 测试越精细,越好。每个测试只是创建一个FolderScan的新实例,为它指定指定给定文件夹的参数。调用run(),并确保:

    1. 队列包含您期望的File个对象。
    2. CountDownLatch已经减少。
    3. 队列中的最后一个元素是'trigger'File
    4. FileScan测试

      在高级别,现在应该清楚测试:创建一些文本File对象,用它们填充队列并使用标记,然后将它们传递给新的FileScan对象。再次,更精细,更好,至少:

      1. 具有匹配字符串的文件
      2. 没有匹配字符串的文件
      3. 多个文件
      4. 这个课程存在问题,这是经典的“如何测试单身人士”问题。实际上,此对象的结果是通过管道传送到System.out,您必须先将其挂钩以检查结果。作为第一遍,我建议重构构造函数以传递PrintStream。在制作中你会传入System.out,在测试中你会传递一些你可以检查的内容:new PrintStream(new ByteArrayOutputStream())你可以检查内容,或者更好的是模拟。

        最后,每个测试都要检查:

        1. 队列已清空。
        2. CountDownLatch已经减少
        3. 指定的PrintStream已写入预期内容。
        4. 您应该非常有信心FileScanFolderScan的工作方式与宣传的一样。

          *测试askUserPathAndWord *

          我没有看到任何直接的方法来测试这个函数/类。它违反了Single Responsibility Principle,只是做了太多事情。我会将以下职责提取到新方法或类中:

          • 向用户询问单词和路径
          • 给定一个单词,使用正确的参数创建FileScan(工厂)
          • 给定路径,使用正确的参数(工厂)
          • 创建FolderScan
          • 给定两个runnable和一个latch,创建一个ExecutorService,排队,然后等待闩锁

          然后你可以独立测试它们。

          *后续步骤*

          进行这些测试的一个好处是,一旦你拥有它们,你就可以自由地进行重构,并确信你没有破坏它们。例如,您可以查看Callable而不是Runnable,这可以让您处理Future引用,从FolderScan中删除输出参数,并删除{{ 1}}完全。

          快乐测试!