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();
// ↓ Records all events
IEventRecording<Player> recording = player.Record().Events();
IEventRecording<Player> trackRecording = player.Record().Events(nameof(Player.TrackStarted));
// ↑ Records only the TrackStarted event
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.
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 fails with the reason; recording it by name fails right away.
Stopping
An expectation stops the recording: it detaches the handlers from the subject as soon as it is evaluated. Every
constraint of that one expectation still sees the recorded events, because .And and .Or combine into a single
expectation. A further expectation on the same recording fails, 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 Expect.That(recording).Triggered(nameof(Player.TrackStarted)).Once();
player.Play("Yesterday");
// ↓ throws, because the previous expectation already stopped the recording
await Expect.That(recording).Triggered(nameof(Player.TrackStarted)).Twice();
.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 Expect.That(recording).Triggered(nameof(Player.TrackStarted)).Once();
player.Play("Yesterday");
await Expect.That(recording).Triggered(nameof(Player.TrackStarted)).Twice();
Disposing detaches the handlers, so an event that is triggered afterwards is not recorded any more and an expectation on the disposed recording fails as well.
Triggering
You can verify that a recording recorded an event:
IEventRecording<Player> recording = player.Record().Events();
player.Play("Let It Be");
await Expect.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 Expect.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.Times()) 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:
using aweXpect.Core; // for `Times()`
IEventRecording<Player> recording = player.Record().Events();
player.Play("Let It Be");
player.Play("Yesterday");
await Expect.That(recording).Triggered(nameof(Player.TrackStarted)).Between(1).And(2.Times());
The same occurrence constraints as for Contains are available:
AtLeast(2.Times()), AtMost(3.Times()), Between(1).And(4.Times()), Exactly(0.Times()), MoreThan(1.Times()),
LessThan(3.Times()), Once(), Twice() and Never().
Filtering
You can filter the recorded events based on their parameters:
IEventRecording<Player> recording = player.Record().Events();
player.Play("Let It Be");
player.Play("Yesterday");
await Expect.That(recording).Triggered(nameof(Player.TrackStarted))
.WithParameter<TrackStartedEventArgs>(e => e.Title == "Yesterday");
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 Expect.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.
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 Expect.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 Expect.That(recording).Triggered(nameof(Player.TrackStarted))
.With<TrackStartedEventArgs>(e => e.Title.StartsWith("Let"))
.Because("the EventArgs are the second parameter");
Special events
For common events, you can create specific overloads.
Included are some overloads for the
INotifyPropertyChanged.PropertyChanged
event:
AlbumViewModel album = // ...implements INotifyPropertyChanged
using IDisposableEventRecording<AlbumViewModel> recording = album.Record().Events().UntilDisposed();
album.Rename("Let It Be... Naked");
await Expect.That(recording).TriggeredPropertyChanged()
.Because("it should trigger the PropertyChanged event for any property name");
await Expect.That(recording).TriggeredPropertyChangedFor(x => x.Title)
.Because("it should trigger the PropertyChanged event for the 'Title' property name");
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 Expect.That(recording).DidNotTriggerPropertyChanged()
.Because("it should not trigger for any property name");
await Expect.That(recording).DidNotTriggerPropertyChangedFor(x => x.Title)
.Because("it should not trigger for the 'Title' property name");
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.