Events
Describes the possible expectations for verifying events.
| Expectation | Negated | Summary |
|---|---|---|
Triggered | DidNotTrigger | the recording recorded the event |
TriggeredPropertyChanged | DidNotTriggerPropertyChanged | PropertyChanged was raised for any property |
TriggeredPropertyChangedFor | DidNotTriggerPropertyChangedFor | PropertyChanged was raised for the property |
The events are recorded first. The samples on this page use the following recording:
using aweXpect.Recording;
class TrackStartedEventArgs(string title = "") : EventArgs
{
public string Title { get; } = title;
}
class Player
{
public event EventHandler<TrackStartedEventArgs>? TrackStarted;
public void Play(string title)
=> TrackStarted?.Invoke(this, new TrackStartedEventArgs(title));
}
Player player = new Player();
IEventRecording<Player> recording = player.Record().Events();
Recording
.Record().Events() in the aweXpect.Recording namespace starts a recording of all events of the subject, or of the
events with the given names:
IEventRecording<Player> recording = player.Record().Events(nameof(Player.TrackStarted));
player.Play("Let It Be");
await That(recording).Triggered(nameof(Player.TrackStarted));
An expectation stops the recording, unless you keep it running with .UntilDisposed().
Events that cannot be recorded
Without a registration from the source generator, the handler is bound
reflectively. Such a handler must take at most four parameters, must return nothing and must take no parameter by
reference. Recording all events skips an event whose handler does not fit, so that the other events of the subject are
still recorded, and an expectation on the skipped event throws a NotSupportedException with the reason; recording it
by name throws right away.
Triggering
You can verify that a recording recorded an event:
IEventRecording<Player> recording = player.Record().Events();
player.Play("Let It Be");
await That(recording).Triggered(nameof(Player.TrackStarted));
You can also verify that a recording did not record an event:
IEventRecording<Player> recording = player.Record().Events();
// Perform an action on the player that must not start a track
await That(recording).DidNotTrigger(nameof(Player.TrackStarted));
Triggered expects the event at least once, and DidNotTrigger is equivalent to Triggered(…).Never(). A count
negates the expectation, so DidNotTrigger(nameof(Player.TrackStarted)).AtLeast(2) expects the event to be triggered
less than twice.
Without Within(…), only the events recorded so far count. To wait for events that are triggered in the background,
see waiting for events.
Counting
You can verify that an event was recorded a specific number of times:
IEventRecording<Player> recording = player.Record().Events();
player.Play("Let It Be");
player.Play("Yesterday");
await That(recording).Triggered(nameof(Player.TrackStarted)).Between(1).And(2);
The same occurrence constraints as for Contains are available:
AtLeast(2), AtMost(3), Between(1).And(4), Exactly(0), MoreThan(1), LessThan(3), Once(), Twice() and
Never(). AtLeast, AtMost, MoreThan and LessThan can also be followed by Once() or Twice(), as in
AtLeast().Once(), AtMost().Twice(), MoreThan().Once() and LessThan().Twice():
IEventRecording<Player> recording = player.Record().Events();
player.Play("Let It Be");
await That(recording).Triggered(nameof(Player.TrackStarted)).AtMost().Twice();
Filtering
You can filter the recorded events based on their parameters, and count only the matching ones:
IEventRecording<Player> recording = player.Record().Events();
player.Play("Let It Be");
player.Play("Yesterday");
await That(recording).Triggered(nameof(Player.TrackStarted))
.WithParameter<TrackStartedEventArgs>(e => e.Title == "Yesterday");
await That(recording).Triggered(nameof(Player.TrackStarted))
.WithParameter<TrackStartedEventArgs>(e => e.Title == "Yesterday").Once();
This matches an event when any of its parameters is of the given type and satisfies the predicate. To check the parameter at a specific zero-based position instead, pass the position first:
IEventRecording<Player> recording = player.Record().Events();
player.Play("Yesterday");
await That(recording).Triggered(nameof(Player.TrackStarted))
.WithParameter<TrackStartedEventArgs>(1, e => e.Title == "Yesterday");
An event whose parameter at that position is missing or of another type does not match.
null parameters
A parameter that is null is passed to the predicate, unless the given type is a non-nullable value type; without a
position, null parameters are ignored. The predicates for the sender and for the EventArgs below also receive a
value that is null.
When you follow
the event best practices,
you can also filter the recorded events based on the sender (the first parameter) or on their EventArgs (the second
parameter):
IEventRecording<Player> recording = player.Record().Events();
player.Play("Let It Be");
await That(recording).Triggered(nameof(Player.TrackStarted))
.WithSender(s => s == player)
.Because("the sender is the first parameter");
IEventRecording<Player> recording = player.Record().Events();
player.Play("Let It Be");
await That(recording).Triggered(nameof(Player.TrackStarted))
.With<TrackStartedEventArgs>(e => e.Title.StartsWith("Let"))
.Because("the EventArgs are the second parameter");
Special events
You can verify that an object triggered the
INotifyPropertyChanged.PropertyChanged
event, for any property or for a specific one:
AlbumViewModel album = // ...implements INotifyPropertyChanged
using IDisposableEventRecording<AlbumViewModel> recording = album.Record().Events().UntilDisposed();
album.Rename("Let It Be... Naked");
await That(recording).TriggeredPropertyChanged();
await That(recording).TriggeredPropertyChangedFor(x => x.Title);
The expression has to access a property directly on the subject, as in x => x.Title. For any other name, use the
overload with the property name, e.g. TriggeredPropertyChangedFor("Title").
The negated expectations verify that the event was not triggered:
AlbumViewModel album = // ...implements INotifyPropertyChanged
using IDisposableEventRecording<AlbumViewModel> recording = album.Record().Events().UntilDisposed();
// do something that must not change the album
await That(recording).DidNotTriggerPropertyChanged();
await That(recording).DidNotTriggerPropertyChangedFor(x => x.Title);
An event with a null or empty property name counts as a change of every property.
null or empty property names
As defined by the INotifyPropertyChanged
contract,
an event that was triggered with a null or empty property name notifies that all properties changed: it
satisfies TriggeredPropertyChangedFor for every property name and lets DidNotTriggerPropertyChangedFor
fail for every property name. A whitespace-only name is a name like any other. Expecting the null or the empty
property name itself, e.g. TriggeredPropertyChangedFor((string?)null), matches only the events that notify that all
properties changed, but no named one, and without distinguishing the two spellings, which the contract allows
interchangeably. The expectation then reads "for all properties".
Lifetime of a recording
An expectation stops the recording: it detaches the handlers from the subject as soon as it is evaluated. Until then,
every constraint of that one expectation sees the recorded events, including the ones that arrive while it waits with
Within(…), because .And and .Or combine into a single expectation. A further expectation on the same recording
throws an InvalidOperationException, so that it cannot silently answer from the events that were recorded until then:
IEventRecording<Player> recording = player.Record().Events();
player.Play("Let It Be");
await That(recording).Triggered(nameof(Player.TrackStarted)).Once();
player.Play("Yesterday");
// ↓ throws, because the previous expectation already stopped the recording
await That(recording).Triggered(nameof(Player.TrackStarted)).Twice();
Expect.ThatAll(…) and Expect.ThatAny(…)
The expectations within one Expect.ThatAll(…) or Expect.ThatAny(…), including nested ones, share the recording: it
is stopped when the whole combination was evaluated. They are evaluated one after the other, so the events that arrive
while one of them waits with Within(…) also count for the following ones, as with .And.
.UntilDisposed() keeps the recording running across multiple expectations and hands its lifetime to you:
using IDisposableEventRecording<Player> recording = player.Record().Events().UntilDisposed();
player.Play("Let It Be");
await That(recording).Triggered(nameof(Player.TrackStarted)).Once();
player.Play("Yesterday");
await That(recording).Triggered(nameof(Player.TrackStarted)).Twice();
Disposing detaches the handlers, so an event that is triggered afterwards is not recorded any more.
Stopped or disposed recordings
Call .UntilDisposed() before the first expectation: on a recording that an expectation already stopped, it throws an
InvalidOperationException. An expectation on a disposed recording throws as well.