Notifications and recorded calls
You can verify which changes the code under test made to a MockFileSystem: the notifications it raised, the events
of a single watcher, and the method calls and property accesses it recorded.
The samples use the TimeSpan helpers (100.Milliseconds()) from
aweXpect.Chronology.
File-system notifications
A MockFileSystem raises notifications when files or directories change. You can verify that the code under test
triggered a notification:
MockFileSystem fileSystem = new();
fileSystem.File.WriteAllText("let-it-be.txt", "let it be");
await Expect.That(fileSystem).TriggeredNotification();
await Expect.That(fileSystem).TriggeredNotification(c => c.Name == "let-it-be.txt");
Without .Within(timeout), the expectation checks only the notifications triggered so far and does not wait. With
.Within(timeout) it also waits for asynchronous notifications: it completes as soon as the quantifier is decided,
otherwise when the timeout elapses, so an upper bound like .Never() or .AtMost(…) waits for the full timeout:
_ = Task.Run(() => fileSystem.File.WriteAllText("help.txt", "help"));
await Expect.That(fileSystem).TriggeredNotification().Within(100.Milliseconds());
For example, Expect.That(fileSystem).TriggeredNotification(c => c.Name == "yesterday.txt").Within(100.Milliseconds())
fails with:
Expected that fileSystem
has triggered a notification matching c => c.Name == "yesterday.txt" at least once within 0:00.100,
but it was not triggered
DidNotTriggerNotification has the same overloads. With .Within(timeout) it waits for the full timeout and fails as
soon as a matching notification is triggered:
await Expect.That(fileSystem).DidNotTriggerNotification(c => c.Name == "free-as-a-bird.txt");
await Expect.That(fileSystem).DidNotTriggerNotification(c => c.Name == "free-as-a-bird.txt")
.Within(100.Milliseconds());
A timeout of the evaluation (e.g. .WithTimeout(…)) that is not shorter than .Within(timeout) lets the
notifications received until then decide; a cancellation before that leaves the expectation inconclusive.
Timeout.InfiniteTimeSpan waits until the quantifier is decided.
Both accept a quantifier (AtLeast, AtMost, Exactly, Between, Never, Once) to verify how often the
notification was triggered, and a .Matching(c => …) filter with the
change description expectations for each notification:
fileSystem.File.WriteAllText("come-together.txt", "come together");
fileSystem.File.WriteAllText("something.txt", "something");
await Expect.That(fileSystem).TriggeredNotification(c => c.ChangeType == WatcherChangeTypes.Created)
.Exactly(2.Times());
await Expect.That(fileSystem)
.TriggeredNotification()
.Matching(c => c.HasName("come-together.txt").And.HasChangeType(WatcherChangeTypes.Created))
.Exactly(1.Times());
These expectations replay the notification history of the
MockFileSystem. They throw on a file system created withnew MockFileSystem(o => o.WithoutNotificationHistory()).
Watcher events
You can verify the events of a single IFileSystemWatcher. The watcher must come from a MockFileSystem and have
EnableRaisingEvents set to true. Only events of this watcher count; events of other watchers on the same
MockFileSystem are ignored.
MockFileSystem fileSystem = new();
fileSystem.InitializeIn("/music");
using IFileSystemWatcher watcher = fileSystem.FileSystemWatcher.New("/music");
watcher.EnableRaisingEvents = true;
fileSystem.File.WriteAllText("let-it-be.txt", "let it be");
await Expect.That(watcher).Triggered().Within(1.Seconds());
await Expect.That(watcher).Triggered(c => c.Name == "let-it-be.txt");
await Expect.That(watcher).DidNotTrigger(c => c.Name == "free-as-a-bird.txt").Within(100.Milliseconds());
The mock raises the events of a watcher asynchronously, like a real FileSystemWatcher, so an event of a change that
just completed may not be raised yet. Without .Within(timeout), only the events raised so far count; use it to wait
for them. Triggered() completes as soon as a matching event is raised, so a generous timeout only costs time when the
expectation fails.
Triggered and DidNotTrigger support the same quantifiers, .Within(timeout) and .Matching(c => …) filter as the
notification expectations:
await Expect.That(watcher)
.Triggered()
.Matching(c => c.HasName("let-it-be.txt").And.HasChangeType(WatcherChangeTypes.Created))
.Exactly(1.Times());
Change descriptions
You can verify a single ChangeDescription:
await Expect.That(change).HasChangeType(WatcherChangeTypes.Created);
await Expect.That(change).DoesNotHaveChangeType(WatcherChangeTypes.Deleted);
await Expect.That(change).HasFileSystemType(FileSystemTypes.File);
await Expect.That(change).HasNotifyFilters(NotifyFilters.LastWrite);
await Expect.That(change).HasName("let-it-be.txt").And.HasPath("/music/let-it-be.txt");
await Expect.That(renamedChange).HasOldName("scrambled-eggs.txt").And.HasOldPath("/music/scrambled-eggs.txt");
HasChangeType, HasFileSystemType and HasNotifyFilters check whether the flag is contained, so a
LastWrite | FileName change satisfies HasNotifyFilters(NotifyFilters.LastWrite). The empty (default) value throws
an ArgumentException, because it would be contained in every value.
Recorded calls
MockFileSystem.Statistics records every method call and property access on the mock. You can verify what the code
under test called with .Recorded(), which mirrors the IFileSystem API:
MockFileSystem fileSystem = new();
fileSystem.File.WriteAllText("help.txt", "help");
await Expect.That(fileSystem.Statistics).Recorded().File.WriteAllText().Once();
await Expect.That(fileSystem.Statistics).Recorded().File.WriteAllText(path: p => p == "help.txt").Once();
For example, if help.txt was written twice, Expect.That(fileSystem.Statistics).Recorded().File.WriteAllText().Once()
fails with:
Expected that fileSystem.Statistics
has recorded a call to File.WriteAllText exactly once,
but it was recorded 2 times
The mirror has one entry per IFileSystem member (.File, .Directory, .FileInfo[path], .DirectoryInfo[path],
.DriveInfo, .FileStream, .FileSystemWatcher, .FileVersionInfo, .Path), with one method per underlying API and
an indexer ([path]) for the instances of a path. Every result supports the quantifiers Once, Twice, Never,
Exactly, AtLeast, AtMost, Between, …
As for every quantifier in this package, only one can be specified: chaining two, e.g. .Once().Twice(), throws an
InvalidOperationException; use Between(minimum).And(maximum) for a range.
Property reads and writes are recorded with .Get() and .Set():
fileSystem.FileInfo.New("help.txt").IsReadOnly = true;
await Expect.That(fileSystem.Statistics).Recorded().FileInfo["help.txt"].IsReadOnly.Set().Once();
await Expect.That(fileSystem.Statistics).Recorded().DirectoryInfo["beatles"].Exists.Get().AtLeast().Once();
Each parameter of a mirror method is an optional Func<T, bool> predicate that is matched by position against the
recorded arguments:
- Without a predicate (or with
null) the position is skipped and every overload matches, so.File.Open()counts allOpencalls regardless of their arguments. - A predicate at a position beyond the number of parameters of an overload excludes that overload, so filtering
recursiveonDirectory.Deleteonly matches the overload with two parameters. - A predicate whose type differs from the recorded type at that position excludes that overload, so filtering
searchOptiononDirectory.EnumerateDirectoriesnever matches theEnumerationOptionsoverload.
A few methods cannot be filtered completely by position, because two of their overloads have different types at the
same position (File.Open / FileInfo.Open with FileStreamOptions, FileSystemWatcher.WaitForChanged with
TimeSpan).