Table of Contents

Class WillLogAssertion

Namespace
LogAssertions.TUnit
Assembly
LogAssertions.TUnit.dll

TUnit assertion that waits until a Microsoft.Extensions.Logging.Testing.FakeLogCollector holds matching log records: the event-driven form of HasLogged() for records that background work logs after the call that started it returns. Entry point: Assert.That(collector).WillLog(timeout, cancellationToken), then the same filters as HasLogged() and a count terminator.

[AssertionExtension("WillLog")]
public sealed class WillLogAssertion : LogAssertionBase<WillLogAssertion>, IAssertion
Inheritance
Assertion<FakeLogCollector>
WillLogAssertion
Implements
IAssertion
Inherited Members
Assertion<FakeLogCollector>.AssertAsync()
Assertion<FakeLogCollector>.GetAwaiter()
Assertion<FakeLogCollector>.And
Assertion<FakeLogCollector>.Or

Examples

await Assert.That(collector).WillLog(TimeSpan.FromSeconds(5), cancellationToken)
    .AtLevel(LogLevel.Warning).Containing("would collide", StringComparison.Ordinal).Once();

Remarks

It waits on the collector's stream of records, the one WaitForLogAsync(FakeLogCollector, ILogRecordFilter, CancellationToken) reads. The count is read from the collector right after the minimum number of matching records has been captured. Records captured by then count, including ones logged before the assertion ran. A record that arrives after the assertion has passed isn't checked: to pin "no more than one", assert HasLogged()...Once() after the work has settled. The assertion fails when that count is above the maximum. When the timeout or the token ends the wait, the records the collector holds at that moment decide: enough matching records pass (also a record captured just before the timeout fired, or records already captured when the token was canceled before the assertion ran); otherwise it fails with the expectation, the count, and the captured records, the same diagnostics as HasLogged(), not a bare cancellation. A chain whose levels all sit below the collector's capture floor fails at once, since such a record can never be captured.

Clock: the timeout runs in real time (System), whatever clock the collector uses, and starts when the assertion runs (at await), not when the chain is built. It bounds how long the test waits for real background work. When that work waits on a fake clock, start the assertion first, advance the clock, then await the assertion: put the chain in a local function that awaits it (TUnit's analyzer requires every Assert.That to be awaited), call it, advance, and await the task it returned. That await is what surfaces the failure message: a helper that advances until the task completes surfaces it only if the helper awaits the task too.

A wait proves that records arrived, not that no more follow, so there is no Never() or AtMost(n): to pin "no more than one", let the work finish and assert HasLogged()...Once(); for absence, use HasNotLogged() after the work has finished. The wait is built on WaitForLogsAsync(FakeLogCollector, ILogRecordFilter, int, CancellationToken): no polling and no real-time delay; the timeout is a bound, not a poll interval.

Constructors

WillLogAssertion(AssertionContext<FakeLogCollector>, TimeSpan, CancellationToken)

Initializes a waiting log assertion bounded by timeout (real time) and cancellationToken. Called by the TUnit source generator.

public WillLogAssertion(AssertionContext<FakeLogCollector> context, TimeSpan timeout, CancellationToken cancellationToken = default)

Parameters

context AssertionContext<FakeLogCollector>

The assertion context supplied by TUnit.

timeout TimeSpan

The longest the wait may take, in real time, counted from when the assertion runs. Must be positive and at most about 49 days. Choose it below the test's own timeout, so the wait fails with its diagnostics first.

cancellationToken CancellationToken

Ends the wait early, for example the test's token.

Remarks

The count is read from the collector right after the minimum number of matching records has been captured. Records captured by then count, including ones logged before the assertion ran. A record that arrives after the assertion has passed isn't checked: to pin "no more than one", assert HasLogged()...Once() after the work has settled.

Clock: timeout runs on System, from the moment the assertion runs.

Exceptions

ArgumentOutOfRangeException

timeout is zero, negative (including InfiniteTimeSpan), or longer than a CancellationTokenSource accepts.

Methods

AtLeast(int)

Waits for at least count matching records. Without a count terminator the chain waits for at least one.

public WillLogAssertion AtLeast(int count)

Parameters

count int

The minimum. Must be positive.

Returns

WillLogAssertion

This assertion for chaining.

Exceptions

ArgumentOutOfRangeException

count is not positive.

Between(int, int)

Waits until the count of matching records is in the inclusive range [min, max], and fails at once if the wait first sees more than max.

public WillLogAssertion Between(int min, int max)

Parameters

min int

The minimum (inclusive). Must be positive.

max int

The maximum (inclusive). Must be at least min.

Returns

WillLogAssertion

This assertion for chaining.

Exceptions

ArgumentOutOfRangeException

min is not positive, or max is less than min.

CheckAsync(EvaluationMetadata<FakeLogCollector>)

Implements the specific check logic for this assertion. Called after the context has been evaluated. Override this method if your assertion uses the default AssertAsync() flow. If you override AssertAsync() with custom logic (like AndAssertion/OrAssertion), you don't need to implement this.

protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<FakeLogCollector> metadata)

Parameters

metadata EvaluationMetadata<FakeLogCollector>

Metadata about the evaluation including value, exception, and timing information

Returns

Task<AssertionResult>

The result of the assertion check

Exactly(int)

Waits for exactly count matching records: passes when the count reaches it, and fails at once if the wait first sees more.

public WillLogAssertion Exactly(int count)

Parameters

count int

The count. Must be positive.

Returns

WillLogAssertion

This assertion for chaining.

Exceptions

ArgumentOutOfRangeException

count is not positive.

GetExpectation()

Gets a human-readable description of what this assertion expects. Used in error messages.

protected override string GetExpectation()

Returns

string

GetMatch()

Runs the assertion and returns the single matched record once the wait passes, as HasLogged().GetMatch() does. Requires the count to be exactly one (Once(), Exactly(1), or Between(1, 1)).

public Task<FakeLogRecord> GetMatch()

Returns

Task<FakeLogRecord>

The single matched record.

Exceptions

InvalidOperationException

The chain's count expectation is not exactly one; or the assertion failed inside Assert.Multiple(), which records the failure instead of throwing it, so there is no match to return (the message names the failure, and the scope still reports it).

GetMatches()

Runs the assertion and returns the matched records once the wait passes, as HasLogged().GetMatches() does.

public Task<IReadOnlyList<FakeLogRecord>> GetMatches()

Returns

Task<IReadOnlyList<FakeLogRecord>>

The records that matched when the wait passed, in the order they were logged; a snapshot.

Exceptions

InvalidOperationException

The assertion failed inside Assert.Multiple(), which records the failure instead of throwing it, so there are no matches to return; the message names the failure, and the scope still reports it.

Once()

Waits for exactly one matching record: passes when the first arrives, and fails at once if the wait first sees two.

public WillLogAssertion Once()

Returns

WillLogAssertion

This assertion for chaining.