如何使这个 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,“creator”是用于写入文件的注入接口(因此该部分正在测试中)。文件写入的位置是有意进行硬编码的,因为这是作为示例。使用 java.util.logging 只是为了避免外部依赖。
您将如何对其进行分块以使其可进行单元测试(包括在需要时放弃 SwingWorker,但随后替换其功能,至少如此处所使用的)。
就我看来,doInBackground 基本上没问题。基本机制是创建一个编写器并关闭它,这几乎太简单而无法测试,真正的工作正在测试中。然而,done 方法存在引用问题,包括它与父类的 actionPerformed 方法的耦合以及协调按钮的启用和禁用。
然而,将其分开并不明显。注入某种 SwingWorkerFactory 会使捕获 GUI 字段的维护变得更加困难(很难看出这将如何改进设计)。 JOpitonPane 和桌面具有单例的所有“优点”,并且异常处理使得不可能轻松地包装 get。
那么测试这段代码的好解决方案是什么?
Consider this code:
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
is a JTextField and 'creator' is an injected interface for writing the file (so that part is under test). The location where the file is written is hard coded on purpose because this is intended as an example. And java.util.logging is used simply to avoid an external dependency.
How would you chunk this up to make it unit-testable (including abandoning SwingWorker if needed, but then replacing its functionality, at least as used here).
The way I look at it, the doInBackground is basically alright. The fundamental mechanics are creating a writer and closing it, which is almost too simple to test and the real work is under test. However, the done method is quote problematic, including its coupling with the actionPerformed method the parent class and coordinating the enabling and disabling of the button.
However, pulling that apart is not obvious. Injecting some kind of SwingWorkerFactory makes capturing the GUI fields a lot harder to maintain (it is hard to see how it would be a design improvement). The JOpitonPane and the Desktop have all the "goodness" of Singletons, and exception handling makes it impossible to wrap the get easily.
So what would be a good solution to bring this code under test?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
恕我直言,这对于匿名类来说很复杂。我的方法是将匿名类重构为如下所示:
这允许独立于 GUI 测试文件写入功能
然后,
actionPerformed
变为如下所示:另外,
的实例>FileWriterWorker.Response
可以分配给变量并独立于FileWriterWorker
进行测试。IMHO, that's complicated for an anonymous class. My approach would be to refactor the anonymous class to something like this:
That allows the file writing functionality to be tested independent of a GUI
Then, the
actionPerformed
becomes something like this:Additionally, the instance of
FileWriterWorker.Response
can be assigned to a variable and tested independent ofFileWriterWorker
.当前的实现将线程问题、UI 和文件写入耦合在一起 - 正如您所发现的,耦合使得很难单独测试各个组件。
这是一个相当长的响应,但它归结为将这三个问题从当前实现中提取到具有已定义接口的单独类中。
分解应用程序逻辑
首先,关注核心应用程序逻辑并将其移至单独的类/接口中。接口允许更轻松地模拟和使用其他摆动线程框架。这种分离意味着您可以完全独立于其他问题来测试应用程序逻辑。
分离 UI
现在应用程序逻辑已分离,然后我们将成功和错误处理分解出来。这意味着无需实际进行文件写入即可测试 UI。特别是,可以测试错误处理,而无需实际引发这些错误。在这里,错误非常简单,但通常有些错误很难引发。通过分离错误处理,还有重用或替换错误处理方式的机会。例如稍后使用 JXErrorPane 。
分离线程
最后,我们还可以使用 FileWriterService 分离线程问题。使用上面的 FileWriteRequest 可以使编码变得更简单。
系统的每个部分都是可单独测试的——应用程序逻辑、表示(成功和错误处理)和线程实现也是一个单独的关注点。
这可能看起来有很多接口,但实现主要是从原始代码中剪切和粘贴的。这些接口提供了使这些类可测试所需的分离。
我不太喜欢 SwingWorker,因此将它们保留在界面后面有助于避免它们产生的混乱代码。它还允许您使用不同的实现来实现单独的 UI/后台线程。例如,要使用 Spin,您只需提供 FileWriterService 的新实现即可。
The current implementation couples together threading concerns, UI and file writing - and as you've discovered that coupling makes it hard to test the individual components in isolation.
This is quite a long response, but it boils down to pulling out these three concerns from the current implementation into separate classes with a defined interface.
Factor out Application Logic
To start with, focus on the core application logic and move that into a separate class/interface. An interface allows easier mocking, and use of other swing-threading frameworks. The separation means you can test your application logic entirely independently from the other concerns.
Separate out UI
With the application logic now separate, we then factor out the success and error handling. This means that the UI can be tested without actually doing the file writing. In particular, error handling can be tested without actually need to provoke those errors. Here, the errors are quite simple, but often some errors can be very difficult to provoke. By separating out the error handling, there is also chance for reuse, or replacing how the errors are handled. E.g. using a JXErrorPane later.
Separate out Threading
Finally, we can also separate out the threading concerns with a FileWriterService. Using a FileWriteRequest above makes coding this simpler.
Each part of the system is separately testable - the application logic, the presentation (success and error handling) and the threading implementation is also a separate concern.
This may seem like a lot of interfaces, but the implementation is mostly cut-and-paste from your original code. The interfaces provide the separation that is needed to make these classes testable.
I'm not much of a fan of SwingWorker's so keeping them behind an interface helps keep the clutter they produce out of the code. It also allows you to use a different implementation for implementing the separate UI/background threads. For example, to use Spin, you only need to provide a new implementation of FileWriterService.
简单的解决方案:一个简单的计时器是最好的;你启动你的计时器,你启动你的actionPerformed,并且在超时时必须启用按钮等等。
这是一个带有 java.util.Timer 的非常小的例子:
假设的专家解决方案: Swing Worker 是一个 RunnableFuture,其中嵌入了一个可调用的 FutureTask,因此您可以使用自己的执行器来启动它(RunableFuture)。为此,您需要一个具有名称类的 SwingWorker,而不是匿名类。这位所谓的专家说,有了自己的执行器和名称类,您就可以测试您想要的所有内容。
Easy solution : a simple timer is best ; you lanch your timer, you launch your actionPerformed, and at the timeout the bouton must be enabled and so on.
Here is an very littel exemple with a java.util.Timer :
Supposed Expert solution : a Swing Worker is a RunnableFuture, inside it a FutureTask imbeded in a callable, so you can use your own executor to launch it (the RunableFuture). To do that, you need a SwingWorker with a name class, not an anonymous. With your own executor and a name class, you can test all you want, the supposed expert says.