如何使这个 SwingWorker 代码可测试

发布于 2024-09-06 08:45:21 字数 2308 浏览 3 评论 0原文

考虑以下代码:

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 和桌面具有单例的所有“优点”,并且异常处理使得不可能轻松地包装 get。

那么测试这段代码的好解决方案是什么?

Consider this code:

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 is a JTextField and 'creator' is an injected interface for writing the file (so that part is under test). The location where the file is written is hard coded on purpose because this is intended as an example. And java.util.logging is used simply to avoid an external dependency.

How would you chunk this up to make it unit-testable (including abandoning SwingWorker if needed, but then replacing its functionality, at least as used here).

The way I look at it, the doInBackground is basically alright. The fundamental mechanics are creating a writer and closing it, which is almost too simple to test and the real work is under test. However, the done method is quote problematic, including its coupling with the actionPerformed method the parent class and coordinating the enabling and disabling of the button.

However, pulling that apart is not obvious. Injecting some kind of SwingWorkerFactory makes capturing the GUI fields a lot harder to maintain (it is hard to see how it would be a design improvement). The JOpitonPane and the Desktop have all the "goodness" of Singletons, and exception handling makes it impossible to wrap the get easily.

So what would be a good solution to bring this code under test?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

假扮的天使 2024-09-13 08:45:21

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

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 进行测试。

IMHO, that's complicated for an anonymous class. My approach would be to refactor the anonymous class to something like this:

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

That allows the file writing functionality to be tested independent of a GUI

Then, the actionPerformed becomes something like this:

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

Additionally, the instance of FileWriterWorker.Response can be assigned to a variable and tested independent of FileWriterWorker.

痴梦一场 2024-09-13 08:45:21

当前的实现将线程问题、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

现在应用程序逻辑已分离,然后我们将成功和错误处理分解出来。这意味着无需实际进行文件写入即可测试 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 的新实现即可。

The current implementation couples together threading concerns, UI and file writing - and as you've discovered that coupling makes it hard to test the individual components in isolation.

This is quite a long response, but it boils down to pulling out these three concerns from the current implementation into separate classes with a defined interface.

Factor out Application Logic

To start with, focus on the core application logic and move that into a separate class/interface. An interface allows easier mocking, and use of other swing-threading frameworks. The separation means you can test your application logic entirely independently from the other concerns.

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

Separate out UI

With the application logic now separate, we then factor out the success and error handling. This means that the UI can be tested without actually doing the file writing. In particular, error handling can be tested without actually need to provoke those errors. Here, the errors are quite simple, but often some errors can be very difficult to provoke. By separating out the error handling, there is also chance for reuse, or replacing how the errors are handled. E.g. using a JXErrorPane later.

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

Separate out Threading

Finally, we can also separate out the threading concerns with a FileWriterService. Using a FileWriteRequest above makes coding this simpler.

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

}

Each part of the system is separately testable - the application logic, the presentation (success and error handling) and the threading implementation is also a separate concern.

This may seem like a lot of interfaces, but the implementation is mostly cut-and-paste from your original code. The interfaces provide the separation that is needed to make these classes testable.

I'm not much of a fan of SwingWorker's so keeping them behind an interface helps keep the clutter they produce out of the code. It also allows you to use a different implementation for implementing the separate UI/background threads. For example, to use Spin, you only need to provide a new implementation of FileWriterService.

她如夕阳 2024-09-13 08:45:21

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

这是一个带有 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,而不是匿名类。这位所谓的专家说,有了自己的执行器和名称类,您就可以测试您想要的所有内容。

Easy solution : a simple timer is best ; you lanch your timer, you launch your actionPerformed, and at the timeout the bouton must be enabled and so on.

Here is an very littel exemple with a 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();
    }
}

Supposed Expert solution : a Swing Worker is a RunnableFuture, inside it a FutureTask imbeded in a callable, so you can use your own executor to launch it (the RunableFuture). To do that, you need a SwingWorker with a name class, not an anonymous. With your own executor and a name class, you can test all you want, the supposed expert says.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文