2015-04-18 1 views
5

Ниже приведен краткий простой пример использования WatchService для синхронизации данных с файлом. Мой вопрос в том, как надежно проверить код. Тест иногда не срабатывает, вероятно, из-за состояния гонки между os/jvm, который получает событие в службу часов, и тестовую нить, опросившую службу часов. Мое желание состоит в том, чтобы сохранить код простым, однопоточным и не блокирующим, но также можно проверить. Я сильно не люблю помещать вызовы сна произвольной длины в тестовый код. Я надеюсь, что есть лучшее решение.Единичный тестовый код с WatchService

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

ответ

1

Этот вопрос времени должно произойти из-опроса происходит в службе часов.

Этот тест не является модульным тестом, потому что он проверяет фактическую реализацию наблюдателя файловой системы по умолчанию.

Если бы я хотел сделать автономный модульный тест для этого класса, я бы сначала изменил FileWatcher так, чтобы он не полагался на файловую систему по умолчанию. То, как я сделал бы это, было бы ввести WatchService в конструктор вместо 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(); 
    } 

    ... 

Переходя в этой зависимости вместо класса получения трюме 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 FileSystemWatchService)

Надеюсь, что это поможет

+0

Что касается «вонючего» бита - все, что я могу сказать, это «попытаться избежать статических вызовов» !!- Вы всегда можете использовать «PowerMock» (которого я стараюсь избегать, если это абсолютно необходимо) – BretC

+0

Возможно, юнит-тест - неправильное слово. В основном я хочу протестировать его, включая взаимодействие с файловой системой. Это очень простой пример, но реальное использование довольно сложно. Моя основная проблема заключается в том, что path.register нуждается в незадокументированном волшебном частном методе для работы, что делает насмешкой еще более трудным. Функциональность WatchService велика, но API ужасен, напомнив мне уродливый код устаревшего кода, а не недавнюю базовую java. Я хочу попробовать несколько вещей, и если я не могу получить что-то лучше, я соглашусь с этим ответом и просто сплю в тесте. – user2133814

2

Я создал обертку вокруг WatchService для очистки многих проблем, которые у меня есть с API. Теперь это гораздо более проверяемо. Я не уверен в некоторых проблемах параллелизма в PathWatchService, хотя и не провел тщательного тестирования.

Новый FileWatcher:

public class FileWatcher { 

    private final PathWatchService pathWatchService; 
    private final Path path; 
    private String data; 

    public FileWatcher(PathWatchService pathWatchService, Path path) { 
     this.path = path; 
     this.pathWatchService = pathWatchService; 
     try { 
      this.pathWatchService.register(path.toAbsolutePath().getParent()); 
     } catch (IOException ex) { 
      throw new RuntimeException(ex); 
     } 
     load(); 
    } 

    private void load() { 
     try (BufferedReader br = Files.newBufferedReader(path, Charset.defaultCharset())){ 
      data = br.readLine(); 
     } catch (IOException ex) { 
      data = ""; 
     } 
    } 

    public void update(){ 
     PathEvents pe; 
     while ((pe=pathWatchService.poll()) != null) { 
      for (WatchEvent we : pe.getEvents()){ 
       if (path.equals(we.context())){ 
        load(); 
        return; 
       } 
      } 
     } 
    } 

    public String getData(){ 
     update(); 
     return data; 
    } 
} 

Wrapper:

public class PathWatchService implements AutoCloseable { 

    private final WatchService watchService; 
    private final BiMap<WatchKey, Path> watchKeyToPath = HashBiMap.create(); 
    private final ReadWriteLock lock = new ReentrantReadWriteLock(); 
    private final Queue<WatchKey> invalidKeys = new ConcurrentLinkedQueue<>(); 

    /** 
    * Constructor. 
    */ 
    public PathWatchService() { 
     try { 
      watchService = FileSystems.getDefault().newWatchService(); 
     } catch (IOException ex) { 
      throw new RuntimeException(ex); 
     } 
    } 

