使用WatchService进行单元测试代码

use*_*814 5 java unit-testing watchservice

下面是一个使用WatchService保持数据与文件同步的简单示例.我的问题是如何可靠地测试代码.测试偶尔会失败,可能是因为os/jvm将事件带入监视服务和测试线程轮询监视服务之间的竞争条件.我的愿望是保持代码简单,单线程和非阻塞,但也可以测试.我强烈不喜欢将任意长度的睡眠调用放入测试代码中.我希望有更好的解决方案.

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;
}
}
Run Code Online (Sandbox Code Playgroud)

而目前的测试

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());
    }
}
}
Run Code Online (Sandbox Code Playgroud)

Bre*_*etC 5

由于监视服务中发生了轮询,因此必然会发生此计时问题。

这个测试并不是真正的单元测试,因为它正在测试默认文件系统观察器的实际实现。

如果我想为这个类做一个独立的单元测试,我会首先修改FileWatcher它,使它不依赖于默认的文件系统。我这样做的方法是将 aWatchService注入构造函数而不是FileSystem. 例如...

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

    ...
Run Code Online (Sandbox Code Playgroud)

传入这个依赖项而不是类自己获取 aWatchService使这个类在未来更容易重用。例如,如果您想使用不同的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());
    }

}
Run Code Online (Sandbox Code Playgroud)

我可以理解为什么您可能想要一个不使用模拟的“真实”测试 - 在这种情况下,它不会是一个单元测试,并且您可能没有太多选择,只能sleep在检查之间进行(JimFS v1.0 代码很难编码为每5秒轮询,没有看过投票时间对核心JavaFileSystem的WatchService)

希望这可以帮助