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