diff --git a/Src/FluentAssertions/Primitives/TimeOnlyAssertions.cs b/Src/FluentAssertions/Primitives/TimeOnlyAssertions.cs index aa34829871..8f8a4932c6 100644 --- a/Src/FluentAssertions/Primitives/TimeOnlyAssertions.cs +++ b/Src/FluentAssertions/Primitives/TimeOnlyAssertions.cs @@ -131,10 +131,6 @@ public AndConstraint Be(TimeOnly? expected, string because = "", pa /// Asserts that the current is within the specified time /// from the specified value. /// - /// - /// Use this assertion when, for example the database truncates datetimes to nearest 20ms. If you want to assert to the exact datetime, - /// use . - /// /// /// The expected time to compare the actual value with. /// @@ -168,12 +164,57 @@ public AndConstraint Be(TimeOnly? expected, string because = "", pa Execute.Assertion .BecauseOf(because, becauseArgs) - .WithExpectation("Expected {context:the time} to be within {0} from {1}{reason}", precision, nearbyTime) + .WithExpectation("Expected {context:the time} to be within {0} from {1}{reason}, ", precision, nearbyTime) .ForCondition(Subject is not null) - .FailWith(", but found .") + .FailWith("but found .") .Then .ForCondition((Subject >= minimumValue) && (Subject <= maximumValue)) - .FailWith(", but {0} was off by {1}.", Subject, difference) + .FailWith("but {0} was off by {1}.", Subject, difference) + .Then + .ClearExpectation(); + + return new AndConstraint((TAssertions)this); + } + + /// + /// Asserts that the current is not within the specified time + /// from the specified value. + /// + /// + /// The time to compare the actual value with. + /// + /// + /// The maximum amount of time which the two values must differ. + /// + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndConstraint NotBeCloseTo(TimeOnly distantTime, TimeSpan precision, string because = "", + params object[] becauseArgs) + { + if (precision < TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(precision), $"The value of {nameof(precision)} must be non-negative."); + } + + long distanceToMinInTicks = (distantTime - TimeOnly.MinValue).Ticks; + TimeOnly minimumValue = distantTime.Add(TimeSpan.FromTicks(-Math.Min(precision.Ticks, distanceToMinInTicks))); + + long distanceToMaxInTicks = (TimeOnly.MaxValue - distantTime).Ticks; + TimeOnly maximumValue = distantTime.Add(TimeSpan.FromTicks(Math.Min(precision.Ticks, distanceToMaxInTicks))); + + Execute.Assertion + .BecauseOf(because, becauseArgs) + .WithExpectation("Did not expect {context:the time} to be within {0} from {1}{reason}, ", precision, distantTime) + .ForCondition(Subject is not null) + .FailWith("but found .") + .Then + .ForCondition((Subject < minimumValue) || (Subject > maximumValue)) + .FailWith("but it was {0}.", Subject) .Then .ClearExpectation(); diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt index 5086cb5d72..542d9b08ed 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt @@ -2324,6 +2324,7 @@ namespace FluentAssertions.Primitives public FluentAssertions.AndConstraint NotBe(System.TimeOnly? unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotBeAfter(System.TimeOnly unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotBeBefore(System.TimeOnly unexpected, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint NotBeCloseTo(System.TimeOnly distantTime, System.TimeSpan precision, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotBeOnOrAfter(System.TimeOnly unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotBeOnOrBefore(System.TimeOnly unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotHaveHours(int unexpected, string because = "", params object[] becauseArgs) { } diff --git a/Tests/FluentAssertions.Specs/Primitives/TimeOnlyAssertionSpecs.cs b/Tests/FluentAssertions.Specs/Primitives/TimeOnlyAssertionSpecs.cs index 797fd4edaa..7d11af7efd 100644 --- a/Tests/FluentAssertions.Specs/Primitives/TimeOnlyAssertionSpecs.cs +++ b/Tests/FluentAssertions.Specs/Primitives/TimeOnlyAssertionSpecs.cs @@ -195,11 +195,8 @@ public void When_a_time_is_close_to_a_later_time_by_one_tick_it_should_succeed() var dateTime = TimeOnly.FromDateTime(DateTime.UtcNow); var actual = new TimeOnly(dateTime.Ticks - 1); - // Act - Action act = () => actual.Should().BeCloseTo(dateTime, TimeSpan.FromTicks(1)); - - // Assert - act.Should().NotThrow(); + // Act / Assert + actual.Should().BeCloseTo(dateTime, TimeSpan.FromTicks(1)); } [Fact] @@ -209,11 +206,8 @@ public void When_a_time_is_close_to_an_earlier_time_by_one_tick_it_should_succee var dateTime = TimeOnly.FromDateTime(DateTime.UtcNow); var actual = new TimeOnly(dateTime.Ticks - 1); - // Act - Action act = () => actual.Should().BeCloseTo(dateTime, TimeSpan.FromTicks(1)); - - // Assert - act.Should().NotThrow(); + // Act / Assert + actual.Should().BeCloseTo(dateTime, TimeSpan.FromTicks(1)); } [Fact] @@ -223,11 +217,8 @@ public void When_asserting_subject_time_is_close_to_the_minimum_time_it_should_s TimeOnly time = TimeOnly.MinValue.Add(50.Milliseconds()); TimeOnly nearbyTime = TimeOnly.MinValue; - // Act - Action act = () => time.Should().BeCloseTo(nearbyTime, 100.Milliseconds()); - - // Assert - act.Should().NotThrow(); + // Act / Assert + time.Should().BeCloseTo(nearbyTime, 100.Milliseconds()); } [Fact] @@ -237,11 +228,8 @@ public void When_asserting_subject_time_is_close_to_the_maximum_time_it_should_s TimeOnly time = TimeOnly.MaxValue.Add(-50.Milliseconds()); TimeOnly nearbyTime = TimeOnly.MaxValue; - // Act - Action act = () => time.Should().BeCloseTo(nearbyTime, 100.Milliseconds()); - - // Assert - act.Should().NotThrow(); + // Act / Assert + time.Should().BeCloseTo(nearbyTime, 100.Milliseconds()); } [Fact] @@ -283,26 +271,224 @@ public void When_asserting_subject_time_is_close_to_an_earlier_time_by_35ms_it_s TimeOnly time = new TimeOnly(12, 15, 31, 035); TimeOnly nearbyTime = new TimeOnly(12, 15, 31); + // Act / Assert + time.Should().BeCloseTo(nearbyTime, 35.Milliseconds()); + } + + [Fact] + public void When_asserting_subject_nulltime_is_close_to_another_it_should_throw() + { + // Arrange + TimeOnly? time = null; + TimeOnly nearbyTime = new TimeOnly(12, 15, 31); + // Act Action act = () => time.Should().BeCloseTo(nearbyTime, 35.Milliseconds()); // Assert - act.Should().NotThrow(); + act.Should().Throw() + .WithMessage("Expected*, but found ."); } + } + public class NotBeCloseTo + { [Fact] - public void When_asserting_subject_nulltime_is_close_to_another_it_should_throw() + public void A_null_time_is_never_unclose_to_an_other_time() { // Arrange TimeOnly? time = null; TimeOnly nearbyTime = new TimeOnly(12, 15, 31); // Act - Action act = () => time.Should().BeCloseTo(nearbyTime, 35.Milliseconds()); + Action act = () => time.Should().NotBeCloseTo(nearbyTime, 35.Milliseconds()); // Assert act.Should().Throw() - .WithMessage("Expected*, but found ."); + .WithMessage("Did not expect*, but found ."); + } + + [Fact] + public void When_asserting_that_time_is_not_close_to_a_negative_precision_it_should_throw() + { + // Arrange + var dateTime = TimeOnly.FromDateTime(DateTime.UtcNow); + var actual = new TimeOnly(dateTime.Ticks - 1); + + // Act + Action act = () => actual.Should().NotBeCloseTo(dateTime, -1.Ticks()); + + // Assert + act.Should().Throw() + .WithMessage("* value of precision must be non-negative*"); + } + + [Fact] + public void When_a_time_is_close_to_a_later_time_by_one_tick_it_should_fail() + { + // Arrange + var dateTime = TimeOnly.FromDateTime(DateTime.UtcNow); + var actual = new TimeOnly(dateTime.Ticks - 1); + + // Act + Action act = () => actual.Should().NotBeCloseTo(dateTime, TimeSpan.FromTicks(1)); + + // Assert + act.Should().Throw(); + } + + [Fact] + public void When_a_time_is_close_to_an_earlier_time_by_one_tick_it_should_fail() + { + // Arrange + var dateTime = TimeOnly.FromDateTime(DateTime.UtcNow); + var actual = new TimeOnly(dateTime.Ticks + 1); + + // Act + Action act = () => actual.Should().NotBeCloseTo(dateTime, TimeSpan.FromTicks(1)); + + // Assert + act.Should().Throw(); + } + + [Fact] + public void When_a_time_is_close_to_a_min_value_by_one_tick_it_should_fail() + { + // Arrange + var dateTime = TimeOnly.MinValue; + var actual = new TimeOnly(dateTime.Ticks + 1); + + // Act + Action act = () => actual.Should().NotBeCloseTo(dateTime, TimeSpan.FromTicks(1)); + + // Assert + act.Should().Throw(); + } + + [Fact] + public void When_a_time_is_close_to_a_max_value_by_one_tick_it_should_fail() + { + // Arrange + var dateTime = TimeOnly.MaxValue; + var actual = new TimeOnly(dateTime.Ticks - 1); + + // Act + Action act = () => actual.Should().NotBeCloseTo(dateTime, TimeSpan.FromTicks(1)); + + // Assert + act.Should().Throw(); + } + + [Fact] + public void When_asserting_subject_time_is_not_close_to_an_earlier_time_it_should_throw() + { + // Arrange + TimeOnly time = new TimeOnly(12, 15, 31, 020); + TimeOnly nearbyTime = new TimeOnly(12, 15, 31); + + // Act + Action act = () => time.Should().NotBeCloseTo(nearbyTime, 20.Milliseconds()); + + // Assert + act.Should().Throw() + .WithMessage("Did not expect time to be within 20ms from <12:15:31.000>, but it was <12:15:31.020>."); + } + + [Fact] + public void When_asserting_subject_time_is_not_close_to_an_earlier_time_by_a_20ms_timespan_it_should_throw() + { + // Arrange + TimeOnly time = new TimeOnly(12, 15, 31, 020); + TimeOnly nearbyTime = new TimeOnly(12, 15, 31); + + // Act + Action act = () => time.Should().NotBeCloseTo(nearbyTime, TimeSpan.FromMilliseconds(20)); + + // Assert + act.Should().Throw() + .WithMessage("Did not expect time to be within 20ms from <12:15:31.000>, but it was <12:15:31.020>."); + } + + [Fact] + public void When_asserting_subject_time_is_not_close_to_another_value_that_is_later_by_more_than_20ms_it_should_succeed() + { + // Arrange + TimeOnly time = new TimeOnly(12, 15, 30, 979); + TimeOnly nearbyTime = new TimeOnly(12, 15, 31); + + // Act / Assert + time.Should().NotBeCloseTo(nearbyTime, 20.Milliseconds()); + } + + [Fact] + public void When_asserting_subject_time_is_not_close_to_another_value_that_is_earlier_by_more_than_20ms_it_should_succeed() + { + // Arrange + TimeOnly time = new TimeOnly(12, 15, 31, 021); + TimeOnly nearbyTime = new TimeOnly(12, 15, 31); + + // Act / Assert + time.Should().NotBeCloseTo(nearbyTime, 20.Milliseconds()); + } + + [Fact] + public void When_asserting_subject_datetime_is_not_close_to_an_earlier_datetime_by_35ms_it_should_throw() + { + // Arrange + TimeOnly time = new TimeOnly(12, 15, 31, 035); + TimeOnly nearbyTime = new TimeOnly(12, 15, 31); + + // Act + Action act = () => time.Should().NotBeCloseTo(nearbyTime, 35.Milliseconds()); + + // Assert + act.Should().Throw() + .WithMessage("Did not expect time to be within 35ms from <12:15:31.000>, but it was <12:15:31.035>."); + } + + [Fact] + public void When_asserting_subject_null_time_is_not_close_to_another_it_should_throw() + { + // Arrange + TimeOnly? time = null; + TimeOnly nearbyTime = new TimeOnly(12, 15, 31); + + // Act + Action act = () => time.Should().NotBeCloseTo(nearbyTime, 35.Milliseconds()); + + // Assert + act.Should().Throw() + .WithMessage("Did not expect*, but found ."); + } + + [Fact] + public void When_asserting_subject_time_is_not_close_to_the_minimum_time_it_should_throw() + { + // Arrange + TimeOnly time = TimeOnly.MinValue.Add(50.Milliseconds()); + TimeOnly nearbyTime = TimeOnly.MinValue; + + // Act + Action act = () => time.Should().NotBeCloseTo(nearbyTime, 100.Milliseconds()); + + // Assert + act.Should().Throw() + .WithMessage("Did not expect time to be within 100ms from <00:00:00.000>, but it was <00:00:00.050>."); + } + + [Fact] + public void When_asserting_subject_time_is_not_close_to_the_maximum_time_it_should_throw() + { + // Arrange + TimeOnly time = TimeOnly.MaxValue.Add(-50.Milliseconds()); + TimeOnly nearbyTime = TimeOnly.MaxValue; + + // Act + Action act = () => time.Should().NotBeCloseTo(nearbyTime, 100.Milliseconds()); + + // Assert + act.Should().Throw() + .WithMessage("Did not expect time to be within 100ms from <23:59:59.999>, but it was <23:59:59.949>."); } }