What's new in v4
aweXpect v4 releases all packages together with one version and gives them one vocabulary: the extension packages follow the same naming, option and nesting rules as aweXpect, and the types that extension authors build on are consistent across Core and the packages. This page covers what you need to know to upgrade. The complete list of changes, pull request by pull request, is in the GitHub releases.
To upgrade:
- Update all aweXpect packages to the same 4.x version and fix the compile errors. Most of them are renames or changes in an extension package.
- Run your tests. Some failure messages changed and a few expectations are stricter now; the behavior changes explain them.
- If you wrote your own expectations, also read Extensions.
Most changes only affect code that names a result type or passes an argument by name; chained expectations keep compiling.
One version for all packages
aweXpect.Json, aweXpect.Web, aweXpect.Xml, aweXpect.Reflection and aweXpect.Snapshots (with
aweXpect.Snapshots.DiffEngine and aweXpect.Snapshots.PublicApi) are now released together with aweXpect and
aweXpect.Core, with one version. Use the same version for all of them; a breaking change in any package waits for
the next major version.
- The extension packages jump from their own versions to 4.0.0.
aweXpect.Xmland theaweXpect.Snapshotspackages are still in beta and are published as4.x.y-beta.1.- The extension packages are now signed with the key of aweXpect, so their public key token changes. This only
matters for
InternalsVisibleToentries or binding redirects that name it.
Renamed expectations
None of the renames has an [Obsolete] forwarder; each is a compile error that is fixed once:
| v3 | v4 |
|---|---|
IsEquivalentTo(…, o => o.Ignoring(predicate)) | IsEquivalentTo(…, o => o.IgnoringMembers(predicate)) |
IsEquivalentTo(…, o => o.Ignoring(type => …)) | IsEquivalentTo(…, o => o.IgnoringMembers((_, type) => …)) |
IsValidJson().Which(j => …) | IsValidJson(j => …) |
IsValidXml().Which(root => …) | IsValidXml(root => …) |
HasStatusCode().DifferentTo(…) | HasStatusCode().NotEqualTo(…) |
WithJsonOptions(…) in the Json options | WithDocumentOptions(…) |
o => o with { … } on JsonOptions | the option methods, e.g. o => o.IgnoringArrayOrder() |
Ignoring(predicate) in the Json and Xml options | IgnoringMembers(predicate) |
IsValidJson(o => o with { … }) | IsValidJson(o => o.WithDocumentOptions(d => d with { … })) |
IsValidJson(null) without a parameter name is now ambiguous; write IsValidJson(options: null).
Parameters that name a looked-up item are called name (HasHeader(name), DoesNotHaveHeader(name)), and compared
values are called expected or unexpected. This only affects arguments passed by name.
Behavior changes
Failure messages
Failure messages were aligned across the packages, so tests that assert on the exact text of a failure message may need an update. Among others:
Containing(…)on a string property such asHasMessage()names the quantifier, e.g.has message containing "foo" at least once.- Conflicting options are reported with one wording, e.g.
Using cannot be combined with AsWildcard. - In a plural context the noun of a generated kind check stays singular, e.g.
whose Items are a boolean for all items, and a negated JSON kind check names the actual kind. - The JSON and XML options are listed in the failure of the comparison itself, not in the failure of an unrelated expectation on the same subject.
- A missing JSON property and a missing or unexpected XML node name the value like a missing array element, e.g.
$.title had missing "Let It Be"instead of$.title was missing. - The status code expectations read like the operators of the other property results, e.g.
DoesNotHaveStatusCode(404)andHasStatusCode().NotEqualTo(404)readdoes not have status code equal to 404 NotFound, andHasStatusCode().Success()readshas status code in the success range (2xx).
Not nullable results
Expectations that a null subject can never satisfy return the subject as not nullable when awaited in all
packages, as they already did in aweXpect. This covers the Xml, Json and Snapshots expectations, the status code
expectations of aweXpect.Web and the collection expectations of aweXpect.Reflection. You may be able to remove !
operators after await.
Containing on string properties
HasMessage().Containing(…), WithMessage().Containing(…), HasParamName().Containing(…) and the Uri property
checks of aweXpect.Web offer the quantifiers and match types of Contains, e.g.
HasMessage().Containing("retry").AtLeast(2).
Collections
AreEqualToon a struct enumerable such asImmutableArray<T>takes an expected value of the item type and a typed comparer. An expected value of another type no longer compiles.AreEquivalentToafter a quantifier no longer offersUsing(…), which always threw; pass the equivalency options toAreEquivalentToitself.- The items of a collection of strings keep the nullability of the collection, like other items.
Options
- A repeated option replaces the earlier one instead of throwing, e.g.
IgnoringCase().IgnoringCase(false)compares case-sensitively andAtLeast(2).AtLeast(3)requires three. Options that set the same thing in different ways, e.g.AsPrefix().AsSuffix()orAtLeast(2).AtMost(5), still throw. - An option set to
falseis the same as not set, soIgnoringIndentation(false).AsBlock()no longer throws. - A match type that does not support several options already set names all of them, e.g.
IgnoringIndentation and IgnoringNewlineStyle cannot be combined with AsBlock. WithCancellation(…),WithTimeProvider(…)andAllocates(…), which apply to the whole chain of expectations, still throw when they are specified more than once.
Customization
- The collection-valued settings, such as
ExcludedAssemblyPrefixes,ExcludedAttributeTypesand the content processors of aweXpect.Web, areIReadOnlyList<T>instead of arrays. - The excluded assembly prefixes moved to
Customize.aweXpect.Settings().ExcludedAssemblyPrefixes. A prefix ending with a dot, such asMyCompany., now matches in Core as well.
Time provider
WithTimeSystem(…) and ITimeSystem from aweXpect.Core.Internal are replaced by WithTimeProvider(…) in the
namespace aweXpect.Core, which takes a System.TimeProvider, e.g. the FakeTimeProvider of
Microsoft.Extensions.TimeProvider.Testing. On .NET Framework, TimeProvider comes from the
Microsoft.Bcl.TimeProvider package, a new dependency of aweXpect.Core for netstandard2.0. See
on a virtual clock.
New expectations
- Dates gain
HasDayOfWeek(…)onDateTime,DateTimeOffsetandDateOnly. - Dictionaries with string values offer the string match types, e.g.
ContainsValue("prefix*").AsWildcard(). - Exceptions keep their type after
HasInner(Type)andDoesNotHaveInner(Type), so.And.HasParamName(…)follows. - Delegates keep their result type after
Because,WithTimeoutandWithCancellation, so.Because(…).WhoseResultcompiles. - Nested subjects such as
Throws().Which,WhoseResultorHasSingle().WhichofferIs<TType>()and the other type checks.
Extension packages
aweXpect.Json
- The nested expectations of
IsObjectandIsArray(With,Without,At,AString,ANumber,WithElements, …) are removed. Verify a value withHasProperty,DoesNotHaveProperty,HasValueAtorHasElementAt, and its .NET value withIsString,IsNumber,IsNumber<TNumber>orIsBoolean, either against an expected value, e.g.IsString("Let It Be"), or with all expectations of aweXpect. Counts and shapes are covered byIsObject().WhoseProperties,IsArray().WhoseElementsandMatches. IsString()andIsNumber()offer the operators of a property on their result, so that the value is compared in the same sentence, e.g.IsString().StartingWith("Let").IgnoringCase()orIsNumber().EqualTo(4.5).Within(0.1). They returnIsStringResultandIsNumberResultfromaweXpect.Json.Resultsinstead of anAndOrResult, which only affects code that names the result type.IsArray().WhoseElementsandIsObject().WhosePropertieson aJsonNodeprovide the nodes, asJsonNode?andKeyValuePair<string, JsonNode?>items, e.g. forContains(node), instead ofJsonElementandJsonPropertyitems.IsArrayResultandIsObjectResulttake the item type as third type parameter, which only affects code that names the result type.MatchesandIsValidJsonMatchingreport additional array elements. Addo => o.IgnoringAdditionalArrayElements()to allow them.- An expected
stringinMatches,MatchesExactly,IsValidJsonMatchingandIsValidJsonMatchingExactlyis JSON text, like inIsJsonSerializedAs, and invalid JSON throws anArgumentException. A string that is a valid JSON scalar, e.g."42","true"or"null", silently changes its meaning. Pass"\"Let It Be\""or(object)"Let It Be"for a JSON string. JsonOptionsis immutable likeXmlOptionsand configured only through its methods.
aweXpect.Reflection
- Inherited by default. Checks consider inherited attributes, operators, members and relations, and
.Directly()restricts them to what is declared on the item itself or to a direct relation.bool inherit,MemberScope, theforceDirectflag and the…Directly…methods are removed, e.g.InheritsFrom<Base>(true)is nowInheritsFrom<Base>().Directly(). Operator and member checks such asContainsMethodswere declared-only before; append.Directly()to keep that. Listing members (Types().Methods()) stays declared-only;.IncludingInherited()adds the inherited ones and.IncludingObjectMembers()also those ofSystem.Object. AtIndex(i).FromEnd()is nowAtIndexFromEnd(i), as in aweXpect.- The namespace filters are
WhichAreWithinNamespaceandWhichAreNotWithinNamespace. - The member-type filters of properties, fields and events are
WhichAreOfType,WhichAreOfExactType,WhichAreNotOfTypeandWhichAreNotOfExactType(beforeOfType,OfExactType,NotOfTypeandNotOfExactType);OrOfTypeandOrOfExactTypekeep their names.WithoutParameters()is nowWithNoParameters(), like the expectationHasNoParameters(). TypeFilters.GenericTypes.GenericTypesWithArgumentand…WithNamedArgumentare no longer nested inGenericTypesbut declared inTypeFilters, like their counterparts inMethodFilters.- The name filters use the string options of aweXpect, so they chain in any order and offer every option, e.g.
WithName("x").IgnoringCase().AsPrefix(). - Parameters can be filtered by a predicate with
Matching(p => …). - The filter options (
TypeFilterOptions,AttributeFilterOptions<TMember>,ParameterFilterOptions,GenericArgumentsFilterOptionsandGenericArgumentFilterOptions) are internal, and the results and filters no longer offer them throughIOptionsProvider<…>. - The expectations on all items that check a condition on each item, such as
AreSealed(),InheritFrom<T>(),Implement<T>(),HaveOperator(…)orContainMethods(…), use the quantifier wording of aweXpect (all are sealed, but only 2 of 5 were, andnone inherit from Basefor a negation), work with quantifiers (AtLeast(2).InheritFrom<Base>().Directly()), and anullitem fails them and their negation. - The version components (
HasVersion().WithMajor) offer all comparisons of anintproperty, includingBetween(…).And(…). Customize.aweXpect.Reflection()and its settings needusing aweXpect.Reflection;.AccessModifiersmoved fromaweXpect.Reflection.CollectionstoaweXpect.Reflection; replace theusingwhere you name it.IFilter<TEntity>.Appliesreceives theCancellationTokenof the enumeration; add the parameter to your own filters and pass it on to asynchronous work.
aweXpect.Snapshots (from 0.x)
- Rules are written with
.For: select, filter, then act, e.g..For.Members().WithName("Id").Ignore()or.For.Guids().Mask(). They replace.Ignoringand.Masking, andSnapshotRule.Forwith theRulessetting replacesSnapshotIgnoring,SnapshotMaskingand their settings. .Accepting(SnapshotAcceptance.All),NeworNonereplaces.AcceptingChanges(bool);AWEXPECT_ACCEPT_SNAPSHOTStakesnone,neworall.- Snapshot files carry the marker
.awexpect-snapshot.instead of.snapshot., and pending files match*.awexpect-snapshot.pending.*. Rename existing files and update your.gitignore. - Names from
.Named(…)are escaped in file names, so a name with.or%has to be accepted again. - Numbered placeholders are wrapped in braces, e.g.
{Guid_1}, so snapshots with masked values have to be accepted again. byte[],StreamandFileInfosubjects are snapshotted as binary files.MatchesPublicApiSnapshotselects the API withWithinandIgnoringinstead ofExcludingAttributeandExcludingForwardedTypes, and needs no extrausing.MatchesPublicApiSnapshothas one overload per subject with an optionaloptionsparameter instead of one with and one without options. Calls compile unchanged, but have to be compiled again.PublicApiOptionsis immutable: each option returns new options, so a call whose result is discarded has no effect.
aweXpect.Web
HasHeader(…).WithValue(…)andHasQueryParameter(…).WithValue(…)return aStringEqualityTypeResultand offer the match types, e.g.WithValue("application/*").AsWildcard(), like the type, title, detail and instance ofHasProblemDetailsContent. This only affects code that names the result type.HasStatusCode(…),HasStatusCode().EqualTo(…)andHasStatusCode().NotEqualTo(…)take anHttpStatusCodeinstead of anHttpStatusCode?. A response always has a status code, sonullnever matched.HasStatusCode()returns the enum property result of aweXpect.Core,PropertyResult.Enum<…, HttpStatusCode>, instead ofStatusCodeResult, which was removed.Success(),Redirection(),ClientError(),ServerError()andError()are extension methods inHttpStatusCodePropertyResultExtensions. Calls compile unchanged, but code that namesStatusCodeResulthas to use the new type.HasProblemDetailsContent()without a type no longer offers string options or match types, andHasProblemDetailsContent(…)requires a non-null type. A content without atypemember has the type"about:blank"instead of failing.- The expected values of
HasProblemDetailsContent(…)and itsWithTitle,WithStatus,WithDetailandWithInstanceare namedexpectedinstead oftype,title,status,detailandinstance. This only affects calls with named arguments. HasHeaderandDoesNotHaveHeaderalso read the headers of the content, soHasHeader("Content-Type")now succeeds andDoesNotHaveHeader("Content-Type")fails when the content has this header, the same forContent-Length,Content-EncodingorContent-Disposition. UseHasContentTypeto verify the media type.HasHeader(…).WhoseValue(…)andHasQueryParameter(…).WhoseValue(…)fail, also when negated, if the header or query parameter does not have exactly one value, likeWithValue. Before, the expectations on the value receivednull, so that e.g.WhoseValue(value => value.IsNull())succeeded for several values.- A request without content has an empty string content, like a response, so
HasContent("")andHasContent(c => c.IsEmpty())succeed andDoesNotHaveContent("")fails, as HTTP does not distinguish a missing body from an empty one. HasPathkeeps an encoded slash (%2F) in the path, so/artists/AC%2FDCno longer has the path/artists/AC/DC, but/artists/AC%2FDC.
Namespaces
Results of the extension packages are in aweXpect.<Package>.Results, their option types (JsonOptions,
XmlOptions, PublicApiOptions) in aweXpect.<Package>.Options and their other public types in
aweXpect.<Package>. Add the matching using where you name such a type. The option methods, e.g.
o => o.IgnoringArrayOrder(), are in aweXpect and need no extra using.
Extensions
If you wrote your own expectations on aweXpect.Core, the compiler points out most of the changes. The important ones:
- Match types have one lifecycle:
StringMatchTypeandObjectMatchType<TElement>create a matcher per evaluation (StringMatcher,ObjectMatcher<TElement>) that returns aMatchResult. They replaceIStringMatchTypeandIObjectMatchType. See Custom match types. - The operators of property results are extension methods on one interface per property type, such as
IIntPropertyResult<TResult>orIEnumPropertyResult<TResult, TEnum>, so a result of your own gets them by implementing the interface.IDoublePropertyResult<TResult, TToleranceResult>andIDecimalPropertyResult<TResult, TToleranceResult>withPropertyResult.DoubleandPropertyResult.Decimalare new, and theirEqualTooffers a tolerance, e.g.EqualTo(4.5).Within(0.1). Thestringoperators are split overIStringEqualityPropertyResult<TResult>,IStringAffixPropertyResult<TResult>andIStringOccurrencePropertyResult<TResult>, whichPropertyResult.String<TType, TThat, TValue>implements instead of offering them as instance methods. Calls such asHasMessage().EqualTo(…)compile unchanged, but have to be compiled again. - The expectations on quantified items extend the Core interfaces
IEnumerableItems<TItem>,IAsyncEnumerableItems<TItem>,IStructEnumerableItems<TCollection, TItem>andINonGenericEnumerableItems<TCollection>, which replaceIEnumerableElements<TItem>and its siblings. - The options of an expectation are immutable values in one
OptionsHolder<TOptions>, a reference to the current value.IOptionsProvider<TOptions>.Optionsreturns the holder: create it in your expectation, e.g.new OptionsHolder<Quantifier>(Quantifier.Default), pass it to your constraint and your result, and readholder.Valuewhen the constraint evaluates. A constraint that only reads the options can take anIReadOnlyOptionsHolder<TOptions>. See Options. Quantifier,CollectionMatchOptions,CollectionIndexOptions,CollectionOrderOptions<TElement>,NumberTolerance<TNumber>,TimeTolerance,SignalerOptions,RepeatedCheckOptionsandExecutionTimeOptionsarereadonly structs, andStringEqualityOptionsis a sealed immutable class. Start from theirDefault, e.g.StringEqualityOptions.Default, instead of a parameterless constructor, and for a date fromTimeTolerance.Days. A method that sets an option returns a new value, so use the value it returns: a discarded call on a struct, e.g.options.AtLeast(2);, compiles and changes nothing. A repeated option replaces the earlier one, and methods that mirror a user-facing option take its name, e.g.Withininstead ofSetTolerance.Quantifier.Never()is an option method instead of a static factory, e.g.Quantifier.Default.Never().- The option methods are also extension methods on the holder in
aweXpect.Options, which replace the value in the holder, e.g.result.Options.AtLeast(2). ImportaweXpect.Optionswhere you call them: without it, the compiler finds only the user-facing option of the same name and reportsCS0311. ObjectEqualityOptions<TSubject>andObjectEqualityWithToleranceOptions<TSubject, TTolerance>are removed. Object comparisons keep theirObjectMatchType<TElement>in anOptionsHolder<ObjectMatchType<TElement>>, also with a tolerance, for whichObjectMatchType<TElement>.WithTolerance(…)creates anObjectToleranceMatchType<TElement, TTolerance>.ObjectEqualityOptionsExtensionsis renamed toObjectMatchTypeExtensions.- An
ObjectMatchType<TElement>passes the name of its option to its base constructor, andSetMatchType(matchType)reads it, so that the same option again replaces the match type and another one throws. A match type without a name is rejected. IOptionsEquality<TSubject>is renamed toIEqualityEvaluator<TElement>. The newIEqualityOptions<TElement>returns the evaluator for one evaluation withForEvaluation(…), andObjectMatcher<TElement>is such an evaluator.DayToleranceis removed;TimeTolerance.Daysrejects a tolerance that is not a whole number of days.IRelativeToleranceOptions<T>is removed as well:Percent()calls the newSetRelativeTolerance(…)ofIWithinToleranceResult<TResult, TTolerance>, which a result of your own implements: it makes the tolerance relative and returns the result to continue with.- The string options carry no parameter name. The members that can reject the expected value take the name of the
parameter instead:
ValidateExpected(expected, parameterName),MatchWithExplanation(actual, expected, parameterName),CountOccurrences(…, parameterName)andAreConsideredEqual(actual, expected, parameterName)on theStringEqualityOptions, andValidateExpected(expected, parameterName)of aStringMatchType. CollectionMatchOptions.SetEquivalenceRelationis removed; pass the relation to the constructor.ExecutesInResultandExecutesInToleranceResulttake theExpectationBuilderas first constructor argument.ExpectationBuilder.Timeoutonly returns the timeouts added withWithTimeout(…). The limits ofExecutesIn()and ofWithin(…)afterThrows…()are kept apart, so that a later limit replaces an earlier one, and still apply when the expectation is evaluated.- The predicates of a
Signaler<TParameter>moved fromSignalerOptions<TParameter>toSignalerPredicateOptions<TParameter>, whereWithPredicateisAddPredicate, like on theTriggerEventFilter. - The signaler and event recording expectations extend the nullable
IThat<Signaler?>,IThat<Signaler<TParameter>?>andIThat<IEventRecording<TSubject>?>, and their results continue on it, so your own expectations on these subjects extend it as well. ConstraintResultcreates a result that fails both ways withFailureBothWaysinstead ofFail.- A
ConstraintResultnegates itself inprotected override void NegateInPlace()instead ofpublic override ConstraintResult Negate(), so a negation can no longer return a new result that is then ignored. Negate a result with.Invert(), or with the new.InvertIf(isNegated). ExpectationBuilder.WithCancellationandWithTimeoutreturn the builder, and the asyncForWhichisForAsyncWhich.MemberAccessor.FromFuncandFromFuncAsMemberAccessorareFromFuncWithNameandFromFuncWithCallerExpression.- Nested subjects are
IThatSubject<T>, and nested expectations take anAction<IThatSubject<T>>. - Results no longer implement
IOptionsProvider<ExpectationBuilder>; useEquivalencyExpectations.TryGetBuilderforIt.Is…expectations. - The result constructors of aweXpect are internal; the results of
aweXpect.Corestay public as extension points.ObjectEqualityWithToleranceResultandStringCollectionMatchResultmoved to Core. - The quantified collection constraints moved to
aweXpect.Core.Extending, and the occurrence count ofContainsis available there asAddStringOccurrenceConstraint. ReflectionFallbackmoved fromaweXpect.CoretoaweXpect.Core.Extending, and itsNotSupported(what, remedy)is public, so that an extension throws the same error as the built-in expectations when reflection is switched off. aweXpect.Json and aweXpect.Reflection use it too, so their wording of this error changed slightly. The runtime switchaweXpect.ReflectionFallback.IsSupportedand theAweXpectReflectionFallbackproperty keep their names.- The type parameters of results follow one scheme:
TTypefor the subject,TThatfor the subject wrapper,TSelffor the result itself,TResultfor the result a call continues with,TCollectionandTItemfor a collection and its items andTElementfor the value an option compares, e.g.CollectionOrderResult<TCollection, TThat, TElement>. Code that names a result changes where the order or the number of type parameters changed:PropertyResult.Int<TType, TThat, TValue>, its siblings andEnumValueResult<TType, TValue>start with the subject type,HasItemResult,ObjectHasItemResult,ObjectHasItemWithToleranceResult,StringHasItemResultandStringBlockHasItemResulttake aTThatafter the collection, andContainsKeyResultandContainsKeysResulthave noTKey. [GenerateCollectionExpectation]also generates the expectation directly on a collection withCollectionOverloadandNegatedName.ITestFrameworkAdapterhas the new membersTryGetCurrentTest,TryGetCancellationTokenandTryAddAttachment, so an adapter of your own has to implement them; each returnsfalsewhen the test framework does not support it. See the running test.