|
1 | | - |
2 | | -[](https://www.nuget.org/packages/Testably.Abstractions) |
3 | | -[](https://www.nuget.org/packages/Testably.Abstractions.Testing) |
4 | | -[](https://github.com/Testably/Testably.Abstractions/actions/workflows/build.yml) |
5 | | -[](https://sonarcloud.io/summary/new_code?id=Testably_Testably.Abstractions) |
6 | | -[](https://sonarcloud.io/summary/new_code?id=Testably_Testably.Abstractions) |
7 | | - |
8 | | -This library is a feature complete testing helper for the [IFileSystem abstractions for I/O-related functionality](https://github.com/TestableIO/System.IO.Abstractions) from the `System.IO` namespace. It uses an in-memory file system that behaves exactly like the real file system and can be used in unit tests for dependency injection. |
9 | | -The testing helper also supports advanced scenarios like |
10 | | -- [Multiple drives with limited size](Examples/DriveManagement/README.md) |
11 | | -- [`FileSystemWatcher`](Examples/FileSystemWatcher/README.md) and |
12 | | -- a way to work with [SafeFileHandles](Examples/SafeFileHandle/README.md) |
13 | | - |
14 | | -The companion projects [Testably.Abstractions.Compression](https://www.nuget.org/packages/Testably.Abstractions.Compression) and [Testably.Abstractions.AccessControl](https://www.nuget.org/packages/Testably.Abstractions.AccessControl) allow working with [Zip-Files](Examples/ZipFile/README.md) and [Access Control Lists](Examples/AccessControlLists/README.md) respectively. |
15 | | - |
16 | | -As the test suite runs both against the mocked and the real file system, the behaviour between the two is identical and it also allows [simulating the file system on other operating systems](#simulating-other-operating-systems) (Linux, MacOS and Windows). |
17 | | - |
18 | | -In addition, the following interfaces are defined: |
19 | | -- The `ITimeSystem` interface abstracts away time-related functionality: |
20 | | - - `DateTime` methods give access to the current time |
21 | | - - `Stopwatch` is a wrapper around [`System.Diagnostics.Stopwatch`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.stopwatch) |
22 | | - - `Task` allows replacing [`Task.Delay`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.delay) |
23 | | - - `Thread` allows replacing [`Thread.Sleep`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.thread.sleep) |
24 | | - - `PeriodicTimer` is a wrapper around [`System.Threading.PeriodicTimer`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.periodictimer) |
25 | | - - `Timer` is a wrapper around [`System.Threading.Timer`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.timer) |
26 | | -- The `IRandomSystem` interface abstracts away functionality related to randomness: |
27 | | - `Random` methods implement a thread-safe Shared instance also under .NET Framework and `Guid` methods allow creating new GUIDs. |
28 | | - |
29 | | -## Example |
30 | | -Use the interfaces and their default implementations using your prefered dependency injection method, e.g.: |
31 | | -```csharp |
32 | | -private readonly IFileSystem _fileSystem; |
| 1 | +# Testably.Abstractions |
33 | 2 |
|
34 | | -public class MyService(IFileSystem fileSystem) |
35 | | -{ |
36 | | - _fileSystem = fileSystem; |
37 | | -} |
| 3 | +[](https://www.nuget.org/packages/Testably.Abstractions) |
| 4 | +[](https://www.nuget.org/packages/Testably.Abstractions.Testing) |
| 5 | +[](https://github.com/Testably/Testably.Abstractions/actions/workflows/build.yml) |
| 6 | +[](https://sonarcloud.io/summary/new_code?id=Testably_Testably.Abstractions) |
| 7 | +[](https://sonarcloud.io/summary/new_code?id=Testably_Testably.Abstractions) |
38 | 8 |
|
39 | | -public void StoreData() |
40 | | -{ |
41 | | - var fileContent = GetFileContent(); |
42 | | - _fileSystem.File.WriteAllText("result.xml", fileContent); |
43 | | -} |
| 9 | +Injectable abstractions for the static parts of the .NET BCL - file system, time and randomness - with feature-complete in-memory mocks for tests. |
| 10 | + |
| 11 | +**📖 Full documentation: [docs.testably.org](https://docs.testably.org)** |
| 12 | + |
| 13 | +## Quick example |
44 | 14 |
|
45 | | -private string GetFileContent() |
| 15 | +```csharp |
| 16 | +public class ReportService(IFileSystem fileSystem) |
46 | 17 | { |
47 | | - // Generate the file content |
| 18 | + public void Save(string content) |
| 19 | + { |
| 20 | + fileSystem.Directory.CreateDirectory("reports"); |
| 21 | + fileSystem.File.WriteAllText("reports/latest.xml", content); |
| 22 | + } |
48 | 23 | } |
49 | 24 | ``` |
50 | 25 |
|
51 | | -Then you test your class with the mocked types in `Testably.Abstractions.Testing`: |
52 | 26 | ```csharp |
53 | | -[Test] |
54 | | -public void StoreData_ShouldWriteValidFile() |
| 27 | +[Fact] |
| 28 | +public async Task Save_WritesReportToReportsFolder() |
55 | 29 | { |
56 | | - IFileSystem fileSystem = new MockFileSystem(); |
57 | | - MyService sut = new MyService(fileSystem); |
| 30 | + var fileSystem = new MockFileSystem(); |
| 31 | + var sut = new ReportService(fileSystem); |
58 | 32 |
|
59 | | - sut.StoreData(); |
| 33 | + sut.Save("<report />"); |
60 | 34 |
|
61 | | - var fileContent = fileSystem.File.ReadAllText("result.xml"); |
62 | | - // Validate fileContent |
| 35 | + await Expect.That(fileSystem.File.ReadAllText("reports/latest.xml")) |
| 36 | + .IsEqualTo("<report />"); |
63 | 37 | } |
64 | 38 | ``` |
65 | 39 |
|
66 | | -**More examples can be found in the [examples section](Examples/README.md)!** |
67 | | - |
68 | | -## Getting Started |
69 | | - |
70 | | -- Install `Testably.Abstractions` as nuget package in your production projects and `Testably.Abstractions.Testing` as nuget package in your test projects. |
71 | | - ```ps |
72 | | - dotnet add package Testably.Abstractions |
73 | | - dotnet add package Testably.Abstractions.Testing |
74 | | - ``` |
75 | | - |
76 | | -- Configure your dependeny injection framework, e.g. with `Microsoft.Extensions.DependencyInjections` in ASP.NET core: |
77 | | - ```csharp |
78 | | - builder.Services |
79 | | - .AddSingleton<IFileSystem, RealFileSystem>() |
80 | | - .AddSingleton<IRandomSystem, RealRandomSystem>() |
81 | | - .AddSingleton<ITimeSystem, RealTimeSystem>(); |
82 | | - ``` |
83 | | - |
84 | | -**You can now use the interfaces in your services!** |
85 | | - |
86 | | -## Testing |
87 | | -In order to simplify testing, the `Testably.Abstractions.Testing` project provides mocked instances for the abstraction interfaces, which are configured using fluent syntax: |
| 40 | +## Install |
88 | 41 |
|
89 | | -### Initialization |
90 | | - |
91 | | -The following two code snippets initialize the mocked `fileSystem` with a structure like the following: |
92 | | -- Directory "foo" |
93 | | - - Directory "bar" |
94 | | - - Empty file "bar.txt" |
95 | | -- File "foo.txt" with "some file content" as content |
96 | | - |
97 | | -```csharp |
98 | | -var fileSystem = new MockFileSystem(); |
99 | | -fileSystem.Initialize().With( |
100 | | - new DirectoryDescription("foo", |
101 | | - new DirectoryDescription("bar"), |
102 | | - new FileDescription("bar.txt")), |
103 | | - new FileDescription("foo.txt", "some file content")); |
| 42 | +```ps |
| 43 | +dotnet add package Testably.Abstractions |
| 44 | +dotnet add package Testably.Abstractions.Testing |
104 | 45 | ``` |
105 | 46 |
|
106 | | -```csharp |
107 | | -var fileSystem = new MockFileSystem(); |
108 | | -fileSystem.Initialize() |
109 | | - .WithSubdirectory("foo").Initialized(d => d |
110 | | - .WithSubdirectory("bar") |
111 | | - .WithFile("bar.txt")) |
112 | | - .WithFile("foo.txt").Which(f => f.HasStringContent("some file content")); |
113 | | -``` |
| 47 | +Then register the implementations in your DI container - see [Getting Started](https://docs.testably.org/docs/getting-started). |
114 | 48 |
|
115 | | -### Simulating other operating systems |
| 49 | +## Packages |
116 | 50 |
|
117 | | -The `MockFileSystem` can also simulate other operating systems than the one it is currently running on. This can be achieved, by providing the corresponding `SimulationMode` in the constructor: |
| 51 | +| Package | Purpose | |
| 52 | +|--------------------------------------|-------------------------------------------------------------------------| |
| 53 | +| `Testably.Abstractions` | Production interfaces (`IFileSystem`, `ITimeSystem`, `IRandomSystem`) | |
| 54 | +| `Testably.Abstractions.Testing` | `MockFileSystem`, `MockTimeSystem`, `MockRandomSystem` | |
| 55 | +| `Testably.Abstractions.Compression` | Zip / `ZipArchive` extension methods on `IFileSystem` | |
| 56 | +| `Testably.Abstractions.AccessControl`| `GetAccessControl` / `SetAccessControl` on files and directories | |
118 | 57 |
|
119 | | -```csharp |
120 | | -var linuxFileSystem = new MockFileSystem(o => o.SimulatingOperatingSystem(SimulationMode.Linux)); |
121 | | -// The `linuxFileSystem` now behaves like a Linux file system even under Windows: |
122 | | -// - case-sensitive |
123 | | -// - slash as directory separator |
124 | | -
|
125 | | -var windowsFileSystem = new MockFileSystem(o => o.SimulatingOperatingSystem(SimulationMode.Windows)); |
126 | | -// The `windowsFileSystem` now behaves like a Windows file system even under Linux or MacOS: |
127 | | -// - multiple drives |
128 | | -// - case-insensitive |
129 | | -// - backslash as directory separator |
130 | | -``` |
| 58 | +## Already on TestableIO? |
131 | 59 |
|
132 | | -By running all tests against the real file system and the simulated under Linux, MacOS and Windows, the behaviour is consistent between the native and simulated mock file systems. |
| 60 | +`Testably.Abstractions` shares the `IFileSystem` interface with [TestableIO.System.IO.Abstractions](https://github.com/TestableIO/System.IO.Abstractions), so production code stays untouched. See the [migration guide](https://docs.testably.org/docs/migration-from-testableio). |
133 | 61 |
|
134 | | -### Drive management |
135 | | -```csharp |
136 | | -var fileSystem = new MockFileSystem(); |
137 | | -fileSystem |
138 | | - .WithDrive("D:", d => d |
139 | | - .SetTotalSize(1024 * 1024)) |
140 | | - .InitializeIn("D:") |
141 | | - .WithFile("foo.txt") |
142 | | - .WithSubdirectory("sub-dir").Initialized(s => s |
143 | | - .WithAFile(".json").Which( |
144 | | - f => f.HasStringContent("{\"count\":1}"))); |
145 | | -``` |
146 | | -Initializes the mocked file system with a second drive `D:` with 1MB total available space and creates on it an empty text file `foo.txt` and a directory `sub-dir` which contains randomly named json file with `{"count":1}` as file content. |
147 | | - |
148 | | -On non-Windows systems, the main drive can still be configured, e.g. |
149 | | -```csharp |
150 | | -var fileSystem = new MockFileSystem(); |
151 | | -fileSystem.WithDrive(d => d.SetTotalSize(20)); |
152 | | - |
153 | | -// this will throw an IOException that there is not enough space on the disk. |
154 | | -fileSystem.File.WriteAllText("foo", "some text longer than 20 bytes"); |
155 | | -``` |
| 62 | +## Contributing |
156 | 63 |
|
157 | | -## Relationship with TestableIO.System.IO.Abstractions |
158 | | - |
159 | | -This library uses the same interfaces as [TestableIO.System.IO.Abstractions](https://github.com/TestableIO/System.IO.Abstractions), which means you can switch between the two testing libraries **without changing your production code**. Both libraries provide `IFileSystem` implementations, but with different testing capabilities and API surfaces. |
160 | | - |
161 | | -### When to use Testably.Abstractions vs TestableIO |
162 | | -- **Use Testably.Abstractions** if you need: |
163 | | - - Advanced testing scenarios (FileSystemWatcher, SafeFileHandles, multiple drives) |
164 | | - - Additional abstractions (ITimeSystem, IRandomSystem) |
165 | | - - Cross-platform file system simulation (Linux, MacOS, Windows) |
166 | | - - More extensive and consistent behavior validation |
167 | | - - Active development and new features |
168 | | - |
169 | | -- **Use TestableIO.System.IO.Abstractions** if you need: |
170 | | - - Basic file system mocking capabilities |
171 | | - - Direct manipulation of stored file entities (MockFileData, MockDirectoryData) |
172 | | - - Established codebase with existing TestableIO integration |
173 | | - |
174 | | -### Migrating from TestableIO |
175 | | -Switching from TestableIO to Testably only requires changes in your test projects: |
176 | | - |
177 | | -1. Replace the NuGet package reference in your test projects: |
178 | | - ```xml |
179 | | - <!-- Remove --> |
180 | | - <PackageReference Include="TestableIO.System.IO.Abstractions.TestingHelpers" /> |
181 | | - <!-- Add --> |
182 | | - <PackageReference Include="Testably.Abstractions.Testing" /> |
183 | | - ``` |
184 | | - |
185 | | -2. Update your test code to use the new `MockFileSystem`: |
186 | | - ```csharp |
187 | | - // Before (TestableIO) |
188 | | - var fileSystem = new MockFileSystem(); |
189 | | - fileSystem.AddDirectory("some-directory"); |
190 | | - fileSystem.AddFile("some-file.txt", new MockFileData("content")); |
191 | | - |
192 | | - // After (Testably) |
193 | | - var fileSystem = new MockFileSystem(); |
194 | | - fileSystem.Directory.CreateDirectory("some-directory"); |
195 | | - fileSystem.File.WriteAllText("some-file.txt", "content"); |
196 | | - // or using fluent initialization: |
197 | | - fileSystem.Initialize() |
198 | | - .WithSubdirectory("some-directory") |
199 | | - .WithFile("some-file.txt").Which(f => f |
200 | | - .HasStringContent("content")); |
201 | | - ``` |
202 | | - |
203 | | -Your production code using `IFileSystem` remains unchanged. |
| 64 | +Issues and pull requests are welcome - see [CONTRIBUTING.md](CONTRIBUTING.md) and the [issue tracker](https://github.com/Testably/Testably.Abstractions/issues). |
0 commit comments