diff --git a/Source/Testably.Abstractions.FluentAssertions/FileSystemExtensions.cs b/Source/Testably.Abstractions.FluentAssertions/FileSystemExtensions.cs
index d93221f..796268a 100644
--- a/Source/Testably.Abstractions.FluentAssertions/FileSystemExtensions.cs
+++ b/Source/Testably.Abstractions.FluentAssertions/FileSystemExtensions.cs
@@ -1,4 +1,6 @@
-namespace Testably.Abstractions.FluentAssertions;
+using Testably.Abstractions.Testing.Statistics;
+
+namespace Testably.Abstractions.FluentAssertions;
///
/// Assertion extensions on .
@@ -32,4 +34,11 @@ public static FileSystemInfoAssertions Should(this IFileSystemInfo? instance)
///
public static FileSystemAssertions Should(this IFileSystem instance)
=> new(instance);
+
+ ///
+ /// Returns a object that can be used to
+ /// assert the current .
+ ///
+ public static StatisticAssertions Should(this IStatistics? instance)
+ => new(instance);
}
diff --git a/Source/Testably.Abstractions.FluentAssertions/StatisticAssertions.cs b/Source/Testably.Abstractions.FluentAssertions/StatisticAssertions.cs
new file mode 100644
index 0000000..cf0b024
--- /dev/null
+++ b/Source/Testably.Abstractions.FluentAssertions/StatisticAssertions.cs
@@ -0,0 +1,51 @@
+using System.Linq;
+using Testably.Abstractions.Testing.Statistics;
+
+namespace Testably.Abstractions.FluentAssertions;
+
+///
+/// Assertions on .
+///
+public class StatisticAssertions :
+ ReferenceTypeAssertions>
+{
+ ///
+ protected override string Identifier => "statistics";
+
+ internal StatisticAssertions(IStatistics? instance)
+ : base(instance)
+ {
+ }
+
+ ///
+ /// Returns a object that can be used to assert that the
+ /// property named was accessed a certain number of times.
+ ///
+ public StatisticPropertyAssertions> HaveAccessed(
+ string propertyName)
+ {
+ if (Subject == null)
+ {
+ return new StatisticPropertyAssertions>(this, propertyName);
+ }
+
+ return new StatisticPropertyAssertions>(this, propertyName,
+ Subject.Properties.Where(p => p.Name == propertyName));
+ }
+
+ ///
+ /// Returns a object that can be used to assert that the
+ /// method named was called a certain number of times.
+ ///
+ public StatisticMethodAssertions> HaveCalled(
+ string methodName)
+ {
+ if (Subject == null)
+ {
+ return new StatisticMethodAssertions>(this, methodName);
+ }
+
+ return new StatisticMethodAssertions>(this, methodName,
+ Subject.Methods.Where(m => m.Name == methodName));
+ }
+}
diff --git a/Source/Testably.Abstractions.FluentAssertions/StatisticAssertionsCount.cs b/Source/Testably.Abstractions.FluentAssertions/StatisticAssertionsCount.cs
new file mode 100644
index 0000000..26bb163
--- /dev/null
+++ b/Source/Testably.Abstractions.FluentAssertions/StatisticAssertionsCount.cs
@@ -0,0 +1,242 @@
+namespace Testably.Abstractions.FluentAssertions;
+
+///
+/// Assertions on statistics.
+///
+public abstract class StatisticAssertionsCount(TAssertions assertions)
+ where TAssertions : StatisticAssertions
+{
+ ///
+ /// Flag indicating if the subject of the assertion is null.
+ ///
+ protected abstract bool IsSubjectNull { get; }
+
+ ///
+ /// The name of the statistic.
+ ///
+ protected abstract string StatisticName { get; }
+
+ ///
+ /// The type of the statistic.
+ ///
+ protected abstract string StatisticType { get; }
+
+ ///
+ /// The verb to interact with the statistic type.
+ ///
+ protected abstract string StatisticTypeVerb { get; }
+
+ ///
+ /// Asserts that the number of calls on the filtered methods/properties is at least times.
+ ///
+ public AndConstraint AtLeast(int count,
+ string because = "", params object[] becauseArgs)
+ {
+ int actualCount = GetCount();
+ Execute.Assertion
+ .WithDefaultIdentifier("Statistic")
+ .BecauseOf(because, becauseArgs)
+ .ForCondition(!IsSubjectNull)
+ .FailWith("You can't assert a statistic if it is null.")
+ .Then
+ .ForCondition(actualCount >= count)
+ .FailWith(
+ $"Expected {StatisticType} `{StatisticName}` to be {StatisticTypeVerb} at least {CountToString(count)}{{reason}}, but it was {CountToString(actualCount)}.");
+
+ return new AndConstraint(assertions);
+ }
+
+ ///
+ /// Asserts that the number of calls on the filtered methods/properties is at least 1.
+ ///
+ public AndConstraint AtLeastOnce(
+ string because = "", params object[] becauseArgs)
+ {
+ int count = GetCount();
+ Execute.Assertion
+ .WithDefaultIdentifier("Statistic")
+ .BecauseOf(because, becauseArgs)
+ .ForCondition(!IsSubjectNull)
+ .FailWith("You can't assert a statistic if it is null.")
+ .Then
+ .ForCondition(count >= 1)
+ .FailWith(
+ $"Expected {StatisticType} `{StatisticName}` to be {StatisticTypeVerb} at least once{{reason}}, but it was {CountToString(count)}.");
+
+ return new AndConstraint(assertions);
+ }
+
+ ///
+ /// Asserts that the number of calls on the filtered methods/properties is at least 2.
+ ///
+ public AndConstraint AtLeastTwice(
+ string because = "", params object[] becauseArgs)
+ {
+ int count = GetCount();
+ Execute.Assertion
+ .WithDefaultIdentifier("Statistic")
+ .BecauseOf(because, becauseArgs)
+ .ForCondition(!IsSubjectNull)
+ .FailWith("You can't assert a statistic if it is null.")
+ .Then
+ .ForCondition(count >= 2)
+ .FailWith(
+ $"Expected {StatisticType} `{StatisticName}` to be {StatisticTypeVerb} at least twice{{reason}}, but it was {CountToString(count)}.");
+
+ return new AndConstraint(assertions);
+ }
+
+ ///
+ /// Asserts that the number of calls on the filtered methods/properties is at most times.
+ ///
+ public AndConstraint AtMost(int count,
+ string because = "", params object[] becauseArgs)
+ {
+ int actualCount = GetCount();
+ Execute.Assertion
+ .WithDefaultIdentifier("Statistic")
+ .BecauseOf(because, becauseArgs)
+ .ForCondition(!IsSubjectNull)
+ .FailWith("You can't assert a statistic if it is null.")
+ .Then
+ .ForCondition(actualCount <= count)
+ .FailWith(
+ $"Expected {StatisticType} `{StatisticName}` to be {StatisticTypeVerb} at most {CountToString(count)}{{reason}}, but it was {CountToString(actualCount)}.");
+
+ return new AndConstraint(assertions);
+ }
+
+ ///
+ /// Asserts that the number of calls on the filtered methods/properties is at most 1.
+ ///
+ public AndConstraint AtMostOnce(
+ string because = "", params object[] becauseArgs)
+ {
+ int count = GetCount();
+ Execute.Assertion
+ .WithDefaultIdentifier("Statistic")
+ .BecauseOf(because, becauseArgs)
+ .ForCondition(!IsSubjectNull)
+ .FailWith("You can't assert a statistic if it is null.")
+ .Then
+ .ForCondition(count <= 1)
+ .FailWith(
+ $"Expected {StatisticType} `{StatisticName}` to be {StatisticTypeVerb} at most once{{reason}}, but it was {CountToString(count)}.");
+
+ return new AndConstraint(assertions);
+ }
+
+ ///
+ /// Asserts that the number of calls on the filtered methods/properties is at most 2.
+ ///
+ public AndConstraint AtMostTwice(
+ string because = "", params object[] becauseArgs)
+ {
+ int count = GetCount();
+ Execute.Assertion
+ .WithDefaultIdentifier("Statistic")
+ .BecauseOf(because, becauseArgs)
+ .ForCondition(!IsSubjectNull)
+ .FailWith("You can't assert a statistic if it is null.")
+ .Then
+ .ForCondition(count <= 2)
+ .FailWith(
+ $"Expected {StatisticType} `{StatisticName}` to be {StatisticTypeVerb} at most twice{{reason}}, but it was {CountToString(count)}.");
+
+ return new AndConstraint(assertions);
+ }
+
+ ///
+ /// Asserts that the number of calls on the filtered methods/properties is exactly times.
+ ///
+ public AndConstraint Exactly(int count,
+ string because = "", params object[] becauseArgs)
+ {
+ int actualCount = GetCount();
+ Execute.Assertion
+ .WithDefaultIdentifier("Statistic")
+ .BecauseOf(because, becauseArgs)
+ .ForCondition(!IsSubjectNull)
+ .FailWith("You can't assert a statistic if it is null.")
+ .Then
+ .ForCondition(actualCount == count)
+ .FailWith(
+ $"Expected {StatisticType} `{StatisticName}` to be {StatisticTypeVerb} {CountToString(count)}{{reason}}, but it was {CountToString(actualCount)}.");
+
+ return new AndConstraint(assertions);
+ }
+
+ ///
+ /// Asserts that the number of calls on the filtered methods/properties is 0 (zero).
+ ///
+ public AndConstraint Never(
+ string because = "", params object[] becauseArgs)
+ {
+ int count = GetCount();
+ Execute.Assertion
+ .WithDefaultIdentifier("Statistic")
+ .BecauseOf(because, becauseArgs)
+ .ForCondition(!IsSubjectNull)
+ .FailWith("You can't assert a statistic if it is null.")
+ .Then
+ .ForCondition(count == 0)
+ .FailWith(
+ $"Expected {StatisticType} `{StatisticName}` to never be {StatisticTypeVerb}{{reason}}, but it was {CountToString(count)}.");
+
+ return new AndConstraint(assertions);
+ }
+
+ ///
+ /// Asserts that the number of calls on the filtered methods/properties is 1.
+ ///
+ public AndConstraint Once(
+ string because = "", params object[] becauseArgs)
+ {
+ int count = GetCount();
+ Execute.Assertion
+ .WithDefaultIdentifier("Statistic")
+ .BecauseOf(because, becauseArgs)
+ .ForCondition(!IsSubjectNull)
+ .FailWith("You can't assert a statistic if it is null.")
+ .Then
+ .ForCondition(count == 1)
+ .FailWith(
+ $"Expected {StatisticType} `{StatisticName}` to be {StatisticTypeVerb} once{{reason}}, but it was {CountToString(count)}.");
+
+ return new AndConstraint(assertions);
+ }
+
+ ///
+ /// Asserts that the number of calls on the filtered methods/properties is 2.
+ ///
+ public AndConstraint Twice(
+ string because = "", params object[] becauseArgs)
+ {
+ int count = GetCount();
+ Execute.Assertion
+ .WithDefaultIdentifier("Statistic")
+ .BecauseOf(because, becauseArgs)
+ .ForCondition(!IsSubjectNull)
+ .FailWith("You can't assert a statistic if it is null.")
+ .Then
+ .ForCondition(count == 2)
+ .FailWith(
+ $"Expected {StatisticType} `{StatisticName}` to be {StatisticTypeVerb} twice{{reason}}, but it was {CountToString(count)}.");
+
+ return new AndConstraint(assertions);
+ }
+
+ ///
+ /// Get the count of matching statistic values.
+ ///
+ protected abstract int GetCount();
+
+ private static string CountToString(int count)
+ => count switch
+ {
+ 0 => "never",
+ 1 => "once",
+ 2 => "twice",
+ _ => $"{count} times"
+ };
+}
diff --git a/Source/Testably.Abstractions.FluentAssertions/StatisticMethodAssertions.cs b/Source/Testably.Abstractions.FluentAssertions/StatisticMethodAssertions.cs
new file mode 100644
index 0000000..095e2ae
--- /dev/null
+++ b/Source/Testably.Abstractions.FluentAssertions/StatisticMethodAssertions.cs
@@ -0,0 +1,116 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Testably.Abstractions.Testing.Statistics;
+
+namespace Testably.Abstractions.FluentAssertions;
+
+///
+/// Assertions on method statistics.
+///
+public class StatisticMethodAssertions
+ : StatisticAssertionsCount
+ where TAssertions : StatisticAssertions
+{
+ ///
+ protected override bool IsSubjectNull { get; }
+
+ ///
+ protected override string StatisticName { get; }
+
+ ///
+ protected override string StatisticType => "method";
+
+ ///
+ protected override string StatisticTypeVerb => "called";
+
+ private readonly TAssertions _assertions;
+
+ private readonly IEnumerable _methods;
+
+ internal StatisticMethodAssertions(TAssertions assertions, string methodName)
+ : base(assertions)
+ {
+ _assertions = assertions;
+ IsSubjectNull = true;
+ StatisticName = methodName;
+ _methods = Array.Empty();
+ }
+
+ internal StatisticMethodAssertions(TAssertions assertions, string methodName,
+ IEnumerable methods)
+ : base(assertions)
+ {
+ IsSubjectNull = false;
+ StatisticName = methodName;
+ _assertions = assertions;
+ _methods = methods;
+ }
+
+ ///
+ /// Filters for methods whose first parameter equals .
+ ///
+ public StatisticMethodAssertions WithFirstParameter(
+ TParameter parameterValue)
+ => WithParameterAt(0,
+ p => p is null ? parameterValue is null : p.Equals(parameterValue));
+
+ ///
+ /// Filters for methods whose first parameter matches the .
+ ///
+ public StatisticMethodAssertions WithFirstParameter(
+ Func predicate)
+ => WithParameterAt(0, predicate);
+
+ ///
+ /// Filters for methods whose parameter at the zero-based equals
+ /// .
+ ///
+ public StatisticMethodAssertions WithParameterAt(int index,
+ TParameter parameterValue)
+ => WithParameterAt(index,
+ p => p is null ? parameterValue is null : p.Equals(parameterValue));
+
+ ///
+ /// Filters for methods whose parameter at the zero-based
+ /// matches the .
+ ///
+ public StatisticMethodAssertions WithParameterAt(int index,
+ Func predicate)
+ => new(_assertions, StatisticName,
+ _methods.Where(p => p.Parameters[index].Is(predicate)));
+
+ ///
+ /// Filters for methods whose second parameter equals .
+ ///
+ public StatisticMethodAssertions WithSecondParameter(
+ TParameter parameterValue)
+ => WithParameterAt(1,
+ p => p is null ? parameterValue is null : p.Equals(parameterValue));
+
+ ///
+ /// Filters for methods whose second parameter matches the .
+ ///
+ public StatisticMethodAssertions WithSecondParameter(
+ Func predicate)
+ => WithParameterAt(1, predicate);
+
+ ///
+ /// Filters for methods whose third parameter equals .
+ ///
+ public StatisticMethodAssertions WithThirdParameter(
+ TParameter parameterValue)
+ => WithParameterAt(2,
+ p => p is null ? parameterValue is null : p.Equals(parameterValue));
+
+ ///
+ /// Filters for methods whose third parameter matches the .
+ ///
+ public StatisticMethodAssertions WithThirdParameter(
+ Func predicate)
+ => WithParameterAt(2, predicate);
+
+ ///
+ protected override int GetCount()
+ => _methods.Count();
+}
diff --git a/Source/Testably.Abstractions.FluentAssertions/StatisticPropertyAssertions.cs b/Source/Testably.Abstractions.FluentAssertions/StatisticPropertyAssertions.cs
new file mode 100644
index 0000000..96813d5
--- /dev/null
+++ b/Source/Testably.Abstractions.FluentAssertions/StatisticPropertyAssertions.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Testably.Abstractions.Testing.Statistics;
+
+namespace Testably.Abstractions.FluentAssertions;
+
+///
+/// Assertions on property statistics.
+///
+public class StatisticPropertyAssertions
+ : StatisticAssertionsCount
+ where TAssertions : StatisticAssertions
+{
+ ///
+ protected override bool IsSubjectNull { get; }
+
+ ///
+ protected override string StatisticName { get; }
+
+ ///
+ protected override string StatisticType => "property";
+
+ ///
+ protected override string StatisticTypeVerb => "accessed";
+
+ private readonly IEnumerable _properties;
+
+ internal StatisticPropertyAssertions(TAssertions assertions, string propertyName)
+ : base(assertions)
+ {
+ IsSubjectNull = true;
+ StatisticName = propertyName;
+ _properties = Array.Empty();
+ }
+
+ internal StatisticPropertyAssertions(TAssertions assertions, string propertyName,
+ IEnumerable properties)
+ : base(assertions)
+ {
+ IsSubjectNull = false;
+ StatisticName = propertyName;
+ _properties = properties;
+ }
+
+ ///
+ protected override int GetCount()
+ => _properties.Count();
+}
diff --git a/Source/Testably.Abstractions.FluentAssertions/Testably.Abstractions.FluentAssertions.csproj b/Source/Testably.Abstractions.FluentAssertions/Testably.Abstractions.FluentAssertions.csproj
index 502e5a5..ece9d1f 100644
--- a/Source/Testably.Abstractions.FluentAssertions/Testably.Abstractions.FluentAssertions.csproj
+++ b/Source/Testably.Abstractions.FluentAssertions/Testably.Abstractions.FluentAssertions.csproj
@@ -14,7 +14,7 @@
-
+
diff --git a/Tests/Testably.Abstractions.FluentAssertions.Tests/StatisticAssertionsCountTests.cs b/Tests/Testably.Abstractions.FluentAssertions.Tests/StatisticAssertionsCountTests.cs
new file mode 100644
index 0000000..9e697d3
--- /dev/null
+++ b/Tests/Testably.Abstractions.FluentAssertions.Tests/StatisticAssertionsCountTests.cs
@@ -0,0 +1,428 @@
+using AutoFixture.Xunit2;
+using FluentAssertions;
+using System;
+using System.IO.Abstractions;
+using Testably.Abstractions.Testing;
+using Testably.Abstractions.Testing.Statistics;
+using Xunit;
+
+namespace Testably.Abstractions.FluentAssertions.Tests;
+
+public class StatisticAssertionsCountTests
+{
+ [Theory]
+ [InlineAutoData(2, 2)]
+ [InlineAutoData(4, 3)]
+ [InlineAutoData(5, 5)]
+ [InlineAutoData(6, 0)]
+ public void AtLeast_WhenCalledEnough_ShouldNotThrow(int callCount, int expectedCount)
+ {
+ MockFileSystem fileSystem = new();
+ for (int i = 0; i < callCount; i++)
+ {
+ fileSystem.File.WriteAllText($"foo-{i}", "bar");
+ }
+
+ IStatistics sut = fileSystem.Statistics.File;
+
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText)).AtLeast(expectedCount);
+ }
+
+ [Theory]
+ [InlineAutoData(0, "never", 1, "once")]
+ [InlineAutoData(0, "never", 8, "8 times")]
+ [InlineAutoData(5, "5 times", 6, "6 times")]
+ public void AtLeast_WhenCalledLess_ShouldThrow(
+ int actualCount, string actualText, int expectedCount, string expectedText,
+ string because)
+ {
+ MockFileSystem fileSystem = new();
+ for (int i = 0; i < actualCount; i++)
+ {
+ fileSystem.File.WriteAllText($"foo-{i}", "bar");
+ }
+
+ IStatistics sut = fileSystem.Statistics.File;
+
+ Exception? exception = Record.Exception(() =>
+ {
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText)).AtLeast(expectedCount, because);
+ });
+
+ exception.Should().NotBeNull();
+ exception!.Message.Should()
+ .Be(
+ $"Expected method `WriteAllText` to be called at least {expectedText} {because}, but it was {actualText}.");
+ }
+
+ [Theory]
+ [InlineAutoData(1)]
+ [InlineAutoData(2)]
+ [InlineAutoData(3)]
+ public void AtLeastOnce_WhenCalledAtLeastOnce_ShouldNotThrow(int callCount)
+ {
+ MockFileSystem fileSystem = new();
+ for (int i = 0; i < callCount; i++)
+ {
+ fileSystem.File.WriteAllText($"foo-{i}", "bar");
+ }
+
+ IStatistics sut = fileSystem.Statistics.File;
+
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText)).AtLeastOnce();
+ }
+
+ [Theory]
+ [InlineAutoData(0, "never")]
+ public void AtLeastOnce_WhenCalledLessThanOnce_ShouldThrow(
+ int callCount, string text,
+ string because)
+ {
+ MockFileSystem fileSystem = new();
+ for (int i = 0; i < callCount; i++)
+ {
+ fileSystem.File.WriteAllText($"foo-{i}", "bar");
+ }
+
+ IStatistics sut = fileSystem.Statistics.File;
+
+ Exception? exception = Record.Exception(() =>
+ {
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText)).AtLeastOnce(because);
+ });
+
+ exception.Should().NotBeNull();
+ exception!.Message.Should()
+ .Be(
+ $"Expected method `WriteAllText` to be called at least once {because}, but it was {text}.");
+ }
+
+ [Theory]
+ [InlineAutoData(2)]
+ [InlineAutoData(3)]
+ public void AtLeastTwice_WhenCalledAtLeastTwice_ShouldNotThrow(int callCount)
+ {
+ MockFileSystem fileSystem = new();
+ for (int i = 0; i < callCount; i++)
+ {
+ fileSystem.File.WriteAllText($"foo-{i}", "bar");
+ }
+
+ IStatistics sut = fileSystem.Statistics.File;
+
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText)).AtLeastTwice();
+ }
+
+ [Theory]
+ [InlineAutoData(0, "never")]
+ [InlineAutoData(1, "once")]
+ public void AtLeastTwice_WhenCalledLessThanTwice_ShouldThrow(
+ int callCount, string text,
+ string because)
+ {
+ MockFileSystem fileSystem = new();
+ for (int i = 0; i < callCount; i++)
+ {
+ fileSystem.File.WriteAllText($"foo-{i}", "bar");
+ }
+
+ IStatistics sut = fileSystem.Statistics.File;
+
+ Exception? exception = Record.Exception(() =>
+ {
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText)).AtLeastTwice(because);
+ });
+
+ exception.Should().NotBeNull();
+ exception!.Message.Should()
+ .Be(
+ $"Expected method `WriteAllText` to be called at least twice {because}, but it was {text}.");
+ }
+
+ [Theory]
+ [InlineAutoData(2, 2)]
+ [InlineAutoData(3, 4)]
+ [InlineAutoData(5, 5)]
+ [InlineAutoData(0, 6)]
+ public void AtMost_WhenCalledEnough_ShouldNotThrow(int callCount, int expectedCount)
+ {
+ MockFileSystem fileSystem = new();
+ for (int i = 0; i < callCount; i++)
+ {
+ fileSystem.File.WriteAllText($"foo-{i}", "bar");
+ }
+
+ IStatistics sut = fileSystem.Statistics.File;
+
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText)).AtMost(expectedCount);
+ }
+
+ [Theory]
+ [InlineAutoData(1, "once", 0, "never")]
+ [InlineAutoData(8, "8 times", 0, "never")]
+ [InlineAutoData(6, "6 times", 5, "5 times")]
+ public void AtMost_WhenCalledLess_ShouldThrow(
+ int actualCount, string actualText, int expectedCount, string expectedText,
+ string because)
+ {
+ MockFileSystem fileSystem = new();
+ for (int i = 0; i < actualCount; i++)
+ {
+ fileSystem.File.WriteAllText($"foo-{i}", "bar");
+ }
+
+ IStatistics sut = fileSystem.Statistics.File;
+
+ Exception? exception = Record.Exception(() =>
+ {
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText)).AtMost(expectedCount, because);
+ });
+
+ exception.Should().NotBeNull();
+ exception!.Message.Should()
+ .Be(
+ $"Expected method `WriteAllText` to be called at most {expectedText} {because}, but it was {actualText}.");
+ }
+
+ [Theory]
+ [InlineAutoData(0)]
+ [InlineAutoData(1)]
+ public void AtMostOnce_WhenCalledAtMostOnce_ShouldNotThrow(int callCount)
+ {
+ MockFileSystem fileSystem = new();
+ for (int i = 0; i < callCount; i++)
+ {
+ fileSystem.File.WriteAllText($"foo-{i}", "bar");
+ }
+
+ IStatistics sut = fileSystem.Statistics.File;
+
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText)).AtMostOnce();
+ }
+
+ [Theory]
+ [InlineAutoData(2, "twice")]
+ [InlineAutoData(3, "3 times")]
+ public void AtMostOnce_WhenCalledMoreThanOnce_ShouldThrow(
+ int callCount, string text,
+ string because)
+ {
+ MockFileSystem fileSystem = new();
+ for (int i = 0; i < callCount; i++)
+ {
+ fileSystem.File.WriteAllText($"foo-{i}", "bar");
+ }
+
+ IStatistics sut = fileSystem.Statistics.File;
+
+ Exception? exception = Record.Exception(() =>
+ {
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText)).AtMostOnce(because);
+ });
+
+ exception.Should().NotBeNull();
+ exception!.Message.Should()
+ .Be(
+ $"Expected method `WriteAllText` to be called at most once {because}, but it was {text}.");
+ }
+
+ [Theory]
+ [InlineAutoData(0)]
+ [InlineAutoData(1)]
+ [InlineAutoData(2)]
+ public void AtMostTwice_WhenCalledAtMostTwice_ShouldNotThrow(int callCount)
+ {
+ MockFileSystem fileSystem = new();
+ for (int i = 0; i < callCount; i++)
+ {
+ fileSystem.File.WriteAllText($"foo-{i}", "bar");
+ }
+
+ IStatistics sut = fileSystem.Statistics.File;
+
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText)).AtMostTwice();
+ }
+
+ [Theory]
+ [InlineAutoData(3, "3 times")]
+ public void AtMostTwice_WhenCalledMoreThanTwice_ShouldThrow(
+ int callCount, string text,
+ string because)
+ {
+ MockFileSystem fileSystem = new();
+ for (int i = 0; i < callCount; i++)
+ {
+ fileSystem.File.WriteAllText($"foo-{i}", "bar");
+ }
+
+ IStatistics sut = fileSystem.Statistics.File;
+
+ Exception? exception = Record.Exception(() =>
+ {
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText)).AtMostTwice(because);
+ });
+
+ exception.Should().NotBeNull();
+ exception!.Message.Should()
+ .Be(
+ $"Expected method `WriteAllText` to be called at most twice {because}, but it was {text}.");
+ }
+
+ [Theory]
+ [InlineAutoData(0)]
+ [InlineAutoData(1)]
+ [InlineAutoData(2)]
+ [InlineAutoData(3)]
+ [AutoData]
+ public void Exactly_WhenCalledCorrectTimes_ShouldNotThrow(int count)
+ {
+ MockFileSystem fileSystem = new();
+ for (int i = 0; i < count; i++)
+ {
+ fileSystem.File.WriteAllText($"foo-{i}", "bar");
+ }
+
+ IStatistics sut = fileSystem.Statistics.File;
+
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText)).Exactly(count);
+ }
+
+ [Theory]
+ [InlineAutoData(0, "never", 1, "once")]
+ [InlineAutoData(1, "once", 0, "never")]
+ [InlineAutoData(5, "5 times", 6, "6 times")]
+ [InlineAutoData(8, "8 times", 7, "7 times")]
+ public void Exactly_WhenCalledDifferentTimes_ShouldThrow(
+ int actualCount, string actualText, int expectedCount, string expectedText,
+ string because)
+ {
+ MockFileSystem fileSystem = new();
+ for (int i = 0; i < actualCount; i++)
+ {
+ fileSystem.File.WriteAllText($"foo-{i}", "bar");
+ }
+
+ IStatistics sut = fileSystem.Statistics.File;
+
+ Exception? exception = Record.Exception(() =>
+ {
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText)).Exactly(expectedCount, because);
+ });
+
+ exception.Should().NotBeNull();
+ exception!.Message.Should()
+ .Be(
+ $"Expected method `WriteAllText` to be called {expectedText} {because}, but it was {actualText}.");
+ }
+
+ [Theory]
+ [InlineAutoData(1, "once")]
+ [InlineAutoData(2, "twice")]
+ [InlineAutoData(3, "3 times")]
+ public void Never_WhenCalled_ShouldThrow(
+ int callCount, string text,
+ string because)
+ {
+ MockFileSystem fileSystem = new();
+ for (int i = 0; i < callCount; i++)
+ {
+ fileSystem.File.WriteAllText($"foo-{i}", "bar");
+ }
+
+ IStatistics sut = fileSystem.Statistics.File;
+
+ Exception? exception = Record.Exception(() =>
+ {
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText)).Never(because);
+ });
+
+ exception.Should().NotBeNull();
+ exception!.Message.Should()
+ .Be(
+ $"Expected method `WriteAllText` to never be called {because}, but it was {text}.");
+ }
+
+ [Fact]
+ public void Never_WhenNotCalled_ShouldNotThrow()
+ {
+ MockFileSystem fileSystem = new();
+ IStatistics sut = fileSystem.Statistics.File;
+
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText)).Never();
+ }
+
+ [Theory]
+ [InlineAutoData(0, "never")]
+ [InlineAutoData(2, "twice")]
+ [InlineAutoData(3, "3 times")]
+ public void Once_WhenCalled_ShouldThrow(
+ int callCount, string text,
+ string because)
+ {
+ MockFileSystem fileSystem = new();
+ for (int i = 0; i < callCount; i++)
+ {
+ fileSystem.File.WriteAllText($"foo-{i}", "bar");
+ }
+
+ IStatistics sut = fileSystem.Statistics.File;
+
+ Exception? exception = Record.Exception(() =>
+ {
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText)).Once(because);
+ });
+
+ exception.Should().NotBeNull();
+ exception!.Message.Should()
+ .Be(
+ $"Expected method `WriteAllText` to be called once {because}, but it was {text}.");
+ }
+
+ [Fact]
+ public void Once_WhenCalledOnce_ShouldNotThrow()
+ {
+ MockFileSystem fileSystem = new();
+ fileSystem.File.WriteAllText("foo", "bar");
+ IStatistics sut = fileSystem.Statistics.File;
+
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText)).Once();
+ }
+
+ [Theory]
+ [InlineAutoData(0, "never")]
+ [InlineAutoData(1, "once")]
+ [InlineAutoData(3, "3 times")]
+ public void Twice_WhenCalled_ShouldThrow(
+ int callCount, string text,
+ string because)
+ {
+ MockFileSystem fileSystem = new();
+ for (int i = 0; i < callCount; i++)
+ {
+ fileSystem.File.WriteAllText($"foo-{i}", "bar");
+ }
+
+ IStatistics sut = fileSystem.Statistics.File;
+
+ Exception? exception = Record.Exception(() =>
+ {
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText)).Twice(because);
+ });
+
+ exception.Should().NotBeNull();
+ exception!.Message.Should()
+ .Be(
+ $"Expected method `WriteAllText` to be called twice {because}, but it was {text}.");
+ }
+
+ [Fact]
+ public void Twice_WhenCalledTwice_ShouldNotThrow()
+ {
+ MockFileSystem fileSystem = new();
+ fileSystem.File.WriteAllText("foo-1", "bar");
+ fileSystem.File.WriteAllText("foo-2", "bar");
+ IStatistics sut = fileSystem.Statistics.File;
+
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText)).Twice();
+ }
+}
diff --git a/Tests/Testably.Abstractions.FluentAssertions.Tests/StatisticMethodAssertionsTests.cs b/Tests/Testably.Abstractions.FluentAssertions.Tests/StatisticMethodAssertionsTests.cs
new file mode 100644
index 0000000..b5d5120
--- /dev/null
+++ b/Tests/Testably.Abstractions.FluentAssertions.Tests/StatisticMethodAssertionsTests.cs
@@ -0,0 +1,280 @@
+using AutoFixture.Xunit2;
+using FluentAssertions;
+using System;
+using System.IO.Abstractions;
+using System.Text;
+using Testably.Abstractions.Testing;
+using Testably.Abstractions.Testing.Statistics;
+using Xunit;
+
+namespace Testably.Abstractions.FluentAssertions.Tests;
+
+public class StatisticMethodAssertionsTests
+{
+ [Theory]
+ [AutoData]
+ public void HaveCalled_Never_WhenCalled_ShouldThrow(
+ string because)
+ {
+ MockFileSystem fileSystem = new();
+ fileSystem.File.WriteAllText("foo", "bar");
+ IStatistics sut = fileSystem.Statistics.File;
+
+ Exception? exception = Record.Exception(() =>
+ {
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText)).Never(because);
+ });
+
+ exception.Should().NotBeNull();
+ exception!.Message.Should()
+ .Be(
+ $"Expected method `WriteAllText` to never be called {because}, but it was once.");
+ }
+
+ [Fact]
+ public void HaveCalled_Never_WhenNotCalled_ShouldNotThrow()
+ {
+ MockFileSystem fileSystem = new();
+ IStatistics sut = fileSystem.Statistics.File;
+
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText)).Never();
+ }
+
+ [Theory]
+ [AutoData]
+ public void WithFirstParameter_WithPredicate_Never_WhenCalled_ShouldThrow(
+ string because)
+ {
+ MockFileSystem fileSystem = new();
+ fileSystem.File.WriteAllText("foo", "bar");
+ IStatistics sut = fileSystem.Statistics.File;
+
+ Exception? exception = Record.Exception(() =>
+ {
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText))
+ .WithFirstParameter(p => p == "foo")
+ .Never(because);
+ });
+
+ exception.Should().NotBeNull();
+ exception!.Message.Should()
+ .Be(
+ $"Expected method `WriteAllText` to never be called {because}, but it was once.");
+ }
+
+ [Fact]
+ public void WithFirstParameter_WithPredicate_Never_WhenNotCalled_ShouldNotThrow()
+ {
+ MockFileSystem fileSystem = new();
+ fileSystem.File.WriteAllText("foo", "bar");
+ IStatistics sut = fileSystem.Statistics.File;
+
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText))
+ .WithFirstParameter(p => p == "bar")
+ .Never();
+ }
+
+ [Theory]
+ [AutoData]
+ public void WithFirstParameter_WithValue_Never_WhenCalled_ShouldThrow(
+ string because)
+ {
+ MockFileSystem fileSystem = new();
+ fileSystem.File.WriteAllText("foo", "bar");
+ IStatistics sut = fileSystem.Statistics.File;
+
+ Exception? exception = Record.Exception(() =>
+ {
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText))
+ .WithFirstParameter("foo")
+ .Never(because);
+ });
+
+ exception.Should().NotBeNull();
+ exception!.Message.Should()
+ .Be(
+ $"Expected method `WriteAllText` to never be called {because}, but it was once.");
+ }
+
+ [Fact]
+ public void WithFirstParameter_WithValue_Never_WhenNotCalled_ShouldNotThrow()
+ {
+ MockFileSystem fileSystem = new();
+ fileSystem.File.WriteAllText("foo", "bar");
+ IStatistics sut = fileSystem.Statistics.File;
+
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText))
+ .WithFirstParameter("bar")
+ .Never();
+ }
+
+ [Theory]
+ [AutoData]
+ public void WithParameterAt_WithPredicate_Never_WhenCalled_ShouldThrow(
+ string because)
+ {
+ MockFileSystem fileSystem = new();
+ fileSystem.File.WriteAllText("foo", "bar");
+ IStatistics sut = fileSystem.Statistics.File;
+
+ Exception? exception = Record.Exception(() =>
+ {
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText))
+ .WithParameterAt(0, p => p == "foo")
+ .Never(because);
+ });
+
+ exception.Should().NotBeNull();
+ exception!.Message.Should()
+ .Be(
+ $"Expected method `WriteAllText` to never be called {because}, but it was once.");
+ }
+
+ [Fact]
+ public void WithParameterAt_WithPredicate_Never_WhenNotCalled_ShouldNotThrow()
+ {
+ MockFileSystem fileSystem = new();
+ fileSystem.File.WriteAllText("foo", "bar");
+ IStatistics sut = fileSystem.Statistics.File;
+
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText))
+ .WithParameterAt(0, p => p == "bar")
+ .Never();
+ }
+
+ [Theory]
+ [AutoData]
+ public void WithParameterAt_WithValue_Never_WhenCalled_ShouldThrow(
+ string because)
+ {
+ MockFileSystem fileSystem = new();
+ fileSystem.File.WriteAllText("foo", "bar");
+ IStatistics sut = fileSystem.Statistics.File;
+
+ Exception? exception = Record.Exception(() =>
+ {
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText))
+ .WithParameterAt(0, "foo")
+ .Never(because);
+ });
+
+ exception.Should().NotBeNull();
+ exception!.Message.Should()
+ .Be(
+ $"Expected method `WriteAllText` to never be called {because}, but it was once.");
+ }
+
+ [Fact]
+ public void WithParameterAt_WithValue_Never_WhenNotCalled_ShouldNotThrow()
+ {
+ MockFileSystem fileSystem = new();
+ fileSystem.File.WriteAllText("foo", "bar");
+ IStatistics sut = fileSystem.Statistics.File;
+
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText))
+ .WithParameterAt(0, "bar")
+ .Never();
+ }
+
+ [Theory]
+ [AutoData]
+ public void WithSecondParameter_WithPredicate_Never_WhenCalled_ShouldThrow(
+ string because)
+ {
+ MockFileSystem fileSystem = new();
+ fileSystem.File.WriteAllText("foo", "bar");
+ IStatistics sut = fileSystem.Statistics.File;
+
+ Exception? exception = Record.Exception(() =>
+ {
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText))
+ .WithSecondParameter(p => p == "bar")
+ .Never(because);
+ });
+
+ exception.Should().NotBeNull();
+ exception!.Message.Should()
+ .Be(
+ $"Expected method `WriteAllText` to never be called {because}, but it was once.");
+ }
+
+ [Fact]
+ public void WithSecondParameter_WithPredicate_Never_WhenNotCalled_ShouldNotThrow()
+ {
+ MockFileSystem fileSystem = new();
+ fileSystem.File.WriteAllText("foo", "bar");
+ IStatistics sut = fileSystem.Statistics.File;
+
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText))
+ .WithSecondParameter(p => p == "foo")
+ .Never();
+ }
+
+ [Theory]
+ [AutoData]
+ public void WithSecondParameter_WithValue_Never_WhenCalled_ShouldThrow(
+ string because)
+ {
+ MockFileSystem fileSystem = new();
+ fileSystem.File.WriteAllText("foo", "bar");
+ IStatistics sut = fileSystem.Statistics.File;
+
+ Exception? exception = Record.Exception(() =>
+ {
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText))
+ .WithSecondParameter("bar")
+ .Never(because);
+ });
+
+ exception.Should().NotBeNull();
+ exception!.Message.Should()
+ .Be(
+ $"Expected method `WriteAllText` to never be called {because}, but it was once.");
+ }
+
+ [Fact]
+ public void WithSecondParameter_WithValue_Never_WhenNotCalled_ShouldNotThrow()
+ {
+ MockFileSystem fileSystem = new();
+ fileSystem.File.WriteAllText("foo", "bar");
+ IStatistics sut = fileSystem.Statistics.File;
+
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText))
+ .WithSecondParameter("foo")
+ .Never();
+ }
+
+ [Theory]
+ [AutoData]
+ public void WithThirdParameter_WithValue_Never_WhenCalled_ShouldThrow(
+ string because)
+ {
+ MockFileSystem fileSystem = new();
+ fileSystem.File.WriteAllText("foo", "bar", Encoding.UTF8);
+ IStatistics sut = fileSystem.Statistics.File;
+
+ Exception? exception = Record.Exception(() =>
+ {
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText))
+ .WithThirdParameter(Encoding.UTF8)
+ .Never(because);
+ });
+
+ exception.Should().NotBeNull();
+ exception!.Message.Should()
+ .Be(
+ $"Expected method `WriteAllText` to never be called {because}, but it was once.");
+ }
+
+ [Fact]
+ public void WithThirdParameter_WithValue_Never_WhenNotCalled_ShouldNotThrow()
+ {
+ MockFileSystem fileSystem = new();
+ fileSystem.File.WriteAllText("foo", "bar", Encoding.UTF8);
+ IStatistics sut = fileSystem.Statistics.File;
+
+ sut.Should().HaveCalled(nameof(IFile.WriteAllText))
+ .WithThirdParameter(Encoding.ASCII)
+ .Never();
+ }
+}
diff --git a/Tests/Testably.Abstractions.FluentAssertions.Tests/StatisticPropertyAssertionsTests.cs b/Tests/Testably.Abstractions.FluentAssertions.Tests/StatisticPropertyAssertionsTests.cs
new file mode 100644
index 0000000..d7f7675
--- /dev/null
+++ b/Tests/Testably.Abstractions.FluentAssertions.Tests/StatisticPropertyAssertionsTests.cs
@@ -0,0 +1,41 @@
+using AutoFixture.Xunit2;
+using FluentAssertions;
+using System;
+using System.IO.Abstractions;
+using Testably.Abstractions.Testing;
+using Testably.Abstractions.Testing.Statistics;
+using Xunit;
+
+namespace Testably.Abstractions.FluentAssertions.Tests;
+
+public class StatisticPropertyAssertionsTests
+{
+ [Theory]
+ [AutoData]
+ public void HaveAccessed_Never_WhenAccessed_ShouldThrow(
+ string because)
+ {
+ MockFileSystem fileSystem = new();
+ _ = fileSystem.Path.DirectorySeparatorChar;
+ IStatistics sut = fileSystem.Statistics.Path;
+
+ Exception? exception = Record.Exception(() =>
+ {
+ sut.Should().HaveAccessed(nameof(IPath.DirectorySeparatorChar)).Never(because);
+ });
+
+ exception.Should().NotBeNull();
+ exception!.Message.Should()
+ .Be(
+ $"Expected property `DirectorySeparatorChar` to never be accessed {because}, but it was once.");
+ }
+
+ [Fact]
+ public void HaveAccessed_Never_WhenNotAccessed_ShouldNotThrow()
+ {
+ MockFileSystem fileSystem = new();
+ IStatistics sut = fileSystem.Statistics.Path;
+
+ sut.Should().HaveAccessed(nameof(IPath.DirectorySeparatorChar)).Never();
+ }
+}