Drop-in aweXpect expectations for the
file-system and time-system mocks from
Testably.Abstractions:
MockFileSystem, IFileInfo, IDirectoryInfo, IDriveInfo,
IFileVersionInfo, IFileSystemWatcher, IFileSystemStatistics and
ITimerMock.
Verify that a file, directory or drive is present in the file system. Every
positive assertion has a DoesNot… counterpart:
IFileSystem fileSystem = new MockFileSystem();
fileSystem.Directory.CreateDirectory("my/path");
fileSystem.File.WriteAllText("my-file.txt", "some content");
await That(fileSystem).HasDirectory("my/path");
await That(fileSystem).HasFile("my-file.txt");
await That(fileSystem).DoesNotHaveDirectory("not/here");
await That(fileSystem).DoesNotHaveFile("missing.txt");HasFile(path) returns a result that lets you chain assertions about the
file's content and timestamps without re-resolving it:
IFileSystem fileSystem = new MockFileSystem();
fileSystem.File.WriteAllText("my-file.txt", "some content");
await That(fileSystem).HasFile("my-file.txt").WithContent("some content").IgnoringCase();
await That(fileSystem).HasFile("my-file.txt").WithContent().NotEqualTo("some unexpected content");
await That(fileSystem).HasFile("my-file.txt").WithContent(new byte[] { 0x73, 0x6F, 0x6D, 0x65 });You can compare against another file on the same file system:
fileSystem.File.WriteAllText("my-other-file.txt", "SOME CONTENT");
fileSystem.File.WriteAllText("my-third-file.txt", "some other content");
await That(fileSystem).HasFile("my-file.txt").WithContent().SameAs("my-other-file.txt").IgnoringCase();
await That(fileSystem).HasFile("my-file.txt").WithContent().NotSameAs("my-third-file.txt");…and against the file's timestamps. .Within(tolerance) widens the comparison
to a window:
await That(fileSystem).HasFile("my-file.txt").WithCreationTime(DateTime.Now).Within(1.Second());
await That(fileSystem).HasFile("my-file.txt").WithLastAccessTime(DateTime.Now).Within(1.Second());
await That(fileSystem).HasFile("my-file.txt").WithLastWriteTime(DateTime.Now).Within(1.Second());HasDirectory(path) exposes sub-collections:
IFileSystem fileSystem = new MockFileSystem();
fileSystem.Directory.CreateDirectory("foo/bar1");
fileSystem.Directory.CreateDirectory("foo/bar2/baz");
fileSystem.File.WriteAllText("foo/bar/my-file.txt", "some content");
await That(fileSystem).HasDirectory("foo").WithDirectories(d => d.HasCount().EqualTo(2));
await That(fileSystem).HasDirectory("foo/bar").WithFiles(f => f
.All().ComplyWith(x => x.HasContent("SOME CONTENT").IgnoringCase()));HasFile, HasDirectory and HasDrive each expose a .Which property that
returns the resolved IFileInfo / IDirectoryInfo / IDriveInfo so the
subject-level assertions below light up directly in the chain:
await That(fileSystem).HasFile("my-file.txt").Which.HasLength(12).And.HasContent("some content");
await That(fileSystem).HasDirectory("logs").Which.IsEmpty();
await That(fileSystem).HasDrive("D:\\").Which.IsReady().And.HasDriveFormat("NTFS");IFileInfo fileInfo = fileSystem.FileInfo.New("my-file.txt");
await That(fileInfo).Exists();
await That(fileInfo).DoesNotExist();
await That(fileInfo).HasName("my-file.txt");
await That(fileInfo).HasExtension(".txt");
await That(fileInfo).HasLength(12);
await That(fileInfo).HasContent("some content");
await That(fileInfo).HasContent(new byte[] { 0x73, 0x6F, 0x6D, 0x65 });
await That(fileInfo).IsReadOnly();
await That(fileInfo).IsNotReadOnly();
await That(fileInfo).HasAttribute(FileAttributes.ReadOnly);
await That(fileInfo).DoesNotHaveAttribute(FileAttributes.Hidden);
await That(fileInfo).HasCreationTime(DateTime.Now).Within(1.Second());
await That(fileInfo).HasLastAccessTime(DateTime.Now).Within(1.Second());
await That(fileInfo).HasLastWriteTime(DateTime.Now).Within(1.Second());HasAttribute / DoesNotHaveAttribute use flag containment, so
FileAttributes.ReadOnly | FileAttributes.Hidden satisfies
HasAttribute(FileAttributes.ReadOnly). The empty / default value is
rejected with an ArgumentException to avoid silent passes.
On .NET 10 or later, WhoseParent switches the subject to the containing
directory so the directory-level assertions can be reused:
await That(fileInfo).WhoseParent.HasName("docs").And.IsNotEmpty();IDirectoryInfo dirInfo = fileSystem.DirectoryInfo.New("foo");
await That(dirInfo).Exists();
await That(dirInfo).DoesNotExist();
await That(dirInfo).HasName("foo");
await That(dirInfo).IsEmpty();
await That(dirInfo).IsNotEmpty();
await That(dirInfo).HasFile("bar/my-file.txt");
await That(dirInfo).DoesNotHaveFile("bar/missing.txt");
await That(dirInfo).HasDirectory("bar").Which.HasFile("my-file.txt");
await That(dirInfo).DoesNotHaveDirectory("not-here");
await That(dirInfo).HasAttribute(FileAttributes.Directory);
await That(dirInfo).DoesNotHaveAttribute(FileAttributes.Hidden);
await That(dirInfo).HasCreationTime(DateTime.Now).Within(1.Second());
await That(dirInfo).HasLastAccessTime(DateTime.Now).Within(1.Second());
await That(dirInfo).HasLastWriteTime(DateTime.Now).Within(1.Second());HasFile / HasDirectory on IDirectoryInfo return the same chain
results as the file-system-level versions, so .WithContent(...),
.WithLastWriteTime(...), .Which, etc. all work as well: the path is
resolved relative to the directory.
On .NET 10 or later, WhoseParent switches to the parent directory:
await That(dirInfo).WhoseParent.HasName("…");MockFileSystem fileSystem = new(o => o.SimulatingOperatingSystem(SimulationMode.Windows));
fileSystem.WithDrive("D:", d => d.SetTotalSize(2048));
IDriveInfo driveInfo = fileSystem.DriveInfo.New("D:");
await That(driveInfo).HasAvailableFreeSpace(2048);
await That(driveInfo).HasTotalSize(2048).And.HasTotalFreeSpace(2048);
await That(driveInfo).HasDriveFormat("NTFS");
await That(driveInfo).HasDriveType(DriveType.Fixed);
await That(driveInfo).HasName(driveInfo.Name).And.HasVolumeLabel(driveInfo.VolumeLabel);
await That(driveInfo).IsReady();Drives are matched by name (case-insensitive) against
IFileSystem.DriveInfo.GetDrives(). UNC drives, which do not appear inGetDrives(), are not supported byHasDrive.
IFileVersionInfo instances obtained via
MockFileSystem.FileVersionInfo.GetVersionInfo can be asserted directly. The
values come from MockFileSystem.WithFileVersionInfo(glob, builder):
MockFileSystem fileSystem = new();
fileSystem.WithFileVersionInfo("*.dll", v => v
.SetCompanyName("Acme")
.SetProductName("Anvil")
.SetFileVersion("1.2.3.4")
.SetIsDebug(true));
fileSystem.File.WriteAllText("Acme.dll", "");
IFileVersionInfo info = fileSystem.FileVersionInfo.GetVersionInfo("Acme.dll");
await That(info).HasCompanyName("Acme").And.HasProductName("Anvil");
await That(info).HasFileVersion("1.2.3.4").And.HasFileMajorPart(1);
await That(info).IsDebug().And.IsNotPreRelease();Dedicated assertions cover every IFileVersionInfo property — strings via
Has…(string), the integer version parts via Has…(int) and the booleans
as Is…() / IsNot…() pairs.
| Property | Assertion |
|---|---|
Comments |
HasComments(string) |
CompanyName |
HasCompanyName(string) |
FileDescription |
HasFileDescription(string) |
FileName |
HasFileName(string) |
FileVersion |
HasFileVersion(string) |
InternalName |
HasInternalName(string) |
Language |
HasLanguage(string) |
LegalCopyright |
HasLegalCopyright(string) |
LegalTrademarks |
HasLegalTrademarks(string) |
OriginalFilename |
HasOriginalFilename(string) |
PrivateBuild |
HasPrivateBuild(string) |
ProductName |
HasProductName(string) |
ProductVersion |
HasProductVersion(string) |
SpecialBuild |
HasSpecialBuild(string) |
FileBuildPart |
HasFileBuildPart(int) |
FileMajorPart |
HasFileMajorPart(int) |
FileMinorPart |
HasFileMinorPart(int) |
FilePrivatePart |
HasFilePrivatePart(int) |
ProductBuildPart |
HasProductBuildPart(int) |
ProductMajorPart |
HasProductMajorPart(int) |
ProductMinorPart |
HasProductMinorPart(int) |
ProductPrivatePart |
HasProductPrivatePart(int) |
IsDebug |
IsDebug() / IsNotDebug() |
IsPatched |
IsPatched() / IsNotPatched() |
IsPreRelease |
IsPreRelease() / IsNotPreRelease() |
IsPrivateBuild |
IsPrivateBuild() / IsNotPrivateBuild() |
IsSpecialBuild |
IsSpecialBuild() / IsNotSpecialBuild() |
A MockFileSystem raises notifications when files or directories change. Run
the code under test, then assert against the notifications it produced:
MockFileSystem fileSystem = new();
fileSystem.File.WriteAllText("my-file.txt", "some content");
await That(fileSystem).TriggeredNotification();
await That(fileSystem).TriggeredNotification(c => c.Name == "my-file.txt");.Within(timeout) (default 30 s) lets the assertion wait for asynchronous
notifications. If a matching notification already fired, the assertion
completes synchronously; otherwise it waits up to the timeout for a late
arrival:
_ = Task.Run(() => fileSystem.File.WriteAllText("foo.txt", "x"));
await That(fileSystem).TriggeredNotification().Within(100.Milliseconds());DidNotTriggerNotification mirrors the same shapes and short-circuits as soon
as a matching notification is observed:
await That(fileSystem).DidNotTriggerNotification().Within(100.Milliseconds());
await That(fileSystem).DidNotTriggerNotification(c => c.Name == "secret.txt");Both accept a Quantifier (AtLeast, AtMost, Exactly, Between,
Never, Once) so you can assert how often the notification fires, and a
.Which(c => …) callback that composes the per-notification expectations
from ChangeDescription:
fileSystem.File.WriteAllText("a.txt", "x");
fileSystem.File.WriteAllText("b.txt", "y");
await That(fileSystem).TriggeredNotification(c => c.ChangeType == WatcherChangeTypes.Created)
.Exactly(2.Times());
await That(fileSystem)
.TriggeredNotification()
.Which(c => c.HasName("a.txt").And.HasChangeType(WatcherChangeTypes.Created))
.Exactly(1.Times());Replay of historical notifications relies on the
MockFileSystemnotification history. Disable it vianew MockFileSystem(o => o.WithoutNotificationHistory())only if you don't use these assertions: they throw against a history-disabled file system.
An individual IFileSystemWatcher can also be the subject. The watcher must
come from a MockFileSystem, and EnableRaisingEvents must be true for any
event to be observed. Only events fired on this specific watcher count;
events fired on other watchers of the same MockFileSystem are ignored.
MockFileSystem fileSystem = new();
using IFileSystemWatcher watcher = fileSystem.FileSystemWatcher.New("/");
watcher.EnableRaisingEvents = true;
fileSystem.File.WriteAllText("my-file.txt", "some content");
await That(watcher).Triggered();
await That(watcher).Triggered(c => c.Name == "my-file.txt");
await That(watcher).DidNotTrigger().Within(100.Milliseconds());
await That(watcher).DidNotTrigger(c => c.Name == "secret.txt");Triggered and DidNotTrigger share the same shape as the
notification assertions: a Quantifier
(AtLeast, AtMost, Exactly, Between, Never, Once), a
.Within(timeout) (default 30 s), and a .Which(c => …) callback that
composes the per-change expectations from
ChangeDescription:
await That(watcher)
.Triggered()
.Which(c => c.HasName("my-file.txt").And.HasChangeType(WatcherChangeTypes.Created))
.Exactly(1.Times());Individual ChangeDescription instances can be asserted directly:
await That(change).HasChangeType(WatcherChangeTypes.Created);
await That(change).DoesNotHaveChangeType(WatcherChangeTypes.Deleted);
await That(change).HasFileSystemType(FileSystemTypes.File);
await That(change).HasNotifyFilters(NotifyFilters.LastWrite);
await That(change).HasName("my-file.txt").And.HasPath("/abs/my-file.txt");
await That(renamedChange).HasOldName("old.txt").And.HasOldPath("/abs/old.txt");HasChangeType, HasFileSystemType and HasNotifyFilters use flag
containment (so a LastWrite | FileName change satisfies
HasNotifyFilters(NotifyFilters.LastWrite)); the empty / default value is
rejected with an ArgumentException to avoid silent passes.
MockFileSystem.Statistics records every method call and property access on
the mock. .Recorded() exposes a fluent mirror over these recordings, so you
can assert what the system under test actually called:
MockFileSystem fileSystem = new();
fileSystem.File.WriteAllText("foo.txt", "x");
await That(fileSystem.Statistics).Recorded().File.WriteAllText().Once();
await That(fileSystem.Statistics).Recorded().File.WriteAllText(path: p => p == "foo.txt").Once();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 per-instance buckets. Every
result inherits the count vocabulary (Once, Twice, Never, Exactly,
AtLeast, AtMost, Between, …).
Property reads and writes are recorded with .Get() / .Set():
fileSystem.FileInfo.New("foo.txt").IsReadOnly = true;
await That(fileSystem.Statistics).Recorded().FileInfo["foo.txt"].IsReadOnly.Set().Once();
await That(fileSystem.Statistics).Recorded().DirectoryInfo["foo"].Exists.Get().AtLeast().Once();Each parameter on a mirror method is an optional Func<T, bool> predicate
matched positionally against the recorded argument:
- Supplying no predicate (or
null) skips that position and matches every overload, so.File.Open()counts allOpeninvocations regardless of arity. - A predicate whose position exceeds an overload's arity excludes that
overload, so filtering
recursiveonDirectory.Deleteonly matches the two-argument overload. - A predicate whose type differs from the recorded type at that position
silently excludes that overload, so filtering
searchOptiononDirectory.EnumerateDirectoriesnever matches theEnumerationOptionsoverload.
A handful of methods can't be filtered fully through this positional model
because two overloads place different types at the same recording position
(File.Open / FileInfo.Open with FileStreamOptions,
FileSystemWatcher.WaitForChanged with TimeSpan).
A MockTimeSystem exposes timers as ITimerMock. You can assert how often
the timer callback was executed without blocking the test thread:
MockTimeSystem timeSystem = new();
ITimerMock timer = (ITimerMock)timeSystem.Timer.New(
_ => { }, null, TimeSpan.Zero, TimeSpan.FromMilliseconds(10));
await That(timer).Executed(3.Times()).Within(5.Seconds());Executed() accepts a Quantifier (AtLeast, AtMost, Exactly,
Between, Never, Once) and exposes .Within(timeout) for asynchronous
execution. The assertion polls ITimerMock.ExecutionCount until the
quantifier is satisfied or the timeout expires (30 seconds by default).
await That(timer).Executed().AtLeast(2.Times()).Within(100.Milliseconds());