下面是一个使用 WatchService 使数据与文件保持同步的简短示例。我的问题是如何可靠地测试代码。测试偶尔会失败,可能是因为 os/jvm 将事件放入监视服务和测试线程轮询监视服务之间存在竞争条件。我的愿望是保持代码简单、单线程和非阻塞,但也可测试。我非常不喜欢将任意长度的 sleep 调用放入测试代码中。我希望有更好的解决方案。
public class FileWatcher {
private final WatchService watchService;
private final Path path;
private String data;
public FileWatcher(Path path){
this.path = path;
try {
watchService = FileSystems.getDefault().newWatchService();
path.toAbsolutePath().getParent().register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
load();
}
private void load() {
try (BufferedReader br = Files.newBufferedReader(path, Charset.defaultCharset())){
data = br.readLine();
} catch (IOException ex) {
data = "";
}
}
private void update(){
WatchKey key;
while ((key=watchService.poll()) != null) {
for (WatchEvent<?> e : key.pollEvents()) {
WatchEvent<Path> event = (WatchEvent<Path>) e;
if (path.equals(event.context())){
load();
break;
}
}
key.reset();
}
}
public String getData(){
update();
return data;
}
}
和当前测试
public class FileWatcherTest {
public FileWatcherTest() {
}
Path path = Paths.get("myFile.txt");
private void write(String s) throws IOException{
try (BufferedWriter bw = Files.newBufferedWriter(path, Charset.defaultCharset())) {
bw.write(s);
}
}
@Test
public void test() throws IOException{
for (int i=0; i<100; i++){
write("hello");
FileWatcher fw = new FileWatcher(path);
Assert.assertEquals("hello", fw.getData());
write("goodbye");
Assert.assertEquals("goodbye", fw.getData());
}
}
}
最佳答案
这个计时问题必然会发生,因为在 watch 服务中进行轮询。
这个测试并不是真正的单元测试,因为它是在测试默认文件系统观察器的实际实现。
如果我想为这个类做一个独立的单元测试,我会先修改FileWatcher
,让它不依赖于默认的文件系统。我这样做的方法是将 WatchService
而不是 FileSystem
注入(inject)到构造函数中。例如……
public class FileWatcher {
private final WatchService watchService;
private final Path path;
private String data;
public FileWatcher(WatchService watchService, Path path) {
this.path = path;
try {
this.watchService = watchService;
path.toAbsolutePath().getParent().register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
load();
}
...
传入此依赖项而不是类自己获取 WatchService
使此类在未来更容易重用。例如,如果您想使用不同的 FileSystem
实现(例如内存中的 https://github.com/google/jimfs )怎么办?
您现在可以通过模拟依赖项来测试此类,例如...
import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
import static org.fest.assertions.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.file.FileSystem;
import java.nio.file.Path;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.spi.FileSystemProvider;
import java.util.Arrays;
import org.junit.Before;
import org.junit.Test;
public class FileWatcherTest {
private FileWatcher fileWatcher;
private WatchService watchService;
private Path path;
@Before
public void setup() throws Exception {
// Set up mock watch service and path
watchService = mock(WatchService.class);
path = mock(Path.class);
// Need to also set up mocks for absolute parent path...
Path absolutePath = mock(Path.class);
Path parentPath = mock(Path.class);
// Mock the path's methods...
when(path.toAbsolutePath()).thenReturn(absolutePath);
when(absolutePath.getParent()).thenReturn(parentPath);
// Mock enough of the path so that it can load the test file.
// On the first load, the loaded data will be "[INITIAL DATA]", any subsequent call it will be "[UPDATED DATA]"
// (this is probably the smellyest bit of this test...)
InputStream initialInputStream = createInputStream("[INITIAL DATA]");
InputStream updatedInputStream = createInputStream("[UPDATED DATA]");
FileSystem fileSystem = mock(FileSystem.class);
FileSystemProvider fileSystemProvider = mock(FileSystemProvider.class);
when(path.getFileSystem()).thenReturn(fileSystem);
when(fileSystem.provider()).thenReturn(fileSystemProvider);
when(fileSystemProvider.newInputStream(path)).thenReturn(initialInputStream, updatedInputStream);
// (end smelly bit)
// Create the watcher - this should load initial data immediately
fileWatcher = new FileWatcher(watchService, path);
// Verify that the watch service was registered with the parent path...
verify(parentPath).register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
}
@Test
public void shouldReturnCurrentStateIfNoChanges() {
// Check to see if the initial data is returned if the watch service returns null on poll...
when(watchService.poll()).thenReturn(null);
assertThat(fileWatcher.getData()).isEqualTo("[INITIAL DATA]");
}
@Test
public void shouldLoadNewStateIfFileChanged() {
// Check that the updated data is loaded when the watch service says the path we are interested in has changed on poll...
WatchKey watchKey = mock(WatchKey.class);
@SuppressWarnings("unchecked")
WatchEvent<Path> pathChangedEvent = mock(WatchEvent.class);
when(pathChangedEvent.context()).thenReturn(path);
when(watchKey.pollEvents()).thenReturn(Arrays.asList(pathChangedEvent));
when(watchService.poll()).thenReturn(watchKey, (WatchKey) null);
assertThat(fileWatcher.getData()).isEqualTo("[UPDATED DATA]");
}
@Test
public void shouldKeepCurrentStateIfADifferentPathChanged() {
// Make sure nothing happens if a different path is updated...
WatchKey watchKey = mock(WatchKey.class);
@SuppressWarnings("unchecked")
WatchEvent<Path> pathChangedEvent = mock(WatchEvent.class);
when(pathChangedEvent.context()).thenReturn(mock(Path.class));
when(watchKey.pollEvents()).thenReturn(Arrays.asList(pathChangedEvent));
when(watchService.poll()).thenReturn(watchKey, (WatchKey) null);
assertThat(fileWatcher.getData()).isEqualTo("[INITIAL DATA]");
}
private InputStream createInputStream(String string) {
return new ByteArrayInputStream(string.getBytes());
}
}
我明白为什么您可能想要一个不使用模拟的“真实”测试 - 在这种情况下,它不会是单元测试,您可能别无选择,只能 sleep
检查之间(JimFS v1.0 代码被硬编码为每 5 秒轮询一次,没有查看核心 Java FileSystem
的 WatchService
上的轮询时间)
希望对你有帮助
关于java - 使用 WatchService 的单元测试代码,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/29719932/