Как сделать этот код SwingWorker тестируемым - PullRequest
16 голосов
/ 21 июня 2010

Рассмотрим этот код:

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 усложняет ведение захвата полей графического интерфейса пользователя (трудно понять, как это будет улучшать дизайн).JOpitonPane и рабочий стол обладают всеми «достоинствами» синглетонов, а обработка исключений делает невозможным простое получение.

Итак, что было бы хорошим решением для тестирования этого кода?

Ответы [ 3 ]

10 голосов
/ 24 июня 2010

ИМХО, это сложно для анонимного класса.Мой подход заключается в том, чтобы реорганизовать анонимный класс в нечто вроде этого:

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

, что позволяет проверять функциональность записи файла независимо от графического интерфейса пользователя

Затем 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.

8 голосов
/ 02 июля 2010

Текущая реализация связывает воедино проблемы с потоками, пользовательским интерфейсом и записью файлов - и, как вы обнаружили, из-за объединения сложно тестировать отдельные компоненты по отдельности.

Это довольно длинный ответ, но онсводится к тому, чтобы вытащить эти три проблемы из текущей реализации в отдельные классы с определенным интерфейсом.

Факторизация логики приложения

Для начала сосредоточимся на ядреприложите логику и перенесите ее в отдельный класс / интерфейс.Интерфейс позволяет легче насмехаться и использовать другие фреймворки с многопоточностью.Разделение означает, что вы можете проверить свою логику приложения полностью независимо от других проблем.

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

Отделить пользовательский интерфейс

Теперь, когда логика приложения отделена, мы вычисляемУспех и обработка ошибок.Это означает, что пользовательский интерфейс может быть протестирован без фактической записи файла.В частности, обработка ошибок может быть протестирована без необходимости провоцирования этих ошибок.Здесь ошибки довольно просты, но часто некоторые ошибки могут быть очень трудно спровоцировать.Отделяя обработку ошибок, существует также возможность повторного использования или замены способа обработки ошибок.Например, используя 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.

0 голосов
/ 01 июля 2010

Простое решение: лучше всего использовать простой таймер;Вы запускаете таймер, запускаете 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 - это RunnableFuture, внутри него FutureTask, включенный в вызываемый объект, так что вы можете использовать собственный исполнитель для его запуска (RunableFuture).Для этого вам нужен SwingWorker с именем класса, а не анонимный.Предполагается, что с вашим собственным исполнителем и классом имен вы можете протестировать все, что захотите.

...