2010-06-21 2 views
16

Рассмотрим этот код:Как это SwingWorker код может быть проверяемым

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

Как бы вы это сделали, чтобы сделать его единым для тестирования (в том числе отказаться от SwingWorker, если необходимо, но затем заменить его функциональность, по крайней мере, как используется здесь).

Как я смотрю на это, doInBackground в основном в порядке. Фундаментальная механика создает писателя и закрывает его, что почти слишком просто для тестирования, и настоящая работа находится под контролем. Тем не менее, выполненный метод представляет собой проблему, связанную с проблемой, включая ее связь с методом actionPerformed с родительским классом и координацию включения и отключения кнопки.

Однако потянуть это отдельно не очевидно. Внедрение какого-то SwingWorkerFactory делает захват полей GUI намного сложнее в обслуживании (трудно понять, как это будет улучшение дизайна). JOpitonPane и Desktop имеют всю «доброту» синглтонов, а обработка исключений делает невозможным упрощение переноса.

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

+0

Переформатированный код; пожалуйста, верните, если это неверно. – trashgod

+1

Не полный ответ: Но если вам нравится код качества, не ходите рядом с 'SwingWorker'. В общем, все дело. Там, где у вас есть API, который использует статики/синглтоны, вводит интерфейс с реализацией, используя «реальный» статический API, а другой для издевательств (возможно, другой для аудита). –

+0

@Tom, если у вас есть время написать схему альтернативного дизайна SwingWorker (или если вы знаете о альтернативной лучшей реализации), это будет очень признательно. – Yishai

ответ

10

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

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.

+0

В этом фрагменте кода есть несколько хороших идей/идиом. Спасибо, я немного побью его. – Yishai

+0

Мне просто нравится это. – gdbj

-1

Простое решение: простой таймер лучше; вы задерживаете свой таймер, вы запускаете actionPerformed, а в таймаут должен быть включен бит и так далее.

Вот очень Littel Exemple с 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(); 
    } 
} 

Expert Предполагаемого решения: качелями работником является RunnableFuture, внутри него FutureTask imbeded в вызываемом, так что вы можете используйте свой собственный исполнитель для его запуска (RunableFuture). Для этого вам нужен SwingWorker с классом имен, а не анонимным. По словам предполагаемого эксперта, с вашим собственным исполнителем и классом имен вы можете протестировать все, что хотите.

+1

Я не понимаю, почему вы должны использовать таймер для повторного включения кнопки, если процесс может не завершиться за это время, или отключить эту кнопку без необходимости. – Yishai

+0

Извините, это мой плохой английский. Это не «должно быть включено», но «разрешено», я полагаю. Я редактирую свой ответ с помощью некоторого кода Java. Надеюсь, это лучше. – Istao

8

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

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

фактор из приложения Logic

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

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, поэтому держать их за интерфейсом помогает сохранить беспорядок, который они производят вне кода. Он также позволяет использовать другую реализацию для реализации отдельных потоков пользовательского интерфейса/фона. Например, чтобы использовать Spin, вам нужно только предоставить новую реализацию FileWriterService.

Смежные вопросы