    /** 
    * Register the input path with the WatchService for all 
    * StandardWatchEventKinds. Registering a path which is already being 
    * watched has no effect. 
    * 
    * @param path 
    * @return 
    * @throws IOException 
    */ 
    public void register(Path path) throws IOException { 
     register(path, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY); 
    } 

    /** 
    * Register the input path with the WatchService for the input event kinds. 
    * Registering a path which is already being watched has no effect. 
    * 
    * @param path 
    * @param kinds 
    * @return 
    * @throws IOException 
    */ 
    public void register(Path path, WatchEvent.Kind... kinds) throws IOException { 
     try { 
      lock.writeLock().lock(); 
      removeInvalidKeys(); 
      WatchKey key = watchKeyToPath.inverse().get(path); 
      if (key == null) { 
       key = path.register(watchService, kinds); 
       watchKeyToPath.put(key, path); 
      } 
     } finally { 
      lock.writeLock().unlock(); 
     } 
    } 

    /** 
    * Close the WatchService. 
    * 
    * @throws IOException 
    */ 
    @Override 
    public void close() throws IOException { 
     try { 
      lock.writeLock().lock(); 
      watchService.close(); 
      watchKeyToPath.clear(); 
      invalidKeys.clear(); 
     } finally { 
      lock.writeLock().unlock(); 
     } 
    } 

    /** 
    * Retrieves and removes the next PathEvents object, or returns null if none 
    * are present. 
    * 
    * @return 
    */ 
    public PathEvents poll() { 
     return keyToPathEvents(watchService.poll()); 
    } 

    /** 
    * Return a PathEvents object from the input key. 
    * 
    * @param key 
    * @return 
    */ 
    private PathEvents keyToPathEvents(WatchKey key) { 
     if (key == null) { 
      return null; 
     } 
     try { 
      lock.readLock().lock(); 
      Path watched = watchKeyToPath.get(key); 
      List<WatchEvent<Path>> events = new ArrayList<>(); 
      for (WatchEvent e : key.pollEvents()) { 
       events.add((WatchEvent<Path>) e); 
      } 
      boolean isValid = key.reset(); 
      if (isValid == false) { 
       invalidKeys.add(key); 
      } 
      return new PathEvents(watched, events, isValid); 
     } finally { 
      lock.readLock().unlock(); 
     } 
    } 

    /** 
    * Retrieves and removes the next PathEvents object, waiting if necessary up 
    * to the specified wait time, returns null if none are present after the 
    * specified wait time. 
    * 
    * @return 
    */ 
    public PathEvents poll(long timeout, TimeUnit unit) throws InterruptedException { 
     return keyToPathEvents(watchService.poll(timeout, unit)); 
    } 

    /** 
    * Retrieves and removes the next PathEvents object, waiting if none are yet 
    * present. 
    * 
    * @return 
    */ 
    public PathEvents take() throws InterruptedException { 
     return keyToPathEvents(watchService.take()); 
    } 

    /** 
    * Get all paths currently being watched. Any paths which were watched but 
    * have invalid keys are not returned. 
    * 
    * @return 
    */ 
    public Set<Path> getWatchedPaths() { 
     try { 
      lock.readLock().lock(); 
      Set<Path> paths = new HashSet<>(watchKeyToPath.inverse().keySet()); 
      WatchKey key; 
      while ((key = invalidKeys.poll()) != null) { 
       paths.remove(watchKeyToPath.get(key)); 
      } 
      return paths; 
     } finally { 
      lock.readLock().unlock(); 
     } 
    } 

    /** 
    * Cancel watching the specified path. Cancelling a path which is not being 
    * watched has no effect. 
    * 
    * @param path 
    */ 
    public void cancel(Path path) { 
     try { 
      lock.writeLock().lock(); 
      removeInvalidKeys(); 
      WatchKey key = watchKeyToPath.inverse().remove(path); 
      if (key != null) { 
       key.cancel(); 
      } 
     } finally { 
      lock.writeLock().unlock(); 
     } 
    } 

