Skip to content

Commit

Permalink
Add BeCloseTo() to TimeOnlyAssertions
Browse files Browse the repository at this point in the history
  • Loading branch information
IT-VBFK committed Nov 7, 2022
1 parent b900216 commit 713b5f9
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 0 deletions.
53 changes: 53 additions & 0 deletions Src/FluentAssertions/Primitives/TimeOnlyAssertions.cs
Expand Up @@ -127,6 +127,59 @@ public AndConstraint<TAssertions> Be(TimeOnly? expected, string because = "", pa
return new AndConstraint<TAssertions>((TAssertions)this);
}

/// <summary>
/// Asserts that the current <see cref="TimeOnly"/> is within the specified time
/// from the specified <paramref name="nearbyTime"/> value.
/// </summary>
/// <remarks>
/// Use this assertion when, for example the database truncates datetimes to nearest 20ms. If you want to assert to the exact datetime,
/// use <see cref="Be(TimeOnly, string, object[])"/>.
/// </remarks>
/// <param name="nearbyTime">
/// The expected time to compare the actual value with.
/// </param>
/// <param name="precision">
/// The maximum amount of time which the two values may differ.
/// </param>
/// <param name="because">
/// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion
/// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
/// </param>
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
/// </param>
public AndConstraint<TAssertions> BeCloseTo(TimeOnly nearbyTime, 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 = (nearbyTime - TimeOnly.MinValue).Ticks;
TimeOnly minimumValue = nearbyTime.Add(-TimeSpan.FromTicks(Math.Min(precision.Ticks, distanceToMinInTicks)));

long distanceToMaxInTicks = (TimeOnly.MaxValue - nearbyTime).Ticks;
TimeOnly maximumValue = nearbyTime.Add(TimeSpan.FromTicks(Math.Min(precision.Ticks, distanceToMaxInTicks)));

TimeSpan? difference = (Subject >= nearbyTime
? Subject - nearbyTime
: nearbyTime - Subject)?.Duration();

Execute.Assertion
.BecauseOf(because, becauseArgs)
.WithExpectation("Expected {context:the time} to be within {0} from {1}{reason}", precision, nearbyTime)
.ForCondition(Subject is not null)
.FailWith(", but found <null>.")
.Then
.ForCondition((Subject >= minimumValue) && (Subject <= maximumValue))
.FailWith(", but {0} was off by {1}.", Subject, difference)
.Then
.ClearExpectation();

return new AndConstraint<TAssertions>((TAssertions)this);
}

/// <summary>
/// Asserts that the current <see cref="TimeOnly"/> is before the specified value.
/// </summary>
Expand Down
Expand Up @@ -2308,6 +2308,7 @@ namespace FluentAssertions.Primitives
public FluentAssertions.AndConstraint<TAssertions> Be(System.TimeOnly? expected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeAfter(System.TimeOnly expected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeBefore(System.TimeOnly expected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeCloseTo(System.TimeOnly nearbyTime, System.TimeSpan precision, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeOnOrAfter(System.TimeOnly expected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeOnOrBefore(System.TimeOnly expected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeOneOf(params System.Nullable<System.TimeOnly>[] validValues) { }
Expand Down
136 changes: 136 additions & 0 deletions Tests/FluentAssertions.Specs/Primitives/TimeOnlyAssertionSpecs.cs
@@ -1,4 +1,5 @@
using System;
using FluentAssertions.Extensions;
using Xunit;
using Xunit.Sdk;

Expand Down Expand Up @@ -170,6 +171,141 @@ public void Should_succeed_when_asserting_timeonly_value_is_not_equal_to_a_diffe
}
}

public class BeCloseTo
{
[Fact]
public void When_asserting_that_time_is_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().BeCloseTo(dateTime, -1.Ticks());

// Assert
act.Should().Throw<ArgumentOutOfRangeException>()
.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_succeed()
{
// Arrange
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();
}

[Fact]
public void When_a_time_is_close_to_an_earlier_time_by_one_tick_it_should_succeed()
{
// Arrange
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();
}

[Fact]
public void When_asserting_subject_time_is_close_to_the_minimum_time_it_should_succeed()
{
// Arrange
TimeOnly time = TimeOnly.MinValue.Add(50.Milliseconds());
TimeOnly nearbyTime = TimeOnly.MinValue;

// Act
Action act = () => time.Should().BeCloseTo(nearbyTime, 100.Milliseconds());

// Assert
act.Should().NotThrow();
}

[Fact]
public void When_asserting_subject_time_is_close_to_the_maximum_time_it_should_succeed()
{
// Arrange
TimeOnly time = TimeOnly.MaxValue.Add(-50.Milliseconds());
TimeOnly nearbyTime = TimeOnly.MaxValue;

// Act
Action act = () => time.Should().BeCloseTo(nearbyTime, 100.Milliseconds());

// Assert
act.Should().NotThrow();
}

[Fact]
public void When_asserting_subject_time_is_close_to_another_value_that_is_later_by_more_than_20ms_it_should_throw()
{
// Arrange
TimeOnly time = new TimeOnly(12, 15, 30, 979);
TimeOnly nearbyTime = new TimeOnly(12, 15, 31);

// Act
Action act = () => time.Should().BeCloseTo(nearbyTime, 20.Milliseconds());

// Assert
act.Should().Throw<XunitException>()
.WithMessage(
"Expected time to be within 20ms from <12:15:31.000>, but <12:15:30.979> was off by 21ms.");
}

[Fact]
public void When_asserting_subject_time_is_close_to_another_value_that_is_earlier_by_more_than_20ms_it_should_throw()
{
// Arrange
TimeOnly time = new TimeOnly(12, 15, 31, 021);
TimeOnly nearbyTime = new TimeOnly(12, 15, 31);

// Act
Action act = () => time.Should().BeCloseTo(nearbyTime, 20.Milliseconds());

// Assert
act.Should().Throw<XunitException>()
.WithMessage(
"Expected time to be within 20ms from <12:15:31.000>, but <12:15:31.021> was off by 21ms.");
}

[Fact]
public void When_asserting_subject_time_is_close_to_an_earlier_time_by_35ms_it_should_succeed()
{
// Arrange
TimeOnly time = new TimeOnly(12, 15, 31, 035);
TimeOnly nearbyTime = new TimeOnly(12, 15, 31);

// Act
Action act = () => time.Should().BeCloseTo(nearbyTime, 35.Milliseconds());

// Assert
act.Should().NotThrow();
}

[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().Throw<XunitException>()
.WithMessage("Expected*, but found <null>.");
}
}

public class BeBefore
{
[Fact]
Expand Down

0 comments on commit 713b5f9

Please sign in to comment.