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는 기본적으로 괜찮습니다. 기본 메커니즘은 작가를 만들고 닫는 것인데, 테스트하기가 너무 간단하고 실제 작업이 테스트 중입니다. 그러나 done 메소드는 부모 클래스 인 actionPerformed 메소드와의 결합과 버튼의 활성화 및 비활성화를 조정하는 것을 포함하여 문제가됩니다.

그러나 그 차이는 분명하지 않습니다. 어떤 종류의 SwingWorkerFactory를 주입하면 유지 보수하기가 훨씬 어려워 GUI 필드를 캡처 할 수 있습니다 (디자인 개선이 어떻게되는지 알기가 어렵습니다). JOpitonPane과 Desktop은 Singletons의 "장점"을 모두 가지고 있으며 예외 처리는 get을 쉽게 감쌀 수 없도록 만듭니다.

그럼이 코드를 테스트 할 때 좋은 해결책은 무엇입니까?

+0

서식있는 코드; 잘못된 경우 되돌려주십시오. – trashgod

+1

완전한 대답은 아닙니다 :하지만 당신이 품질의 코드를 좋아한다면,'SwingWorker'를 가까이 가지 마십시오. 일반적으로 요인을 밖으로 나눕니다. 정적/싱글 톤을 사용하는 API를 사용하는 곳에서는 "실제"정적 API를 사용하는 구현과 조롱을 위해 인터페이스를 도입합니다 (감사용으로 사용할 수도 있습니다). –

+0

@Tom, SwingWorker에 대안 디자인의 개요를 쓸 시간이 있다면 (또는 더 나은 대안을 알고 있다면) 대단히 감사하겠습니다. – Yishai

답변

10

IMHO, 익명의 사용자에게는 복잡합니다. 나의 접근 방식은 이런 일에 익명의 클래스를 리팩토링하는 것입니다 :

: 기능을 작성 파일을 수 있습니다

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과 독립적으로 테스트 할 수 있습니다.

+0

코드 스 니펫에는 멋진 아이디어/숙어가 있습니다. 고마워, 나는 그것을 조금 씹을거야. – Yishai

+0

나는 이것을 사랑한다. – gdbj

-1

쉬운 해결책 : 간단한 타이머가 가장 좋습니다. 너는 너의 타이머를 채우고, 너는 너의 행동을 시작하고, 시간 초과되면 부턴이 켜져 있어야한다. 여기

은 java.util.Timer과 매우 교수형 찾는 exemple입니다 :

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

세웠 전문가 솔루션 : 스윙 작업자가 FutureTask가 호출에 imbeded 그 안에 RunnableFuture는, 당신은 할 수 있습니다 자신의 실 행 프로그램을 사용하여 실 행하십시오 (RunableFuture). 그렇게하려면 익명이 아닌 이름 클래스가있는 SwingWorker가 필요합니다. 자신의 집행자와 이름 수업을 통해 원하는 모든 것을 테스트 할 수 있다고 전문가는 말합니다.

+1

그 시간에 프로세스가 완료되지 않았 으면 타이머를 사용하여 버튼을 다시 활성화하는 이유가 무엇인지, 불필요하게 버튼을 비활성화 된 상태로 유지하는 이유는 없습니다. – Yishai

+0

죄송합니다, 그것은 나쁜 영어입니다. "활성화되어 있어야합니다"는 아니지만 "가능합니다"라고 생각합니다. 일부 자바 exemple 코드를 사용하여 내 대답을 편집합니다. 최선이라고 생각합니다. – Istao

8

현재 구현은 스레딩 문제, 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의 새 구현 만 제공하면됩니다.

관련 문제