java - ¿Cómo se puede hacer comprobable este código SwingWorker?
unit-testing tdd (3)
En mi humilde opinión, eso es complicado para una clase anónima. Mi enfoque sería refactorizar la clase anónima a algo como esto:
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);
}
}
}
Eso permite que la funcionalidad de escritura de archivos se pruebe independientemente de una GUI
Entonces, el actionPerformed
convierte en algo como esto:
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();
}
Además, la instancia de FileWriterWorker.Response
puede asignarse a una variable y probarse independientemente de FileWriterWorker
.
Considere este código:
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
es un JTextField y ''creator'' es una interfaz inyectada para escribir el archivo (por lo que esa parte está bajo prueba). La ubicación en la que se escribe el archivo está codificada a propósito, ya que se trata de un ejemplo. Y java.util.logging se usa simplemente para evitar una dependencia externa.
¿Cómo dividiría esto para que sea comprobable por unidad (incluido el abandono de SwingWorker si es necesario, pero luego reemplaza su funcionalidad, al menos como se usa aquí)?
La forma en que lo veo, el fondo de acción es básicamente correcto. La mecánica fundamental es crear un escritor y cerrarlo, lo cual es casi demasiado simple de probar y el trabajo real está bajo prueba. Sin embargo, el método realizado es una cita problemática, incluido su acoplamiento con el método ActionPerformed de la clase principal y la coordinación para habilitar y deshabilitar el botón.
Sin embargo, separar eso no es obvio. La inyección de algún tipo de SwingWorkerFactory hace que la captura de los campos de la GUI sea mucho más difícil de mantener (es difícil ver cómo sería una mejora de diseño). El JOpitonPane y el Escritorio tienen todas las "bondades" de Singletons, y el manejo de excepciones hace que sea imposible envolverlas fácilmente.
Entonces, ¿cuál sería una buena solución para poner a prueba este código?
La implementación actual combina los problemas de subprocesos, la IU y la escritura de archivos, y como ha descubierto, el acoplamiento dificulta la prueba de los componentes individuales de forma aislada.
Esta es una respuesta bastante larga, pero se reduce a sacar estas tres preocupaciones de la implementación actual en clases separadas con una interfaz definida.
Factorizar la lógica de la aplicación
Para empezar, concéntrese en la lógica de la aplicación central y muévala a una clase / interfaz separada. Una interfaz permite una burla más fácil, y el uso de otros marcos de oscilación de subprocesos. La separación significa que puede probar la lógica de su aplicación de forma totalmente independiente de las otras preocupaciones.
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);
}
}
Separar la interfaz de usuario
Ahora que la lógica de la aplicación está separada, factorizamos el éxito y el manejo de errores. Esto significa que la interfaz de usuario se puede probar sin hacer realmente la escritura del archivo. En particular, el manejo de errores se puede probar sin necesidad de provocarlos. Aquí, los errores son bastante simples, pero a menudo algunos errores pueden ser muy difíciles de provocar. Al separar el manejo de errores, también existe la posibilidad de volver a usarlos o de reemplazar cómo se manejan los errores. Por ejemplo, utilizando un JXErrorPane más tarde.
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);
}
}
Separación de hilos
Finalmente, también podemos separar las preocupaciones de los hilos con un FileWriterService. El uso de un FileWriteRequest anterior hace que la codificación sea más sencilla.
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);
}
}
}
}
Cada parte del sistema se puede probar por separado: la lógica de la aplicación, la presentación (manejo de errores y éxito) y la implementación de subprocesos también son una preocupación independiente.
Esto puede parecer una gran cantidad de interfaces, pero la implementación es principalmente de cortar y pegar desde su código original. Las interfaces proporcionan la separación que se necesita para que estas clases sean comprobables.
No soy muy fanático de SwingWorker''s, por lo que mantenerlos detrás de una interfaz ayuda a mantener el desorden que producen fuera del código. También le permite usar una implementación diferente para implementar los hilos de UI / fondo separados. Por ejemplo, para usar Spin , solo necesita proporcionar una nueva implementación de FileWriterService.
Solución fácil: un temporizador simple es mejor; enciende su temporizador, inicia su ActionPerformed, y en el tiempo de espera debe habilitarse el bouton y así sucesivamente.
Aquí hay un ejemplo muy pequeño 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();
}
}
Supuesta solución experta: un Swing Worker es un RunnableFuture, en su interior una FutureTask incrustada en un invocable, por lo que puede usar su propio ejecutor para iniciarlo (el RunableFuture). Para hacer eso, necesita un SwingWorker con una clase de nombre, no un anónimo. Con su propio ejecutor y una clase de nombre, puede probar todo lo que quiera, dice el supuesto experto.