8000 [API Proposal]: Assertions to repeatedly execute an action until it succeeds · Issue #38 · AwesomeAssertions/AwesomeAssertions · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

[API Proposal]: Assertions to repeatedly execute an action until it succeeds #38

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
cbersch opened this issue Feb 18, 2025 · 12 comments
Assignees
Labels
api-needs-work API needs work before it is approved, it is NOT ready for implementation enhancement New feature or request

Comments

@cbersch
Copy link
Contributor
cbersch commented Feb 18, 2025

Background and motivation

I frequently need some kind of testing functionality to wait until a certain condition is fulfilled (similar to awaitility.org).
That could be an addition to the CompleteWithinAsync functionality.

API Proposal

/// ReferenceTypeAssertions
public async Task<AndConstraint<TAssertions>> SatisfyWithinAsync<T>(Action<T> assertion, TimeSpan timeOut)
    where T : TSubject
public async Task<AndConstraint<TAssertions>> SatisfyWithinAsync<T>(Action<T> assertion, TimeSpan timeOut, IPollingInterval pollingInterval)
    where T : TSubject

/// NonGenericAsyncFunctionAssertions
public async Task<AndConstraint<NonGenericAsyncFunctionAssertions>> SucceedWithinAsync(
    TimeSpan timeSpan, string because = "", params object[] becauseArgs);
public async Task<AndConstraint<NonGenericAsyncFunctionAssertions>> SucceedWithinAsync(
    TimeSpan timeSpan, IPollingInterval pollingInterval, string because = "", params object[] becauseArgs);

/// GenericAsyncFunctionAssertions
public async Task<AndWhichConstraint<GenericAsyncFunctionAssertions<TResult>, TResult>> SucceedWithinAsync(
    TimeSpan timeSpan, string because = "", params object[] becauseArgs);
public async Task<AndWhichConstraint<GenericAsyncFunctionAssertions<TResult>, TResult>> SucceedWithinAsync(
    TimeSpan timeSpan, IPollingInterval pollingInterval, string because = "", params object[] becauseArgs);

/// FunctionAssertions
public async Task<AndWhichConstraint<FunctionAssertions<TResult>, TResult>> SucceedWithinAsync(
    TimeSpan timeSpan, string because = "", params object[] becauseArgs);
public async Task<AndConstraint<FunctionAssertions>> SucceedWithinAsync(
    TimeSpan timeSpan, IPollingInterval pollingInterval, string because = "", params object[] becauseArgs);

/// ActionAssertions
public async Task<AndConstraint<ActionAssertions>> SucceedWithinAsync(
    TimeSpan timeSpan, string because = "", params object[] becauseArgs);
public async Task<AndConstraint<ActionAssertions>> SucceedWithinAsync(
    TimeSpan timeSpan, IPollingInterval pollingInterval, string because = "", params object[] becauseArgs);

public interface IPollingInterval
{
    TimeSpan NextWaitingDuration();
}
/// <summary>
/// Always returns the same fixed waiting time.
/// </summary>
internal class FixedPollingInterval : IPollingInterval

/// <summary>
/// Returns an increasing waiting time (modelled e.g. with Fibonacci sequence)
/// </summary>
internal class IncreasingPollingInterval : IPollingInterval

public static IPollingInterval FixedPolling(this TimeSpan waitingTime) =>
    new FixedPollingInterval(waitingTime);

public static IPollingInterval IncreasingPolling(this TimeSpan initialWaitingTime) =>
    new FixedPollingInterval(waitingTime); 

API Usage

Example 1:

int i = 0; 
Task Increment() => Task.FromResult(i++.Should().Be(27));
Increment.Should().SucceedWithinAsync(100.Milliseconds())

Example 2:

public sealed class BackgroundService
{
    public bool Initialized { get; }
    
