2010-06-21 17 views
16

Considerate questo codice:Come può questo codice SwingWorker essere testabile

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 è un JTextField e 'creatore' è un'interfaccia iniettato per la scrittura del file (in modo che una parte è in fase di test). La posizione in cui il file è scritto è hardcoded di proposito perché questo è inteso come un esempio. E java.util.logging è usato semplicemente per evitare una dipendenza esterna.

Come si spende questo per renderlo unit-testabile (incluso l'abbandono di SwingWorker se necessario, ma poi la sua sostituzione, almeno come usato qui).

Il modo in cui lo guardo, il doInBackground è fondamentalmente ok. I meccanismi fondamentali stanno creando uno scrittore e chiudendolo, il che è quasi troppo semplice da testare e il vero lavoro è sotto test. Tuttavia, il metodo done è problematico, incluso il suo accoppiamento con il metodo actionPerformed della classe genitore e il coordinamento dell'abilitazione e disabilitazione del pulsante.

Tuttavia, separare ciò non è ovvio. L'iniezione di una specie di SwingWorkerFactory rende molto più difficile la cattura dei campi della GUI (è difficile vedere come sarebbe un miglioramento del design). Il JOpitonPane e il Desktop hanno tutta la "bontà" di Singletons, e la gestione delle eccezioni rende impossibile avvolgere l'ottenere facilmente.

Quindi quale sarebbe una buona soluzione per portare questo codice sotto test?

+0

codice riformattato; per favore, ripristina se non è corretto. – trashgod

+1

Non una risposta completa: ma se ti piace il codice di qualità, non avvicinarti a 'SwingWorker'. In generale, fatelo fuori. Dove hai un'API che usa statica/singletons presenta un'interfaccia con un'implementazione usando l'API statica "reale" e un'altra per il mocking (possibilmente un'altra per l'auditing). –

+0

@Tom, se hai tempo per scrivere la bozza di un disegno alternativo a SwingWorker (o se conosci una migliore implementazione alternativa) sarebbe molto apprezzato. – Yishai

risposta

10

IMHO, è complicato per una classe anonima. Il mio approccio sarebbe quello di refactoring del classe anonima a qualcosa di simile:

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

che permette il file di scrittura funzionalità da testare indipendente di una GUI

Poi, il actionPerformed diventa qualcosa di simile:

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

Inoltre, l'istanza di FileWriterWorker.Response può essere assegnata a una variabile e verificata indipendente da FileWriterWorker.

+0

Ci sono alcune belle idee/idiomi in quel frammento di codice. Grazie, ho intenzione di masticarlo un po '. – Yishai

+0

Adoro questo. – gdbj

-1

Soluzione facile: un semplice timer è il migliore; lancerai il tuo timer, avvierai la tua actionPerformed, e al timeout il bouton deve essere abilitato e così via.

Ecco un Esempio molto littel con un 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(); 
    } 
} 

soluzione Expert Supposed: un operaio Swing è un RunnableFuture, al suo interno un FutureTask imbeded in un callable, in modo da poter usa il tuo esecutore per lanciarlo (il RunableFuture). Per fare ciò, hai bisogno di uno SwingWorker con una classe nome, non anonimo. Con il tuo stesso esecutore e una classe di nomi, puoi testare tutto ciò che vuoi, dice il presunto esperto.

+1

Non capisco perché dovresti usare un timer per riattivare il pulsante se il processo potrebbe non essere terminato in quel momento, o mantenere il pulsante disabilitato inutilmente. – Yishai

+0

Scusa, è il mio cattivo inglese. Non è "deve essere abilitato" ma "è abilitato", suppongo. Modifico la mia risposta con un codice di esempio java. Spero sia meglio. – Istao

8

L'implementazione corrente combina insieme problemi di threading, UI e scrittura di file e, come hai scoperto, l'accoppiamento rende difficile testare i singoli componenti separatamente.

Questa è una risposta piuttosto lunga, ma si riduce a estrapolare queste tre preoccupazioni dall'attuale implementazione in classi separate con un'interfaccia definita.

Factor su Application Logic

Per cominciare, concentrarsi sulla logica dell'applicazione principale e spostare che in una classe/interfaccia separata. Un'interfaccia consente una simulazione più semplice e l'utilizzo di altri framework di oscillazione. La separazione significa che puoi testare la tua logica applicativa in modo completamente indipendente dalle altre preoccupazioni.

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

separare UI

Con la logica applicazione ora separato, abbiamo poi fattorizzare il successo e gestione degli errori. Ciò significa che l'interfaccia utente può essere testata senza effettivamente scrivere il file. In particolare, la gestione degli errori può essere testata senza effettivamente provocare tali errori. Qui gli errori sono abbastanza semplici, ma spesso alcuni errori possono essere molto difficili da provocare. Separando la gestione degli errori, c'è anche la possibilità di riutilizzarli o sostituire il modo in cui vengono gestiti gli errori. Per esempio. utilizzando un JXErrorPane in seguito.

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

indipendente su Filettatura

Infine, possiamo anche separare le preoccupazioni filettatura con FileWriterService. L'uso di un FileWriteRequest sopra rende la codifica più semplice.

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

} 

Ogni parte del sistema è separatamente testabile - la logica di applicazione, la presentazione (il successo e la gestione degli errori) e l'implementazione di threading è anche una preoccupazione a parte.

Questo può sembrare un sacco di interfacce, ma l'implementazione è principalmente taglia e incolla dal codice originale. Le interfacce forniscono la separazione necessaria per rendere testabili queste classi.

Non sono molto fan di SwingWorker, quindi tenerli dietro un'interfaccia aiuta a mantenere la confusione che producono fuori dal codice. Consente inoltre di utilizzare un'implementazione diversa per implementare i thread UI/background separati. Ad esempio, per utilizzare Spin, è necessario fornire solo una nuova implementazione di FileWriterService.

Problemi correlati