diff --git a/Docs/pages/02-filters.md b/Docs/pages/02-filters.md index faafb230..d329240f 100644 --- a/Docs/pages/02-filters.md +++ b/Docs/pages/02-filters.md @@ -400,7 +400,7 @@ In addition to [access modifiers](#access-modifiers), | abstract / sealed *(properties only)* | `.WhichAreAbstract()` / `.WhichAreSealed()` | `.IsAbstract()` / `.IsSealed()` | `.AreAbstract()` / `.AreSealed()` | | virtual *(properties only)* | `.WhichAreVirtual()` | `.IsVirtual()` | `.AreVirtual()` | | override *(properties only)* | `.WhichOverride()` | `.Overrides()` | `.Override()` | -| required *(properties only)* | `.WhichAreRequired()` | `.IsRequired()` | `.AreRequired()` | +| required *(properties & fields)* | `.WhichAreRequired()` | `.IsRequired()` | `.AreRequired()` | | readable *(properties only)* | `.WhichAreReadable()` | `.IsReadable()` | `.AreReadable()` | | writable *(properties only)* | `.WhichAreWritable()` | `.IsWritable()` | `.AreWritable()` | | read-only *(properties only)* | `.WhichAreReadOnly()` | `.IsReadOnly()` | `.AreReadOnly()` | diff --git a/Docs/pages/comparison/01-fluentassertions-comparison.mdx b/Docs/pages/comparison/01-fluentassertions-comparison.mdx index 31ab8f1b..c3a9d377 100644 --- a/Docs/pages/comparison/01-fluentassertions-comparison.mdx +++ b/Docs/pages/comparison/01-fluentassertions-comparison.mdx @@ -41,6 +41,8 @@ Legend: ✅ dedicated assertion · ⚠️ only via a general mechanism (selector | **Type**: member presence by signature | ✅ | ⚠️ | | **Type**: operator presence (by kind) | ⚠️ | ✅ | | **Type**: conversion operators by signature | ✅ | ✅ | +| **Type**: immutability / member nullability | ❌ | ✅ | +| **Type**: dependencies (on / only on / outside an allowed set) | ❌ | ✅ | | **Method**: virtual / async / return type | ✅ | ✅ | | **Method**: static / abstract / sealed / generic / extension / operator | ❌ | ✅ | | **Method**: override | ❌ | ✅ | @@ -50,9 +52,10 @@ Legend: ✅ dedicated assertion · ⚠️ only via a general mechanism (selector | **Property**: readable / writable | ✅ | ✅ | | **Property**: virtual | ✅ | ✅ | | **Property**: static / abstract / required / indexer / init-setter / override | ❌ | ✅ | +| **Property**: nullable | ❌ | ✅ | | **Property**: return / declared type | ✅ | ✅ | | **Constructor**: dedicated assertions / filtering | ⚠️ | ✅ | -| **Field**: assertions | ❌ | ✅ | +| **Field**: assertions (static / read-only / constant / required / nullable) | ❌ | ✅ | | **Event**: assertions | ❌ | ✅ | The sections below detail each target with side-by-side examples. @@ -91,7 +94,7 @@ await Expect.That(In.AllLoadedAssemblies()).HaveName("System").AsPrefix(); - **FluentAssertions** (`TypeAssertions`): `Be`/`NotBe`, `BeAssignableTo`, `Implement`/`NotImplement`, `BeDerivedFrom`, `BeAbstract`/`BeSealed`/`BeStatic`, `HaveAccessModifier(CSharpAccessModifier)`, `BeDecoratedWith` (with attribute predicate and `OrInherit` variants), member presence (`HaveProperty`/`HaveMethod`/`HaveConstructor`/`HaveIndexer`/`HaveExplicit*`), and conversion operators. Type *kind* is available only as a selector filter (`ThatAreClasses()`), and namespace only on the selector (`BeInNamespace`); a single type's name/namespace use the string API. -- **aweXpect.Reflection**: type-kind assertions (`IsAClass`, `IsAnInterface`, `IsAnEnum`, `IsAStruct`, `IsARecord`, `IsARecordStruct`, `IsARefStruct`, `IsADelegate`, `IsAnAttribute`, `IsAnException`), `IsAbstract`/`IsSealed`/`IsStatic`/`IsReadOnly`/`IsNested`/`IsGeneric`/`IsInstantiable`, access modifiers (`IsPublic`, …), `InheritsFrom().Directly()`, `HasName`, `HasNamespace`/`IsWithinNamespace`, `Has`, quantified member containment (`ContainsMethods()`, …), operator presence by +- **aweXpect.Reflection**: type-kind assertions (`IsAClass`, `IsAnInterface`, `IsAnEnum`, `IsAStruct`, `IsARecord`, `IsARecordStruct`, `IsARefStruct`, `IsADelegate`, `IsAnAttribute`, `IsAnException`), `IsAbstract`/`IsSealed`/`IsStatic`/`IsReadOnly`/`IsNested`/`IsGeneric`/`IsInstantiable`/`IsImmutable`, access modifiers (`IsPublic`, …), `InheritsFrom().Directly()`, `HasName`, `HasNamespace`/`IsWithinNamespace`, `Has`, quantified member containment (`ContainsMethods()`, …), member nullability (`OnlyHasNullableMembers`/`OnlyHasNonNullableMembers`), type-level dependencies (`DependsOn`/`DoesNotDependOn`/`DependsOnlyOn`/`HasDependenciesOutside`, against namespaces or type selections), operator presence by kind (`HasOperator(Operator)`, `HasOperator(Operator)`) and conversion operators by signature (`HasImplicitConversionOperator`, `HasExplicitConversionOperator`). @@ -169,7 +172,7 @@ await Expect.That(In.AssemblyContaining() ## Property - **FluentAssertions** (`PropertyInfoAssertions`): `BeVirtual`/`NotBeVirtual`, `BeReadable`/`BeWritable` (each with an optional `CSharpAccessModifier`), `NotBeReadable`/`NotBeWritable`, `Return`/`NotReturn`, `BeDecoratedWith`. No assertions for `static`/`abstract`/`sealed`/`required`/indexer/init-setter/override, and no name assertion. -- **aweXpect.Reflection**: `IsReadable`/`IsWritable`/`IsReadOnly`/`IsWriteOnly`/`IsReadWrite`, `HasAGetter`/`HasASetter`/`HasAnInitSetter`, `IsStatic`/`IsAbstract`/`IsSealed`/`IsVirtual`, `IsRequired`, `IsAnIndexer`, `Overrides`, `IsOfType`/`IsOfExactType`, `HasName`, `Has`. +- **aweXpect.Reflection**: `IsReadable`/`IsWritable`/`IsReadOnly`/`IsWriteOnly`/`IsReadWrite`, `HasAGetter`/`HasASetter`/`HasAnInitSetter`, `IsStatic`/`IsAbstract`/`IsSealed`/`IsVirtual`, `IsRequired`, `IsNullable`, `IsAnIndexer`, `IsAnExtensionProperty`, `Overrides`, `IsOfType`/`IsOfExactType`, `HasName`, `Has`. @@ -228,7 +231,7 @@ await Expect.That(typeof(MyClass)).HasADefaultConstructor(); ## Field - **FluentAssertions**: no `FieldInfo` assertions. -- **aweXpect.Reflection**: singular (`ThatField`) and plural (`ThatFields`) assertions and an `In.*…Fields()` filter: `IsStatic`, `IsReadOnly`, `IsConstant`, `IsOfType`/`IsOfExactType`, access modifiers (`IsPublic`, …), `HasName`, `Has`. +- **aweXpect.Reflection**: singular (`ThatField`) and plural (`ThatFields`) assertions and an `In.*…Fields()` filter: `IsStatic`, `IsReadOnly`, `IsConstant`, `IsRequired`, `IsNullable`, `IsOfType`/`IsOfExactType`, access modifiers (`IsPublic`, …), `HasName`, `Has`. diff --git a/Docs/pages/comparison/02-architecture-comparison.mdx b/Docs/pages/comparison/02-architecture-comparison.mdx index 73eedd3f..89c1dbe1 100644 --- a/Docs/pages/comparison/02-architecture-comparison.mdx +++ b/Docs/pages/comparison/02-architecture-comparison.mdx @@ -93,8 +93,8 @@ Legend: ✅ built-in · ⚠️ partial or via a general mechanism · ❌ not ava | Type kinds | ⚠️ classes, interfaces | ✅ classes, interfaces, enums, structs, records | ✅ plus delegates, exceptions, attributes, ref structs, record structs | | Access modifiers | ⚠️ public, nested | ✅ | ✅ | | Attribute rules | ✅ presence | ✅ incl. argument values | ✅ incl. typed predicate on the attribute instance | -| Member-level rules (methods, properties, fields) | ❌ | ✅ | ✅ plus events, constructors, parameter modifiers, async, extension methods, operators | -| Immutability / nullability rules | ✅ | ⚠️ immutability | ✅ | +| Member-level rules (methods, properties, fields) | ❌ | ✅ | ✅ plus events, constructors, parameter modifiers, async, extension methods, operators, required / nullable members | +| Immutability / nullability rules | ✅ | ⚠️ immutability | ✅ type- and member-level | | Dependency rules (on / not on / only on) | ✅ | ✅ | ✅ | | Body-level (IL) dependency detection | ✅ | ✅ | ⚠️ signature-level default, IL via [custom resolver](../04-configuration.md#dependency-resolver) | | Dependency cycle detection | ❌ | ✅ via slices | ✅ via namespaces / [slice roots](../03-architecture-rules.md#dependency-cycles) | diff --git a/Source/aweXpect.Reflection/Filters/FieldFilters.WhichAreRequired.cs b/Source/aweXpect.Reflection/Filters/FieldFilters.WhichAreRequired.cs new file mode 100644 index 00000000..3f345eff --- /dev/null +++ b/Source/aweXpect.Reflection/Filters/FieldFilters.WhichAreRequired.cs @@ -0,0 +1,24 @@ +using System.Reflection; +using aweXpect.Reflection.Collections; +using aweXpect.Reflection.Helpers; + +namespace aweXpect.Reflection; + +public static partial class FieldFilters +{ + /// + /// Filters for fields that are required. + /// + public static Filtered.Fields WhichAreRequired(this Filtered.Fields @this) + => @this.Which(Filter.Prefix( + field => field.IsRequired(), + "required ")); + + /// + /// Filters for fields that are not required. + /// + public static Filtered.Fields WhichAreNotRequired(this Filtered.Fields @this) + => @this.Which(Filter.Prefix( + field => !field.IsRequired(), + "non-required ")); +} diff --git a/Source/aweXpect.Reflection/Helpers/FieldInfoHelpers.cs b/Source/aweXpect.Reflection/Helpers/FieldInfoHelpers.cs index df136076..6c9c2199 100644 --- a/Source/aweXpect.Reflection/Helpers/FieldInfoHelpers.cs +++ b/Source/aweXpect.Reflection/Helpers/FieldInfoHelpers.cs @@ -128,4 +128,15 @@ public static bool HasAttribute( return false; } + + /// + /// Checks if the is required (marked with the required modifier). + /// + /// + /// A field is considered required if it carries the + /// System.Runtime.CompilerServices.RequiredMemberAttribute. + /// + public static bool IsRequired(this FieldInfo? fieldInfo) + => fieldInfo != null && fieldInfo.GetCustomAttributes(true) + .Any(attribute => attribute.GetType().FullName == "System.Runtime.CompilerServices.RequiredMemberAttribute"); } diff --git a/Source/aweXpect.Reflection/ThatField.IsRequired.cs b/Source/aweXpect.Reflection/ThatField.IsRequired.cs new file mode 100644 index 00000000..2b2c96f5 --- /dev/null +++ b/Source/aweXpect.Reflection/ThatField.IsRequired.cs @@ -0,0 +1,59 @@ +using System.Reflection; +using System.Text; +using aweXpect.Core; +using aweXpect.Core.Constraints; +using aweXpect.Reflection.Helpers; +using aweXpect.Results; + +namespace aweXpect.Reflection; + +public static partial class ThatField +{ + /// + /// Verifies that the is required. + /// + public static AndOrResult> IsRequired( + this IThat subject) + => new(subject.Get().ExpectationBuilder.AddConstraint((it, grammars) + => new IsRequiredConstraint(it, grammars)), + subject); + + /// + /// Verifies that the is not required. + /// + public static AndOrResult> IsNotRequired( + this IThat subject) + => new(subject.Get().ExpectationBuilder.AddConstraint((it, grammars) + => new IsRequiredConstraint(it, grammars).Invert()), + subject); + + private sealed class IsRequiredConstraint(string it, ExpectationGrammars grammars) + : ConstraintResult.WithNotNullValue(it, grammars), + IValueConstraint + { + public ConstraintResult IsMetBy(FieldInfo? actual) + { + Actual = actual; + Outcome = actual.IsRequired() ? Outcome.Success : Outcome.Failure; + return this; + } + + protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("is required"); + + protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append(It).Append(" was non-required "); + Formatter.Format(stringBuilder, Actual); + } + + protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("is not required"); + + protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append(It).Append(" was required "); + Formatter.Format(stringBuilder, Actual); + } + } +} diff --git a/Source/aweXpect.Reflection/ThatFields.AreRequired.cs b/Source/aweXpect.Reflection/ThatFields.AreRequired.cs new file mode 100644 index 00000000..a89a017b --- /dev/null +++ b/Source/aweXpect.Reflection/ThatFields.AreRequired.cs @@ -0,0 +1,128 @@ +using System.Collections.Generic; +using System.Reflection; +using System.Text; +using aweXpect.Core; +using aweXpect.Core.Constraints; +using aweXpect.Reflection.Helpers; +using aweXpect.Results; +#if NET8_0_OR_GREATER +using System.Threading; +using System.Threading.Tasks; +#endif + +// ReSharper disable PossibleMultipleEnumeration + +namespace aweXpect.Reflection; + +public static partial class ThatFields +{ + /// + /// Verifies that all items in the filtered collection of are required. + /// + public static AndOrResult, IThat>> AreRequired( + this IThat> subject) + => new(subject.Get().ExpectationBuilder.AddConstraint>((it, grammars) + => new AreRequiredConstraint(it, grammars)), + subject); + +#if NET8_0_OR_GREATER + /// + /// Verifies that all items in the filtered collection of are required. + /// + public static AndOrResult, IThat>> AreRequired( + this IThat> subject) + => new(subject.Get().ExpectationBuilder.AddConstraint>((it, grammars) + => new AreRequiredConstraint(it, grammars)), + subject); +#endif + + /// + /// Verifies that all items in the filtered collection of are not required. + /// + public static AndOrResult, IThat>> AreNotRequired( + this IThat> subject) + => new(subject.Get().ExpectationBuilder.AddConstraint>((it, grammars) + => new AreNotRequiredConstraint(it, grammars)), + subject); + +#if NET8_0_OR_GREATER + /// + /// Verifies that all items in the filtered collection of are not required. + /// + public static AndOrResult, IThat>> AreNotRequired( + this IThat> subject) + => new(subject.Get().ExpectationBuilder.AddConstraint>((it, grammars) + => new AreNotRequiredConstraint(it, grammars)), + subject); +#endif + + private sealed class AreRequiredConstraint(string it, ExpectationGrammars grammars) + : CollectionConstraintResult(grammars), + IValueConstraint> +#if NET8_0_OR_GREATER + , IAsyncConstraint> +#endif + { +#if NET8_0_OR_GREATER + public async Task IsMetBy(IAsyncEnumerable actual, + CancellationToken cancellationToken) + => await SetAsyncValue(actual, field => field.IsRequired()); +#endif + + public ConstraintResult IsMetBy(IEnumerable actual) + => SetValue(actual, field => field.IsRequired()); + + protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("are all required"); + + protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append(it).Append(" contained non-required fields "); + Formatter.Format(stringBuilder, NotMatching, FormattingOptions.Indented(indentation)); + } + + protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("are not all required"); + + protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append(it).Append(" only contained required fields "); + Formatter.Format(stringBuilder, Matching, FormattingOptions.Indented(indentation)); + } + } + + private sealed class AreNotRequiredConstraint(string it, ExpectationGrammars grammars) + : CollectionConstraintResult(grammars), + IValueConstraint> +#if NET8_0_OR_GREATER + , IAsyncConstraint> +#endif + { +#if NET8_0_OR_GREATER + public async Task IsMetBy(IAsyncEnumerable actual, + CancellationToken cancellationToken) + => await SetAsyncValue(actual, field => !field.IsRequired()); +#endif + + public ConstraintResult IsMetBy(IEnumerable actual) + => SetValue(actual, field => !field.IsRequired()); + + protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("are all not required"); + + protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append(it).Append(" contained required fields "); + Formatter.Format(stringBuilder, NotMatching, FormattingOptions.Indented(indentation)); + } + + protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("also contain a required field"); + + protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append(it).Append(" only contained non-required fields "); + Formatter.Format(stringBuilder, Matching, FormattingOptions.Indented(indentation)); + } + } +} diff --git a/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_net10.0.txt b/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_net10.0.txt index 6f2b07e5..8d724a85 100644 --- a/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_net10.0.txt +++ b/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_net10.0.txt @@ -288,6 +288,7 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotProtectedInternal(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotPublic(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotReadOnly(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } + public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotRequired(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotStatic(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNullable(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreObsolete(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } @@ -297,6 +298,7 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreProtectedInternal(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichArePublic(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreReadOnly(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } + public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreRequired(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreStatic(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.FieldFilters.FieldsWith With(this aweXpect.Reflection.Collections.Filtered.Fields @this) where TAttribute : System.Attribute { } @@ -1040,6 +1042,7 @@ namespace aweXpect.Reflection public static aweXpect.Results.AndOrResult> IsNotConstant(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotNullable(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotReadOnly(this aweXpect.Core.IThat subject) { } + public static aweXpect.Results.AndOrResult> IsNotRequired(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotStatic(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNullable(this aweXpect.Core.IThat subject) { } public static aweXpect.Reflection.ThatField.FieldOfTypeResult> IsOfExactType(this aweXpect.Core.IThat subject, System.Type fieldType) { } @@ -1047,6 +1050,7 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.ThatField.FieldOfTypeResult> IsOfType(this aweXpect.Core.IThat subject, System.Type fieldType) { } public static aweXpect.Reflection.ThatField.FieldOfTypeResult> IsOfType(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsReadOnly(this aweXpect.Core.IThat subject) { } + public static aweXpect.Results.AndOrResult> IsRequired(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsStatic(this aweXpect.Core.IThat subject) { } public sealed class FieldOfTypeResult : aweXpect.Results.AndOrResult, aweXpect.Core.IOptionsProvider where TResult : aweXpect.Core.IThat @@ -1068,6 +1072,8 @@ namespace aweXpect.Reflection public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotNullable(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotReadOnly(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotReadOnly(this aweXpect.Core.IThat> subject) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotRequired(this aweXpect.Core.IThat> subject) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotRequired(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotStatic(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotStatic(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNullable(this aweXpect.Core.IThat> subject) { } @@ -1082,6 +1088,8 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.ThatFields.FieldsOfTypeResult, aweXpect.Core.IThat>> AreOfType(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreReadOnly(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreReadOnly(this aweXpect.Core.IThat> subject) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreRequired(this aweXpect.Core.IThat> subject) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreRequired(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreStatic(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreStatic(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> DoNotHave(this aweXpect.Core.IThat> subject) diff --git a/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_net8.0.txt b/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_net8.0.txt index 52ae5565..807459c2 100644 --- a/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_net8.0.txt +++ b/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_net8.0.txt @@ -288,6 +288,7 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotProtectedInternal(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotPublic(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotReadOnly(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } + public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotRequired(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotStatic(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNullable(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreObsolete(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } @@ -297,6 +298,7 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreProtectedInternal(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichArePublic(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreReadOnly(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } + public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreRequired(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreStatic(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.FieldFilters.FieldsWith With(this aweXpect.Reflection.Collections.Filtered.Fields @this) where TAttribute : System.Attribute { } @@ -1040,6 +1042,7 @@ namespace aweXpect.Reflection public static aweXpect.Results.AndOrResult> IsNotConstant(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotNullable(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotReadOnly(this aweXpect.Core.IThat subject) { } + public static aweXpect.Results.AndOrResult> IsNotRequired(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotStatic(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNullable(this aweXpect.Core.IThat subject) { } public static aweXpect.Reflection.ThatField.FieldOfTypeResult> IsOfExactType(this aweXpect.Core.IThat subject, System.Type fieldType) { } @@ -1047,6 +1050,7 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.ThatField.FieldOfTypeResult> IsOfType(this aweXpect.Core.IThat subject, System.Type fieldType) { } public static aweXpect.Reflection.ThatField.FieldOfTypeResult> IsOfType(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsReadOnly(this aweXpect.Core.IThat subject) { } + public static aweXpect.Results.AndOrResult> IsRequired(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsStatic(this aweXpect.Core.IThat subject) { } public sealed class FieldOfTypeResult : aweXpect.Results.AndOrResult, aweXpect.Core.IOptionsProvider where TResult : aweXpect.Core.IThat @@ -1068,6 +1072,8 @@ namespace aweXpect.Reflection public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotNullable(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotReadOnly(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotReadOnly(this aweXpect.Core.IThat> subject) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotRequired(this aweXpect.Core.IThat> subject) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotRequired(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotStatic(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotStatic(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNullable(this aweXpect.Core.IThat> subject) { } @@ -1082,6 +1088,8 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.ThatFields.FieldsOfTypeResult, aweXpect.Core.IThat>> AreOfType(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreReadOnly(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreReadOnly(this aweXpect.Core.IThat> subject) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreRequired(this aweXpect.Core.IThat> subject) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreRequired(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreStatic(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreStatic(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> DoNotHave(this aweXpect.Core.IThat> subject) diff --git a/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_netstandard2.0.txt b/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_netstandard2.0.txt index bd112053..35f1fe56 100644 --- a/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_netstandard2.0.txt +++ b/Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_netstandard2.0.txt @@ -288,6 +288,7 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotProtectedInternal(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotPublic(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotReadOnly(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } + public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotRequired(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNotStatic(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreNullable(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreObsolete(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } @@ -297,6 +298,7 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreProtectedInternal(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichArePublic(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreReadOnly(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } + public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreRequired(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.Collections.Filtered.Fields WhichAreStatic(this aweXpect.Reflection.Collections.Filtered.Fields @this) { } public static aweXpect.Reflection.FieldFilters.FieldsWith With(this aweXpect.Reflection.Collections.Filtered.Fields @this) where TAttribute : System.Attribute { } @@ -934,6 +936,7 @@ namespace aweXpect.Reflection public static aweXpect.Results.AndOrResult> IsNotConstant(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotNullable(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotReadOnly(this aweXpect.Core.IThat subject) { } + public static aweXpect.Results.AndOrResult> IsNotRequired(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNotStatic(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsNullable(this aweXpect.Core.IThat subject) { } public static aweXpect.Reflection.ThatField.FieldOfTypeResult> IsOfExactType(this aweXpect.Core.IThat subject, System.Type fieldType) { } @@ -941,6 +944,7 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.ThatField.FieldOfTypeResult> IsOfType(this aweXpect.Core.IThat subject, System.Type fieldType) { } public static aweXpect.Reflection.ThatField.FieldOfTypeResult> IsOfType(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsReadOnly(this aweXpect.Core.IThat subject) { } + public static aweXpect.Results.AndOrResult> IsRequired(this aweXpect.Core.IThat subject) { } public static aweXpect.Results.AndOrResult> IsStatic(this aweXpect.Core.IThat subject) { } public sealed class FieldOfTypeResult : aweXpect.Results.AndOrResult, aweXpect.Core.IOptionsProvider where TResult : aweXpect.Core.IThat @@ -958,6 +962,7 @@ namespace aweXpect.Reflection public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotConstant(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotNullable(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotReadOnly(this aweXpect.Core.IThat> subject) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotRequired(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNotStatic(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreNullable(this aweXpect.Core.IThat> subject) { } public static aweXpect.Reflection.ThatFields.FieldsOfTypeResult, aweXpect.Core.IThat>> AreOfExactType(this aweXpect.Core.IThat> subject, System.Type fieldType) { } @@ -965,6 +970,7 @@ namespace aweXpect.Reflection public static aweXpect.Reflection.ThatFields.FieldsOfTypeResult, aweXpect.Core.IThat>> AreOfType(this aweXpect.Core.IThat> subject, System.Type fieldType) { } public static aweXpect.Reflection.ThatFields.FieldsOfTypeResult, aweXpect.Core.IThat>> AreOfType(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreReadOnly(this aweXpect.Core.IThat> subject) { } + public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreRequired(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> AreStatic(this aweXpect.Core.IThat> subject) { } public static aweXpect.Results.AndOrResult, aweXpect.Core.IThat>> DoNotHave(this aweXpect.Core.IThat> subject) where TAttribute : System.Attribute { } diff --git a/Tests/aweXpect.Reflection.Tests/Filters/FieldFilters.WhichAreNotRequired.Tests.cs b/Tests/aweXpect.Reflection.Tests/Filters/FieldFilters.WhichAreNotRequired.Tests.cs new file mode 100644 index 00000000..75028cb8 --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/Filters/FieldFilters.WhichAreNotRequired.Tests.cs @@ -0,0 +1,24 @@ +using aweXpect.Reflection.Collections; +using aweXpect.Reflection.Tests.TestHelpers; + +namespace aweXpect.Reflection.Tests.Filters; + +public sealed partial class FieldFilters +{ + public sealed class WhichAreNotRequired + { + public sealed class Tests + { + [Fact] + public async Task ShouldAllowFilteringForNonRequiredFields() + { + Filtered.Fields fields = In.AssemblyContaining() + .Fields().WhichAreNotRequired(); + + await That(fields).All().Satisfy(x => !x.IsRequired()).And.IsNotEmpty(); + await That(fields.GetDescription()) + .IsEqualTo("non-required fields in assembly").AsPrefix(); + } + } + } +} diff --git a/Tests/aweXpect.Reflection.Tests/Filters/FieldFilters.WhichAreRequired.Tests.cs b/Tests/aweXpect.Reflection.Tests/Filters/FieldFilters.WhichAreRequired.Tests.cs new file mode 100644 index 00000000..29c161de --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/Filters/FieldFilters.WhichAreRequired.Tests.cs @@ -0,0 +1,24 @@ +using aweXpect.Reflection.Collections; +using aweXpect.Reflection.Tests.TestHelpers; + +namespace aweXpect.Reflection.Tests.Filters; + +public sealed partial class FieldFilters +{ + public sealed class WhichAreRequired + { + public sealed class Tests + { + [Fact] + public async Task ShouldAllowFilteringForRequiredFields() + { + Filtered.Fields fields = In.AssemblyContaining() + .Fields().WhichAreRequired(); + + await That(fields).All().Satisfy(x => x.IsRequired()).And.IsNotEmpty(); + await That(fields.GetDescription()) + .IsEqualTo("required fields in assembly").AsPrefix(); + } + } + } +} diff --git a/Tests/aweXpect.Reflection.Tests/TestHelpers/FieldInfoExtensions.cs b/Tests/aweXpect.Reflection.Tests/TestHelpers/FieldInfoExtensions.cs new file mode 100644 index 00000000..aa9d95b7 --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/TestHelpers/FieldInfoExtensions.cs @@ -0,0 +1,15 @@ +using System.Linq; +using System.Reflection; + +namespace aweXpect.Reflection.Tests.TestHelpers; + +public static class FieldInfoExtensions +{ + /// + /// Checks if the is required (marked with the required modifier). + /// + /// The to check. + public static bool IsRequired(this FieldInfo? fieldInfo) + => fieldInfo != null && fieldInfo.GetCustomAttributes(true) + .Any(attribute => attribute.GetType().FullName == "System.Runtime.CompilerServices.RequiredMemberAttribute"); +} diff --git a/Tests/aweXpect.Reflection.Tests/TestHelpers/Types/ClassWithRequiredMembers.cs b/Tests/aweXpect.Reflection.Tests/TestHelpers/Types/ClassWithRequiredMembers.cs index 36719c14..af22551a 100644 --- a/Tests/aweXpect.Reflection.Tests/TestHelpers/Types/ClassWithRequiredMembers.cs +++ b/Tests/aweXpect.Reflection.Tests/TestHelpers/Types/ClassWithRequiredMembers.cs @@ -2,6 +2,12 @@ public class ClassWithRequiredMembers { + // ReSharper disable once UnusedMember.Global + public string OptionalField = ""; + + // ReSharper disable once UnusedMember.Global + public required string RequiredField; + // ReSharper disable once UnusedMember.Global public required string RequiredProperty { get; set; } @@ -11,6 +17,12 @@ public class ClassWithRequiredMembers public class ClassWithOnlyRequiredMembers { + // ReSharper disable once UnusedMember.Global + public required string FirstRequiredField; + + // ReSharper disable once UnusedMember.Global + public required string SecondRequiredField; + // ReSharper disable once UnusedMember.Global public required string FirstRequiredProperty { get; set; } diff --git a/Tests/aweXpect.Reflection.Tests/ThatField.IsNotRequired.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatField.IsNotRequired.Tests.cs new file mode 100644 index 00000000..869a5554 --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/ThatField.IsNotRequired.Tests.cs @@ -0,0 +1,101 @@ +using System.Reflection; +using aweXpect.Reflection.Tests.TestHelpers.Types; +using Xunit.Sdk; + +namespace aweXpect.Reflection.Tests; + +public sealed partial class ThatField +{ + public sealed class IsNotRequired + { + public sealed class Tests + { + [Fact] + public async Task WhenFieldIsNotRequired_ShouldSucceed() + { + FieldInfo subject = + typeof(ClassWithRequiredMembers).GetField(nameof(ClassWithRequiredMembers.OptionalField))!; + + async Task Act() + { + await That(subject).IsNotRequired(); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenFieldIsNull_ShouldFail() + { + FieldInfo? subject = null; + + async Task Act() + { + await That(subject).IsNotRequired(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + is not required, + but it was + """); + } + + [Fact] + public async Task WhenFieldIsRequired_ShouldFail() + { + FieldInfo subject = + typeof(ClassWithRequiredMembers).GetField(nameof(ClassWithRequiredMembers.RequiredField))!; + + async Task Act() + { + await That(subject).IsNotRequired(); + } + + await That(Act).ThrowsException() + .WithMessage($""" + Expected that subject + is not required, + but it was required {Formatter.Format(subject)} + """); + } + } + + public sealed class NegatedTests + { + [Fact] + public async Task WhenFieldIsNotRequired_ShouldFail() + { + FieldInfo subject = + typeof(ClassWithRequiredMembers).GetField(nameof(ClassWithRequiredMembers.OptionalField))!; + + async Task Act() + { + await That(subject).DoesNotComplyWith(it => it.IsNotRequired()); + } + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + is required, + but it was non-required {Formatter.Format(subject)} + """); + } + + [Fact] + public async Task WhenFieldIsRequired_ShouldSucceed() + { + FieldInfo subject = + typeof(ClassWithRequiredMembers).GetField(nameof(ClassWithRequiredMembers.RequiredField))!; + + async Task Act() + { + await That(subject).DoesNotComplyWith(it => it.IsNotRequired()); + } + + await That(Act).DoesNotThrow(); + } + } + } +} diff --git a/Tests/aweXpect.Reflection.Tests/ThatField.IsRequired.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatField.IsRequired.Tests.cs new file mode 100644 index 00000000..cafc6225 --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/ThatField.IsRequired.Tests.cs @@ -0,0 +1,101 @@ +using System.Reflection; +using aweXpect.Reflection.Tests.TestHelpers.Types; +using Xunit.Sdk; + +namespace aweXpect.Reflection.Tests; + +public sealed partial class ThatField +{ + public sealed class IsRequired + { + public sealed class Tests + { + [Fact] + public async Task WhenFieldIsNotRequired_ShouldFail() + { + FieldInfo subject = + typeof(ClassWithRequiredMembers).GetField(nameof(ClassWithRequiredMembers.OptionalField))!; + + async Task Act() + { + await That(subject).IsRequired(); + } + + await That(Act).ThrowsException() + .WithMessage($""" + Expected that subject + is required, + but it was non-required {Formatter.Format(subject)} + """); + } + + [Fact] + public async Task WhenFieldIsNull_ShouldFail() + { + FieldInfo? subject = null; + + async Task Act() + { + await That(subject).IsRequired(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + is required, + but it was + """); + } + + [Fact] + public async Task WhenFieldIsRequired_ShouldSucceed() + { + FieldInfo subject = + typeof(ClassWithRequiredMembers).GetField(nameof(ClassWithRequiredMembers.RequiredField))!; + + async Task Act() + { + await That(subject).IsRequired(); + } + + await That(Act).DoesNotThrow(); + } + } + + public sealed class NegatedTests + { + [Fact] + public async Task WhenFieldIsNotRequired_ShouldSucceed() + { + FieldInfo subject = + typeof(ClassWithRequiredMembers).GetField(nameof(ClassWithRequiredMembers.OptionalField))!; + + async Task Act() + { + await That(subject).DoesNotComplyWith(it => it.IsRequired()); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenFieldIsRequired_ShouldFail() + { + FieldInfo subject = + typeof(ClassWithRequiredMembers).GetField(nameof(ClassWithRequiredMembers.RequiredField))!; + + async Task Act() + { + await That(subject).DoesNotComplyWith(it => it.IsRequired()); + } + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + is not required, + but it was required {Formatter.Format(subject)} + """); + } + } + } +} diff --git a/Tests/aweXpect.Reflection.Tests/ThatFields.AreNotRequired.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatFields.AreNotRequired.Tests.cs new file mode 100644 index 00000000..b6dbffe1 --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/ThatFields.AreNotRequired.Tests.cs @@ -0,0 +1,132 @@ +using System.Collections.Generic; +using System.Reflection; +using aweXpect.Reflection.Tests.TestHelpers.Types; +#if NET8_0_OR_GREATER +using aweXpect.Reflection.Tests.TestHelpers; +#endif + +namespace aweXpect.Reflection.Tests; + +public sealed partial class ThatFields +{ + public sealed class AreNotRequired + { + public sealed class Tests + { + [Fact] + public async Task WhenFieldsContainRequiredFields_ShouldFail() + { + IEnumerable subject = typeof(ClassWithOnlyRequiredMembers) + .GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + + async Task Act() + { + await That(subject).AreNotRequired(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + are all not required, + but it contained required fields [ + * + ] + """).AsWildcard(); + } + + [Fact] + public async Task WhenFilteringOnlyNonRequiredFields_ShouldSucceed() + { + IEnumerable subject = typeof(BaseClassWithMembers) + .GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + + async Task Act() + { + await That(subject).AreNotRequired(); + } + + await That(Act).DoesNotThrow(); + } + } + + public sealed class NegatedTests + { + [Fact] + public async Task WhenFieldsContainRequiredFields_ShouldSucceed() + { + IEnumerable subject = typeof(ClassWithOnlyRequiredMembers) + .GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + + async Task Act() + { + await That(subject).DoesNotComplyWith(they => they.AreNotRequired()); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenFilteringOnlyNonRequiredFields_ShouldFail() + { + IEnumerable subject = typeof(BaseClassWithMembers) + .GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + + async Task Act() + { + await That(subject).DoesNotComplyWith(they => they.AreNotRequired()); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + also contain a required field, + but it only contained non-required fields [ + * + ] + """).AsWildcard(); + } + } + +#if NET8_0_OR_GREATER + public sealed class AsyncEnumerableTests + { + [Fact] + public async Task WhenFieldsContainRequiredFields_ShouldFail() + { + IAsyncEnumerable subject = typeof(ClassWithOnlyRequiredMembers) + .GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) + .ToTestAsyncEnumerable(); + + async Task Act() + { + await That(subject).AreNotRequired(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + are all not required, + but it contained required fields [ + * + ] + """).AsWildcard(); + } + + [Fact] + public async Task WhenFilteringOnlyNonRequiredFields_ShouldSucceed() + { + IAsyncEnumerable subject = typeof(BaseClassWithMembers) + .GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) + .ToTestAsyncEnumerable(); + + async Task Act() + { + await That(subject).AreNotRequired(); + } + + await That(Act).DoesNotThrow(); + } + } +#endif + } +} diff --git a/Tests/aweXpect.Reflection.Tests/ThatFields.AreRequired.Tests.cs b/Tests/aweXpect.Reflection.Tests/ThatFields.AreRequired.Tests.cs new file mode 100644 index 00000000..53f2fb56 --- /dev/null +++ b/Tests/aweXpect.Reflection.Tests/ThatFields.AreRequired.Tests.cs @@ -0,0 +1,132 @@ +using System.Collections.Generic; +using System.Reflection; +using aweXpect.Reflection.Tests.TestHelpers.Types; +#if NET8_0_OR_GREATER +using aweXpect.Reflection.Tests.TestHelpers; +#endif + +namespace aweXpect.Reflection.Tests; + +public sealed partial class ThatFields +{ + public sealed class AreRequired + { + public sealed class Tests + { + [Fact] + public async Task WhenFieldsContainNonRequiredFields_ShouldFail() + { + IEnumerable subject = typeof(ClassWithRequiredMembers) + .GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + + async Task Act() + { + await That(subject).AreRequired(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + are all required, + but it contained non-required fields [ + * + ] + """).AsWildcard(); + } + + [Fact] + public async Task WhenFilteringOnlyRequiredFields_ShouldSucceed() + { + IEnumerable subject = typeof(ClassWithOnlyRequiredMembers) + .GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + + async Task Act() + { + await That(subject).AreRequired(); + } + + await That(Act).DoesNotThrow(); + } + } + + public sealed class NegatedTests + { + [Fact] + public async Task WhenFieldsContainNonRequiredFields_ShouldSucceed() + { + IEnumerable subject = typeof(ClassWithRequiredMembers) + .GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + + async Task Act() + { + await That(subject).DoesNotComplyWith(they => they.AreRequired()); + } + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenFilteringOnlyRequiredFields_ShouldFail() + { + IEnumerable subject = typeof(ClassWithOnlyRequiredMembers) + .GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + + async Task Act() + { + await That(subject).DoesNotComplyWith(they => they.AreRequired()); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + are not all required, + but it only contained required fields [ + * + ] + """).AsWildcard(); + } + } + +#if NET8_0_OR_GREATER + public sealed class AsyncEnumerableTests + { + [Fact] + public async Task WhenFieldsContainNonRequiredFields_ShouldFail() + { + IAsyncEnumerable subject = typeof(ClassWithRequiredMembers) + .GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) + .ToTestAsyncEnumerable(); + + async Task Act() + { + await That(subject).AreRequired(); + } + + await That(Act).ThrowsException() + .WithMessage(""" + Expected that subject + are all required, + but it contained non-required fields [ + * + ] + """).AsWildcard(); + } + + [Fact] + public async Task WhenFilteringOnlyRequiredFields_ShouldSucceed() + { + IAsyncEnumerable subject = typeof(ClassWithOnlyRequiredMembers) + .GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) + .ToTestAsyncEnumerable(); + + async Task Act() + { + await That(subject).AreRequired(); + } + + await That(Act).DoesNotThrow(); + } + } +#endif + } +}