    public BackgroundService()
    {
        _ = Task.Run(() =>
        {
            await Task.Delay(TimeSpan.FromSeconds(5));
            Initialized = true;
        };
    }
}
BackgroundService instance = new();
instance.Should().SatisfyWithinAsync(
    x => x.Initialized.Should().BeTrue(), 
    10.Seconds(),
    1.Seconds().FixedPolling());

Alternative Designs

No response

Risks

No risks, this does not touch existing API.

Are you willing to help with a proof-of-concept (as PR in that or a separate repo) first and as pull-request later on?

Yes, please assign this issue to me.

@cbersch cbersch self-assigned this Feb 18, 2025
@cbersch cbersch added the enhancement New feature or request label Feb 18, 2025
@jcfnomada
Copy link

I can see myself using this. Without looking at the implementation, I’d expect it to also support as an example:

await myAsyncFunc.Should().SucceedWithinAsync(TimeSpan.FromSeconds(1)).Which.Should().Be(42);

I also thought it could take a CancellationToken, but to make it safe, that should probably be handled inside the implementation.

@jcfnomada
Copy link

Should we create a Milestone for 8.1.0?

@cbersch
Copy link
Contributor Author
cbersch commented Feb 19, 2025

I can see myself using this. Without looking at the implementation, I’d expect it to also support as an example:

await myAsyncFunc.Should().SucceedWithinAsync(TimeSpan.FromSeconds(1)).Which.Should().Be(42);

Yes, if the myAsyncFunc contains other assertions.
For "normal" code, there already is CompleteWithinAsync, ExecutionTime and ExecutionTimeOf.

@cbersch
Copy link
Contributor Author
cbersch commented Feb 19, 2025

Should we create a Milestone for 8.1.0?

I'm not sure. I can't tell how long it will take to implement it properly.
I wouldn't tight this to a fix milestone at the moment.

Also, we should first get the API sound.
For this I'll add some labels regarding the status of the API proposal (like FA does):
Then, when the API is approved, we can target a milestone.

I have to sort out, how that integrates with the existing CompleteWithinAsync and ExecutionTime functionality, how to model the polling in a "proper", fluent way (like 10.Seconds().FixedPolling() or similar).

@cbersch cbersch added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Feb 19, 2025
@cbersch
Copy link
Contributor Author
cbersch commented Feb 22, 2025

I removed the NotSucceedWith* methods. In years I never needed those. So I would only add them, if anyone shows up with a valid use case.

@cbersch
Copy link
Contributor Author
cbersch commented Feb 23, 2025

Added SatisfyWithinAsync method on ReferenceTypeAssertions and updated Example 2 to use that.
This is, what I actually need all the time.

One could also add a MatchWithinAsync<T>(Expression<Func<T, bool>> predicate, ....

Now, the question is also about testability. For the functionality I need an IClock.DelayAsync, which must be exchangeable for testing with a FakeClock.
All other assertions, which make use of an IClock, get it injected in their constructor, and have respective Should(... IClock) overloads for the tests.

I'm not sure, if we also want an additional ReferenceTypeAssertions(T subject, AssertionChain, IClock) constructor.
One alternative would be to have internal methods

public async Task<AndConstraint<TAssertions>> SatisfyWithinAsync<T>(Action<T> assertion, TimeSpan timeOut, IPollingInterval pollingInterval, IClock clock, string because = "", params object[] becauseArgs)
    where T : TSubject

for testing, and the public method calls it with new Clock(). This is my preferred variant, since it involves only one or two additional methods, and not adding dozens of constructors.

Setting the issue as ready to review, as now I could need some input on the current status.

@cbersch cbersch added api-ready-for-review API is ready for review, it is NOT ready for implementation and removed api-suggestion Early API idea and discussion, it is NOT ready for implementation labels Feb 23, 2025
@vbreuss
Copy link
Contributor
vbreuss commented Feb 25, 2025

In the GenericCollectionAssertions "Satisfy" expects a Func<bool>. That's also what I would intuitively expect in SatisfyWithinAsync instead of an Action<T>...

@cbersch
Copy link
Contributor Author
cbersch commented Feb 25, 2025

In the GenericCollectionAssertions "Satisfy" expects a Func<bool>. That's also what I would intuitively expect in SatisfyWithinAsync instead of an Action<T>...

Hmm, I see.
My starting point was ReferenceType.Satisfy, which takes an Action. And ReferenceType.Match takes an Expression<Func<T, bool>>

@vbreuss
Copy link
Contributor
vbreuss commented Feb 25, 2025

Yeah, it seems that the API surface is inconsistent in this regard...

@cbersch
Copy link
Contributor Author
cbersch commented Feb 26, 2025

Ok, so I went through the curren API.
For Satisfy-like methods we have

GenericCollectionAssertions.SatisfyRespectively(params Action<T>[])
GenericCollectionAssertions.SatisfyRespectively(IEnumerable<Action<T>>)
GenericCollectionAssertions.Satisfy(params Expression<Func<T, bool>>[])
GenericCollectionAssertions.Satisfy(IEnumerable<Expression<Func<T, bool>>>)
GenericCollectionAssertions.AllSatisfy(Action<T>)

ReferenceTypeAssertions.Satisfy<T>(Action<T>)

TypeEnumerableExtensions.ThatSatisfy(this IEnumerable<Type> types, Func<Type, bool> predicate)

and for Match-like (leaving beside the StringAssertions), we have

NumericAssertionsBase.Match(Expression<Func<T, bool>>)
EnumAssertions.Match(Expression<Func<TEnum?, bool>>)
ReferenceTypeAssertions.Match<T>(Expression<Func<T, bool>>)

If we plan to have both variants with Action and Func<bool>, then I would use

MatchWithin(Func<T, bool>)
MatchWithinAsync(Func<T, Task<bool>>)
SatisfyWithin(Action<T>)
SatisfyWithinAsync(Func<T, Task>)

Another point: I don't know why some methods use Action and Func and others Expression<Action> and Expression<Func>: Is that historically grown, and then leaving the API as is, or are there any "real" reasons for these differences.

@vbreuss
Copy link
Contributor
vbreuss commented Feb 26, 2025

Another point: I don't know why some methods use Action and Func and others Expression<Action> and Expression<Func>: Is that historically grown, and then leaving the API as is, or are there any "real" reasons for these differences.

You can use an Expression to create a string representation of the predicate in the failure message.

Nowadays you could use the CallerArgumentExpression instead.

@cbersch
Copy link
Contributor Author
cbersch commented Feb 26, 2025

I've just seen, that the DelegateAssertions already have a NotThrowAfterAsync, which kind of do, what I planned to do. Never came across those methods.
I'll update the proposal according to those finding. Sorry for the loooong issue. Just making myself more comfortable with the raw internals :)

@cbersch cbersch added api-needs-work API needs work before it is approved, it is NOT ready for implementation and removed api-ready-for-review API is ready for review, it is NOT ready for implementation labels Mar 1, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-needs-work API needs work before it is approved, it is NOT ready for implementation enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants
0