2015-04-18 6 views
5

Im Folgenden finden Sie ein kurzes einfaches Beispiel für die Verwendung eines WatchService, um Daten mit einer Datei synchron zu halten. Meine Frage ist, wie man den Code zuverlässig testet. Der Test schlägt gelegentlich fehl, wahrscheinlich aufgrund einer Race-Bedingung zwischen dem os/jvm, das das Ereignis in den Überwachungsdienst bringt, und dem Test-Thread, der den Überwachungsdienst abfragt. Mein Wunsch ist es, den Code einfach, single threaded und nicht blockierend zu halten, aber auch testbar zu sein. Ich mag es nicht, Schlafanrufe beliebiger Länge in den Testcode zu schreiben. Ich hoffe, es gibt eine bessere Lösung.Komponententestcode mit 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; 
} 
} 

Und der aktuelle Test

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

Antwort

1

Das Timing-Problem ist verpflichtet, wegen der Abfrage in dem Uhr-Service geschieht passieren.

Dieser Test ist nicht wirklich ein Komponententest, da er die tatsächliche Implementierung des standardmäßigen Dateisystembeobachters testet.

Wenn ich einen eigenständigen Komponententest für diese Klasse machen wollte, würde ich zuerst die FileWatcher ändern, so dass es nicht auf das Standard-Dateisystem angewiesen ist. Die Art, wie ich dies tun würde wäre, eine WatchService in den Konstruktor anstelle einer FileSystem zu injizieren. Zum Beispiel ...

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

    ... 

in dieser Abhängigkeit Passing statt der Klasse habhaft ein WatchService selbst macht diese Klasse etwas mehr in der Zukunft wiederverwendbar. Zum Beispiel, was ist, wenn Sie eine andere FileSystem Implementierung (wie eine In-Memory-wie https://github.com/google/jimfs) verwenden möchten?

Sie jetzt diese Klasse durch die Abhängigkeiten spöttisch, zum Beispiel testen ...

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

} 

kann ich sehen, warum Sie einen „echten“ Test dafür wollen, die nicht spottet nicht verwendet - in diesem Fall es wäre kein Komponententest und Sie könnten nicht viel Auswahl haben, aber sleep zwischen checks (der JimFS v1.0 Code ist hart codiert, um alle 5 Sekunden abzufragen, haben die Pollzeit auf dem Kern Java FileSystem 's WatchService nicht betrachtet)

Hoffe, das hilft

+0

In Bezug auf die "stinkenden" Bit - alles was ich sagen kann ist "versuchen, statische Anrufe zu vermeiden" !!- Sie könnten immer 'PowerMock' verwenden (was ich zu vermeiden versuche, es sei denn, es ist absolut notwendig) – BretC

+0

Vielleicht ist Komponententest das falsche Wort. Grundsätzlich möchte ich es testen, einschließlich der Interaktion mit dem Dateisystem. Dies ist ein sehr einfaches Beispiel, aber die tatsächliche Verwendung ist ein wenig komplexer. Mein Hauptproblem ist, dass path.register eine undokumentierte magische private Methode benötigt, die das Spotten noch schwieriger macht. Die Funktionalität von WatchService ist großartig, aber die API ist schrecklich, erinnert mich an hässlichen Legacy-Code, nicht neueren Base-Java. Ich möchte ein paar Dinge ausprobieren, und wenn ich nichts besseres bekomme, werde ich diese Antwort akzeptieren und einfach im Test schlafen. – user2133814

2

Ich habe einen Wrapper um WatchService erstellt, um viele Probleme mit der API zu beheben. Es ist jetzt viel mehr testbar. Ich bin nicht sicher über einige der Probleme im Zusammenhang mit Parallelität in PathWatchService, und ich habe es nicht gründlich getestet.

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

Datenklasse:

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 + '}'; 
    } 
} 

Und schließlich den Test, beachten Sie dies nicht eine komplette Einheit Test ist aber zeigt die Art und Weise um Tests für diese Situation zu schreiben.

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