【问题标题】:How can this SwingWorker code be made testable如何使这个 SwingWorker 代码可测试
【发布时间】:2011-03-06 03:25:40
【问题描述】:

考虑这段代码:

public void actionPerformed(ActionEvent e) {
    setEnabled(false);
    new SwingWorker<File, Void>() {

        private String location = url.getText();

        @Override
        protected File doInBackground() throws Exception {
            File file = new File("out.txt");
            Writer writer = null;
            try {
                writer = new FileWriter(file);
                creator.write(location, writer);
            } finally {
                if (writer != null) {
                    writer.close();
                }
            }
            return file;
        }

        @Override
        protected void done() {
            setEnabled(true);
            try {
                File file = get();
                JOptionPane.showMessageDialog(FileInputFrame.this,
                    "File has been retrieved and saved to:\n"
                    + file.getAbsolutePath());
                Desktop.getDesktop().open(file);
            } catch (InterruptedException ex) {
                logger.log(Level.INFO, "Thread interupted, process aborting.", ex);
                Thread.currentThread().interrupt();
            } catch (ExecutionException ex) {
                Throwable cause = ex.getCause() == null ? ex : ex.getCause();
                logger.log(Level.SEVERE, "An exception occurred that was "
                    + "not supposed to happen.", cause);
                JOptionPane.showMessageDialog(FileInputFrame.this, "Error: "
                    + cause.getClass().getSimpleName() + " "
                    + cause.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
            } catch (IOException ex) {
                logger.log(Level.INFO, "Unable to open file for viewing.", ex);
            }
        }
    }.execute();

url 是一个 JTextField,'creator' 是一个用于写入文件的注入接口(因此该部分正在测试中)。写入文件的位置是故意硬编码的,因为这只是一个示例。而 java.util.logging 只是用来避免外部依赖。

您将如何将其分块以使其可单元测试(包括在需要时放弃 SwingWorker,但随后替换其功能,至少如此处使用的那样)。

在我看来,doInBackground 基本没问题。基本机制是创建作家并关闭它,这几乎太简单而无法测试,而真正的工作正在测试中。但是 done 方法存在引用问题,包括它与父类 actionPerformed 方法的耦合,以及协调按钮的启用和禁用。

但是,将其分开并不明显。注入某种 SwingWorkerFactory 使得捕获 GUI 字段变得更加难以维护(很难看出这将如何改进设计)。 JOpitonPane 和 Desktop 具有 Singleton 的所有“优点”,而异常处理使得包装 get 变得不可能。

那么什么是测试这段代码的好方法呢?

【问题讨论】:

  • 重新格式化的代码;如果不正确,请恢复。
  • 不是一个完整的答案:但如果你喜欢高质量的代码,请不要靠近SwingWorker。一般来说,把事情排除在外。如果您有一个使用静态/单例的 API,则会引入一个接口,该接口使用“真实”静态 API 的实现和另一个用于模拟(可能另一个用于审计)。
  • @Tom,如果您有时间编写 SwingWorker 的替代设计大纲(或者如果您知道替代更好的实现),我们将不胜感激。

标签: java unit-testing swing tdd swingworker


【解决方案1】:

恕我直言,这对于匿名类来说很复杂。我的方法是将匿名类重构为这样的:

public class FileWriterWorker extends SwingWorker<File, Void> {
    private final String location;
    private final Response target;
    private final Object creator;

    public FileWriterWorker(Object creator, String location, Response target) {
        this.creator = creator;
        this.location = location;
        this.target = target;
    }

    @Override
    protected File doInBackground() throws Exception {
        File file = new File("out.txt");
        Writer writer = null;
        try {
            writer = new FileWriter(file);
            creator.write(location, writer);
        }
        finally {
            if (writer != null) {
                writer.close();
            }
        }
        return file;
    }

    @Override
    protected void done() {
        try {
            File file = get();
            target.success(file);
        }
        catch (InterruptedException ex) {
            target.failure(new BackgroundException(ex));
        }
        catch (ExecutionException ex) {
            target.failure(new BackgroundException(ex));
        }
    }

    public interface Response {
        void success(File f);
        void failure(BackgroundException ex);
    }

    public class BackgroundException extends Exception {
        public BackgroundException(Throwable cause) {
            super(cause);
        }
    }
}

这允许独立于 GUI 测试文件写入功能

然后,actionPerformed 变成这样:

public void actionPerformed(ActionEvent e) {
    setEnabled(false);
    Object creator;
    new FileWriterWorker(creator, url.getText(), new FileWriterWorker.Response() {
        @Override
        public void failure(FileWriterWorker.BackgroundException ex) {
            setEnabled(true);
            Throwable bgCause = ex.getCause();
            if (bgCause instanceof InterruptedException) {
                logger.log(Level.INFO, "Thread interupted, process aborting.", bgCause);
                Thread.currentThread().interrupt();
            }
            else if (cause instanceof ExecutionException) {
                Throwable cause = bgCause.getCause() == null ? bgCause : bgCause.getCause();
                logger.log(Level.SEVERE, "An exception occurred that was "
                    + "not supposed to happen.", cause);
                JOptionPane.showMessageDialog(FileInputFrame.this, "Error: "
                    + cause.getClass().getSimpleName() + " "
                    + cause.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
            }
        }

        @Override
        public void success(File f) {
            setEnabled(true);
            JOptionPane.showMessageDialog(FileInputFrame.this,
                "File has been retrieved and saved to:\n"
                + file.getAbsolutePath());
            try {
                Desktop.getDesktop().open(file);
            }
            catch (IOException iOException) {
                logger.log(Level.INFO, "Unable to open file for viewing.", ex);
            }
        }
    }).execute();
}

此外,FileWriterWorker.Response 的实例可以分配给一个变量并独立于FileWriterWorker 进行测试。

【讨论】:

  • 该代码 sn-p 中有一些不错的想法/习语。谢谢,我要仔细研究一下。
  • 我就是喜欢这个。
【解决方案2】:

当前的实现将线程关注点、UI 和文件写入耦合在一起 - 正如您发现的那样,耦合使得很难单独测试各个组件。

这是一个相当长的响应,但归结为将这三个问题从当前实现中提取到具有定义接口的单独类中。

分解应用程序逻辑

首先,关注核心应用程序逻辑并将其移至单独的类/接口中。接口允许更轻松地模拟和使用其他摆动线程框架。这种分离意味着您可以完全独立于其他关注点来测试您的应用程序逻辑。

interface FileWriter
{
    void writeFile(File outputFile, String location, Creator creator)
         throws IOException;
    // you could also create your own exception type to avoid the checked exception.

    // a request object allows all the params to be encapsulated in one object.
    // this makes chaining services easier. See later.
    void writeFile(FileWriteRequest writeRequest); 
}

class FileWriteRequest
{
    File outputFile;
    String location;
    Creator creator;
    // constructor, getters etc..
}


class DefualtFileWriter implements FileWriter
{
    // this is basically the code from doInBackground()
    public File writeFile(File outputFile, String location, Creator creator)
       throws IOException 
    {
            Writer writer = null;
            try {
                writer = new FileWriter(outputFile);
                creator.write(location, writer);
            } finally {
                if (writer != null) {
                    writer.close();
                }
            }
            return file;
    }   
    public void writeFile(FileWriterRequest request) {
         writeFile(request.outputFile, request.location, request.creator);
    }
}

分离出用户界面

现在应用程序逻辑分离,然后我们将成功和错误处理分解。这意味着可以在不实际执行文件写入的情况下测试 UI。特别是,可以测试错误处理,而实际上不需要引发这些错误。在这里,错误非常简单,但通常有些错误很难引发。通过分离错误处理,也有机会重用或替换错误的处理方式。例如。稍后使用JXErrorPane

interface FileWriterHandler {
     void done();
     void handleFileWritten(File file);
     void handleFileWriteError(Throwable t);
}  

class FileWriterJOptionPaneOpenDesktopHandler implements FileWriterHandler
{
   private JFrame owner;
   private JComponent enableMe;

   public void done() { enableMe.setEnabled(true); }

   public void handleFileWritten(File file) {
       try {
         JOptionPane.showMessageDialog(owner,
                    "File has been retrieved and saved to:\n"
                    + file.getAbsolutePath());
         Desktop.getDesktop().open(file);
       }
       catch (IOException ex) {
           handleDesktopOpenError(ex);
       }
   }

   public void handleDesktopOpenError(IOException ex) {
        logger.log(Level.INFO, "Unable to open file for viewing.", ex);        
   }

   public void handleFileWriteError(Throwable t) {
        if (t instanceof InterruptedException) {
                logger.log(Level.INFO, "Thread interupted, process aborting.", ex);  
                // no point interrupting the EDT thread
        }
       else if (t instanceof ExecutionException) {
           Throwable cause = ex.getCause() == null ? ex : ex.getCause();
           handleGeneralError(cause);
       }
       else
         handleGeneralError(t);
   }

   public void handleGeneralError(Throwable cause) {
        logger.log(Level.SEVERE, "An exception occurred that was "
                    + "not supposed to happen.", cause);
        JOptionPane.showMessageDialog(owner, "Error: "
                    + cause.getClass().getSimpleName() + " "
                    + cause.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
   }
}

分离线程

最后,我们还可以使用 FileWriterService 分离出线程关注点。使用上面的 FileWriteRequest 使编码变得更简单。

interface FileWriterService
{
   // rather than have separate parms for file writing, it is
   void handleWriteRequest(FileWriteRequest request, FileWriter writer, FileWriterHandler handler);
}

class SwingWorkerFileWriterService 
   implements FileWriterService
{
   void handleWriteRequest(FileWriteRequest request, FileWriter writer, FileWriterHandler handler) {
       Worker worker = new Worker(request, fileWriter, fileWriterHandler);
       worker.execute();
   }

   static class Worker extends SwingWorker<File,Void> {
        // set in constructor
        private FileWriter fileWriter;
        private FileWriterHandler fileWriterHandler;
        private FileWriterRequest fileWriterRequest;

        protected File doInBackground() {
            return fileWriter.writeFile(fileWriterRequest);
        }
        protected void done() {
            fileWriterHandler.done();
            try
            {
                File f = get();
                fileWriterHandler.handleFileWritten(f);
            }
            catch (Exception ex)
            {                   
                // you could also specifically unwrap the ExecutorException here, since that
                // is specific to the service implementation using SwingWorker/Executors.
                fileWriterHandler.handleFileError(ex);
            }
        }
   }

}

系统的每个部分都可以单独测试 - 应用程序逻辑、表示(成功和错误处理)和线程实现也是一个单独的问题。

这可能看起来像很多接口,但实现主要是从您的原始代码中剪切和粘贴。接口提供了使这些类可测试所需的分离。

我不太喜欢 SwingWorker,因此将它们保留在接口后面有助于避免它们产生的代码混乱。它还允许您使用不同的实现来实现单独的 UI/后台线程。比如使用Spin,只需要提供一个新的FileWriterService实现即可。

【讨论】:

    【解决方案3】:

    简单的解决方案:一个简单的计时器是最好的;你启动你的计时器,你启动你的actionPerformed,并且在超时时必须启用bouton等等。

    这是一个带有 java.util.Timer 的非常少的例子:

    package goodies;
    
    import java.util.Timer;
    import java.util.TimerTask;
    import javax.swing.JButton;
    
    public class SWTest
    {
      static class WithButton
      {
        JButton button = new JButton();
    
        class Worker extends javax.swing.SwingWorker<Void, Void>
        {
          @Override
          protected Void doInBackground() throws Exception
          {
            synchronized (this)
            {
              wait(4000);
            }
            return null;
          }
    
          @Override
          protected void done()
          {
            button.setEnabled(true);
          }
        }
    
        void startWorker()
        {
          Worker work = new Worker();
          work.execute();
        }
      }
    
        public static void main(String[] args)
        {
          final WithButton with;
          TimerTask verif;
    
          with = new WithButton();
          with.button.setEnabled(false);
          Timer tim = new Timer();
          verif = new java.util.TimerTask()
          {
            @Override
            public void run()
            {
              if (!with.button.isEnabled())
                System.out.println("BAD");
              else
                System.out.println("GOOD");
              System.exit(0);
            }};
          tim.schedule(verif, 5000);
          with.startWorker();
        }
    }
    

    假定的专家解决方案: Swing Worker 是一个 RunnableFuture,在其中嵌入了一个可调用的 FutureTask,因此您可以使用自己的执行程序来启动它(RunableFuture)。为此,您需要一个具有名称类的 SwingWorker,而不是匿名的。这位所谓的专家说,使用您自己的执行器和名称类,您可以测试所有您想要的东西。

    【讨论】:

    • 我不明白为什么你会使用计时器来重新启用按钮,如果该过程可能没有在那个时间内完成,或者不必要地禁用按钮。
    • 对不起,我的英语不好。我想这不是“必须启用”而是“启用”。我用一些 java 示例代码编辑我的答案。我希望它是最好的。
    猜你喜欢
    • 2017-02-14
    • 1970-01-01
    • 2018-04-30
    • 1970-01-01
    • 1970-01-01
    • 2015-12-09
    • 1970-01-01
    • 1970-01-01
    • 2015-03-19
    相关资源
    最近更新 更多