2015-04-18 26 views
5

Dưới đây là một ví dụ đơn giản ngắn về cách sử dụng WatchService để giữ cho dữ liệu được đồng bộ hóa với một tệp. Câu hỏi của tôi là cách kiểm tra mã một cách đáng tin cậy. Kiểm tra không thành công đôi khi, có thể là do điều kiện cuộc đua giữa os/jvm nhận sự kiện vào dịch vụ đồng hồ và chuỗi thử nghiệm bỏ phiếu cho dịch vụ đồng hồ. Mong muốn của tôi là giữ cho mã đơn giản, đơn luồng và không bị chặn mà còn có thể kiểm tra được. Tôi mạnh mẽ không thích đặt các cuộc gọi ngủ có độ dài tùy ý vào mã kiểm tra. Tôi hy vọng có một giải pháp tốt hơn.Mã thử nghiệm đơn vị với 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; 
} 
} 

Và các thử nghiệm hiện

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

Trả lời

1

vấn đề thời gian này chắc chắn sẽ xảy ra vì sự bỏ phiếu diễn ra trong các dịch vụ đồng hồ.

Thử nghiệm này không thực sự là một thử nghiệm đơn vị vì nó đang kiểm tra việc triển khai thực tế của trình theo dõi hệ thống tệp mặc định.

Nếu tôi muốn thực hiện kiểm tra đơn vị độc lập cho lớp này, trước tiên tôi sẽ sửa đổi FileWatcher để nó không dựa vào hệ thống tệp mặc định. Cách tôi sẽ làm điều này sẽ là tiêm một WatchService vào hàm tạo thay vì FileSystem. Ví dụ ...

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

    ... 

Đi qua trong sự phụ thuộc này thay vì các tổ chức lớp nhận của một WatchService bởi chính nó làm cho lớp này một chút tái sử dụng nhiều hơn trong tương lai. Ví dụ: nếu bạn muốn sử dụng triển khai FileSystem khác nhau (chẳng hạn như một bộ nhớ trong như https://github.com/google/jimfs)?

Bây giờ bạn có thể kiểm tra lớp này bằng cách chế giễu sự phụ thuộc, ví dụ ...

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

} 

tôi có thể thấy lý do tại sao bạn có thể muốn có một "thực" thử nghiệm cho việc này mà không sử dụng mocks - trong trường hợp này nó sẽ không phải là một thử nghiệm đơn vị và bạn có thể không có nhiều sự lựa chọn, nhưng để sleep giữa các kiểm tra (mã JimFS v1.0 được mã hóa cứng để thăm dò ý kiến ​​cứ 5 giây một lần, không xem xét thời gian thăm dò trên WatchService của Java FileSystem)

Hy vọng điều này sẽ giúp

+0

Về bit "có mùi" - tất cả những gì tôi có thể nói là "cố gắng tránh các cuộc gọi tĩnh" !!- Bạn luôn có thể sử dụng 'PowerMock' (mà tôi cố gắng tránh trừ khi hoàn toàn cần thiết) – BretC

+0

Có thể kiểm tra đơn vị là từ sai. Về cơ bản tôi muốn thử nghiệm nó, bao gồm cả sự tương tác với hệ thống tập tin. Đây là một ví dụ rất đơn giản, nhưng việc sử dụng thực sự phức tạp hơn một chút. Vấn đề chính của tôi là path.register cần một phương pháp riêng tư ma thuật không có giấy tờ để làm việc, điều này làm cho việc chế nhạo thậm chí còn khó khăn hơn. Các chức năng của WatchService là tuyệt vời nhưng API là khủng khiếp, nhắc nhở tôi về mã di sản xấu xí, không phải java cơ sở gần đây. Tôi muốn thử một vài điều, và nếu tôi không thể có được bất cứ điều gì tốt hơn tôi sẽ chấp nhận câu trả lời này và chỉ ngủ trong bài kiểm tra. – user2133814

2

Tôi đã tạo một trình bao bọc xung quanh WatchService để dọn sạch nhiều vấn đề tôi có với API. Nó bây giờ là nhiều hơn nữa testable. Tôi không chắc chắn về một số vấn đề tương tranh trong PathWatchService mặc dù và tôi đã không thực hiện kiểm tra kỹ lưỡng của nó.

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

lớp dữ liệu:

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

Và cuối cùng là kiểm tra, lưu ý đây không phải là một thử nghiệm đơn vị hoàn chỉnh nhưng chứng tỏ đường đi để viết các bài kiểm tra cho tình huống này.

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()); 
    } 
} 
Các vấn đề liên quan