    /** 
    * Removes any invalid keys from internal data structures. Note this 
    * operation is also performed during register and cancel calls. 
    */ 
    public void cleanUp() { 
     try { 
      lock.writeLock().lock(); 
      removeInvalidKeys(); 
     } finally { 
      lock.writeLock().unlock(); 
     } 
    } 

    /** 
    * Clean up method to remove invalid keys, must be called from inside an 
    * acquired write lock. 
    */ 
    private void removeInvalidKeys() { 
     WatchKey key; 
     while ((key = invalidKeys.poll()) != null) { 
      watchKeyToPath.remove(key); 
     } 
    } 
} 

класс данных:

public class PathEvents { 

    private final Path watched; 
    private final ImmutableList<WatchEvent<Path>> events; 
    private final boolean isValid; 

    /** 
    * Constructor. 
    * 
    * @param watched 
    * @param events 
    * @param isValid 
    */ 
    public PathEvents(Path watched, List<WatchEvent<Path>> events, boolean isValid) { 
     this.watched = watched; 
     this.events = ImmutableList.copyOf(events); 
     this.isValid = isValid; 
    } 

    /** 
    * Return an immutable list of WatchEvent's. 
    * @return 
    */ 
    public List<WatchEvent<Path>> getEvents() { 
     return events; 
    } 

    /** 
    * True if the watched path is valid. 
    * @return 
    */ 
    public boolean isIsValid() { 
     return isValid; 
    } 

    /** 
    * Return the path being watched in which these events occurred. 
    * 
    * @return 
    */ 
    public Path getWatched() { 
     return watched; 
    } 

    @Override 
    public boolean equals(Object obj) { 
     if (obj == null) { 
      return false; 
     } 
     if (getClass() != obj.getClass()) { 
      return false; 
     } 
     final PathEvents other = (PathEvents) obj; 
     if (!Objects.equals(this.watched, other.watched)) { 
      return false; 
     } 
     if (!Objects.equals(this.events, other.events)) { 
      return false; 
     } 
     if (this.isValid != other.isValid) { 
      return false; 
     } 
     return true; 
    } 

    @Override 
    public int hashCode() { 
     int hash = 7; 
     hash = 71 * hash + Objects.hashCode(this.watched); 
     hash = 71 * hash + Objects.hashCode(this.events); 
     hash = 71 * hash + (this.isValid ? 1 : 0); 
     return hash; 
    } 

    @Override 
    public String toString() { 
     return "PathEvents{" + "watched=" + watched + ", events=" + events + ", isValid=" + isValid + '}'; 
    } 
} 

И, наконец, тест, обратите внимание, что это не полный тестовый модуль, но демонстрирует путь написать тесты для этой ситуации.

public class FileWatcherTest { 

    public FileWatcherTest() { 
    } 
    Path path = Paths.get("myFile.txt"); 
    Path parent = path.toAbsolutePath().getParent(); 

    private void write(String s) throws IOException { 
     try (BufferedWriter bw = Files.newBufferedWriter(path, Charset.defaultCharset())) { 
      bw.write(s); 
     } 
    } 

    @Test 
    public void test() throws IOException, InterruptedException{ 
     write("hello"); 

     PathWatchService real = new PathWatchService(); 
     real.register(parent); 
     PathWatchService mock = mock(PathWatchService.class); 

     FileWatcher fileWatcher = new FileWatcher(mock, path); 
     verify(mock).register(parent); 
     Assert.assertEquals("hello", fileWatcher.getData()); 

     write("goodbye"); 
     PathEvents pe = real.poll(10, TimeUnit.SECONDS); 
     if (pe == null){ 
      Assert.fail("Should have an event for writing good bye"); 
     } 
     when(mock.poll()).thenReturn(pe).thenReturn(null); 

     Assert.assertEquals("goodbye", fileWatcher.getData()); 
    } 
}