Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce attributes to refine customizations #71

Merged
merged 61 commits into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from 51 commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
9be29d7
Initial work on FromRange attribute
piotrzajac Oct 17, 2023
3516348
Merge branch 'master' into narrow-customisations
piotrzajac Oct 18, 2023
ab78b83
Merge branch 'master' into narrow-customisations
piotrzajac Oct 31, 2023
107ac2f
Merge branch 'master' into narrow-customisations
piotrzajac Nov 3, 2023
02a9e09
Support more types and cover it with tests
piotrzajac Nov 3, 2023
9ad689a
Combine with MemberAutoData
piotrzajac Nov 3, 2023
47a498e
Remove constructor with types not allowed as attribute arguments.
piotrzajac Nov 3, 2023
8f2aeb1
Bring back some of the constructors
piotrzajac Nov 3, 2023
495a330
Improve design and test coverage
piotrzajac Nov 3, 2023
96cea93
Composition over inheritance
piotrzajac Nov 7, 2023
f6a6f2b
Fix syntax issue
piotrzajac Nov 7, 2023
4c7bf29
Stop exposing internal structures
piotrzajac Nov 7, 2023
5a62aca
Introduce FromValues attribute
piotrzajac Nov 7, 2023
71cf2dc
Further improvments
piotrzajac Nov 8, 2023
e9bad3d
Merge branch 'master' into narrow-customisations
piotrzajac Nov 13, 2023
283556d
Simplify design
piotrzajac Nov 15, 2023
a388600
Validate same values generated when range has a single value.
piotrzajac Nov 15, 2023
3d17e8a
Exception when context is null
piotrzajac Nov 15, 2023
4434ede
Encapsulate logic in enumerable extensions
piotrzajac Nov 15, 2023
6d45cbd
Desing improvments
piotrzajac Nov 16, 2023
213a7c7
Simplify test values
piotrzajac Nov 16, 2023
e52c4c8
Extract RoundRobinEnumerable to separate file and test it.
piotrzajac Nov 16, 2023
86b7973
Ensure comparable values
piotrzajac Nov 16, 2023
197317f
Improve coverage
piotrzajac Nov 16, 2023
337950d
Merge branch 'master' into narrow-customisations
piotrzajac Nov 20, 2023
8ec6bbb
Simplify design and improve testability of the RandomFixedValuesGener…
piotrzajac Nov 20, 2023
a36edc6
Improve FixedValuesRequest testability
piotrzajac Nov 20, 2023
e758138
Disable CA1859: Use concrete types when possible for improved perform…
piotrzajac Nov 20, 2023
d7e5aad
Disable CA1861: Avoid constant arrays as arguments
piotrzajac Nov 20, 2023
545c46f
Improve testability
piotrzajac Nov 20, 2023
625af74
Improve testability and coverage
piotrzajac Nov 20, 2023
b46b5cd
Seal internal classes
piotrzajac Nov 20, 2023
bc45dae
Cover last element of logic
piotrzajac Nov 20, 2023
e8dee05
Single relay for range and values specimen builders
piotrzajac Nov 20, 2023
a670cb4
Introduce Except attribute
piotrzajac Nov 27, 2023
e95889a
Merge branch 'master' into narrow-customisations
piotrzajac Nov 27, 2023
ee5ef35
Ensure CA1859 enabled globally as suggestion
piotrzajac Nov 27, 2023
badf899
Spelling fixes
piotrzajac Nov 27, 2023
8306dfe
Decorate test classes
piotrzajac Nov 27, 2023
1805592
Move common pomponents to same directory
piotrzajac Nov 27, 2023
f87b9bf
Simplify desing
piotrzajac Nov 27, 2023
360967e
Simplify attributes naming
piotrzajac Nov 27, 2023
79adb1c
Ensure NoSpecimen returned when RequestFactory returns no result
piotrzajac Nov 27, 2023
3648f6b
Include typecheck and support strings
piotrzajac Nov 27, 2023
13b04ad
Test agains ExceptAttribute not ValuesAttribute
piotrzajac Nov 28, 2023
35c0f2e
Remove type check - What You Put In Is What You Get Out.
piotrzajac Nov 28, 2023
5736281
Ensure values uniqueness and allow nullable
piotrzajac Nov 28, 2023
3a6f84a
Move namespace to using statement
piotrzajac Nov 28, 2023
c5aec3a
Fix spelling
piotrzajac Nov 28, 2023
dd34f9a
Introduce data filtering attributes
piotrzajac Nov 28, 2023
9963f65
Fix spelling
piotrzajac Nov 28, 2023
5e8bbf1
Use Assert.Equivalent to simplify check for Values driven collection …
piotrzajac Dec 4, 2023
bd1599c
Order
piotrzajac Dec 4, 2023
de20bdc
Remove redundant test case
piotrzajac Dec 4, 2023
078bd78
Use Assert.Equivalent to simplify check for Values driven collection
piotrzajac Dec 4, 2023
81c3d4d
Rename attributes to PickFromRange and PickFromValues
piotrzajac Dec 4, 2023
c6c5c33
Improve exception message
piotrzajac Dec 4, 2023
1af9618
Improve ToString performance
piotrzajac Dec 4, 2023
3384ab4
Replace ConcurrentDictionary with HashSet
piotrzajac Dec 4, 2023
8c4f38a
Align text categories
piotrzajac Dec 5, 2023
e416417
Align attributes order
piotrzajac Dec 5, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 62 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ An attribute that can be applied to parameters in an `AutoDataAttribute`-driven

This attribute allows to disable the generation of members marked as `virtual` on a decorated type wheres `IgnoreVirtualMembers` arguments of mocking attributes mentioned above disable such a generation for all types created by `IFixture`.

**Caution:** Order is important! Applying `IgnoreVirtualMembers` attribute to the subsequent paramater makes precedig parameters of the same type to have `virtual` properties populated and the particular parameter with the following ones of the same type to have `virtual` properties unpopulated.
**Caution:** Order is important! Applying `IgnoreVirtualMembers` attribute to the subsequent parameter makes preceding parameters of the same type to have `virtual` properties populated and the particular parameter with the following ones of the same type to have `virtual` properties unpopulated.

#### Example

Expand Down Expand Up @@ -192,7 +192,7 @@ An attribute that can be applied to parameters in an `AutoDataAttribute`-driven

- IncludeParameterType - indicates whether attribute target parameter `Type` should included as a first argument when creating customization; by default set to `false`

**Caution:** Order is important! Applying `CustomizeWith` attribute to the subsequent paramater makes precedig parameters of the same type to be created without specified customization and the particular parameter with the specified customization.
**Caution:** Order is important! Applying `CustomizeWith` attribute to the subsequent parameter makes preceding parameters of the same type to be created without specified customization and the particular parameter with the specified customization.

#### Example

Expand Down Expand Up @@ -285,6 +285,66 @@ public void CustomizeWithAttributeUsage(

***

## Data filtering attributes

The following attributes helps narrowing down data generation to specific values or omitting certain values.

For these attributes to work, they must be used in conjunction with other data generation attributes.

They can be applied to simple types and collections.

### Except

An attribute ensuring that values from outside the specified list will be generated.

#### Example

```csharp
[Theory]
[AutoData]
public void ExceptAttributeUsage(
[Except(DayOfWeek.Saturday, DayOfWeek.Sunday)] DayOfWeek workday)
{
Assert.True(workday is >= DayOfWeek.Monday and <= DayOfWeek.Friday);
}
```

### Range

An attribute ensuring that only values from specified range will be generated.

#### Example

```csharp
[Theory]
[AutoData]
public void RangeAttributeUsage(
[Range(11, 19)] int teenagerAge)
{
Assert.True(teenagerAge is > 11 and < 19);
}
```

### Values

An attribute ensuring that only values from the specified list will be generated.

#### Example

```csharp
[Theory]
[AutoData]
public void ValuesAttributeUsage(
[Values(DayOfWeek.Saturday, DayOfWeek.Sunday)] HashSet<DayOfWeek> weekend)
{
var weekendDays = new[] { DayOfWeek.Saturday, DayOfWeek.Sunday };
Assert.Equal(weekendDays.Length, weekend.Count);
piotrzajac marked this conversation as resolved.
Show resolved Hide resolved
Assert.All(weekendDays, (day) => Assert.Contains(day, weekend));
}
```

***

## Tips and tricks

### Fixture injection
Expand Down
9 changes: 6 additions & 3 deletions src/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ roslynator_equals_token_new_line = after
roslynator_infinite_loop_style = while
roslynator_max_line_length = 500
roslynator_new_line_at_end_of_file = true
roslynator_new_line_before_while_in_do_statement = false
roslynator_new_line_before_while_in_do_statement = true
roslynator_null_conditional_operator_new_line = after
roslynator_null_check_style = pattern_matching
roslynator_object_creation_parentheses_style = include
Expand Down Expand Up @@ -1368,7 +1368,7 @@ dotnet_diagnostic.RCS1235.severity = warning
dotnet_diagnostic.RCS1236.severity = warning

# RCS1237: Use bit shift operator.
dotnet_diagnostic.RCS1237.severity = suggestion
dotnet_diagnostic.RCS1237.severity = none

# RCS1238: Avoid nested ?: operators.
dotnet_diagnostic.RCS1238.severity = suggestion
Expand Down Expand Up @@ -2176,6 +2176,9 @@ dotnet_diagnostic.CA1849.severity = warning
# CA1850: Prefer static 'HashData' method over 'ComputeHash'
dotnet_diagnostic.CA1850.severity = warning

# CA1859: Use concrete types when possible for improved performance
dotnet_diagnostic.CA1859.severity = suggestion

# CA2000: Dispose objects before losing scope
dotnet_diagnostic.CA2000.severity = warning
dotnet_code_quality.CA2000.excluded_symbol_names =
Expand Down Expand Up @@ -3284,7 +3287,7 @@ dotnet_diagnostic.SA1519.severity = error
dotnet_diagnostic.SA1520.severity = error
dotnet_diagnostic.SA1600.severity = none
dotnet_diagnostic.SA1601.severity = none
dotnet_diagnostic.SA1602.severity = error
dotnet_diagnostic.SA1602.severity = none
dotnet_diagnostic.SA1603.severity = error
dotnet_diagnostic.SA1604.severity = error
dotnet_diagnostic.SA1605.severity = error
Expand Down
2 changes: 2 additions & 0 deletions src/Objectivity.AutoFixture.XUnit2.Core.Tests/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ dotnet_diagnostic.CA1707.severity = none
dotnet_diagnostic.CA1822.severity = none
# CA1859: Use concrete types when possible for improved performance
dotnet_diagnostic.CA1859.severity = none
# CA1861: Avoid constant arrays as arguments
dotnet_diagnostic.CA1861.severity = none
# CA2000: Dispose objects before losing scope
dotnet_diagnostic.CA2000.severity = none
# CA2007: Consider calling ConfigureAwait on the awaited task
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
{
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;

Expand All @@ -10,6 +11,7 @@
using global::AutoFixture;
using global::AutoFixture.Kernel;
using global::AutoFixture.Xunit2;

using Objectivity.AutoFixture.XUnit2.Core.Attributes;
using Objectivity.AutoFixture.XUnit2.Core.Customizations;

Expand Down Expand Up @@ -123,9 +125,9 @@ public void GivenCustomizeWithAttributeWithIncludeParameterTypeSet_WhenGetCustom
customization.Args.Should().HaveCount(1).And.Subject.First().Should().Be(parameter.ParameterType);
}

[Theory(DisplayName = "GIVEN CustomizeWith attribute with arguments WHEN GetCustomization is invoked THEN expected customization is returned with expected numner of arguments")]
[Theory(DisplayName = "GIVEN CustomizeWith attribute with arguments WHEN GetCustomization is invoked THEN expected customization is returned with expected number of arguments")]
[MemberData(nameof(ArgumentsDiscoveryCustomizationTestData))]
public void GivenCustomizeWithAttributeWithArguments_WhenGetCustomizationIsInvoked_ThenExpectedCustomizationIsReturnedWithExpectedNumnerOfArguments(bool includeParameterType, object[] args, int expectedNumberOfArguments)
public void GivenCustomizeWithAttributeWithArguments_WhenGetCustomizationIsInvoked_ThenExpectedCustomizationIsReturnedWithExpectedNumberOfArguments(bool includeParameterType, object[] args, int expectedNumberOfArguments)
{
// Arrange
var customizationType = typeof(ArgumentsDiscoveryCustomization);
Expand Down Expand Up @@ -169,9 +171,9 @@ public void GivenCustomizeWithAppliedToTheFirstArgument_WhenDataPopulated_ThenAl
instanceWithCustomization.Should().BeEmpty();
}

[Theory(DisplayName = "GIVEN CustomizeWith applied to the first argument of a cecrtain type WHEN data populated THEN only the first one has customization")]
[Theory(DisplayName = "GIVEN CustomizeWith applied to the first argument of a certain type WHEN data populated THEN only the first one has customization")]
[AutoData]
public void GivenCustomizeWithAppliedToTheFirstArgumentOfACecrtainType_WhenDataPopulated_ThenOnlyTheFirstOneHasCustomization(
public void GivenCustomizeWithAppliedToTheFirstArgumentOfACertainType_WhenDataPopulated_ThenOnlyTheFirstOneHasCustomization(
[EmptyCollection] IList<string> instanceWithCustomization,
IList<int?> instanceOfDifferentTypeWithoutCustomization)
{
Expand All @@ -182,8 +184,8 @@ public void GivenCustomizeWithAppliedToTheFirstArgumentOfACecrtainType_WhenDataP
instanceOfDifferentTypeWithoutCustomization.Should().NotBeEmpty();
}

[System.Diagnostics.CodeAnalysis.SuppressMessage("Roslynator", "RCS1163:Unused parameter.", Justification = "Required for test")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Required for test")]
[SuppressMessage("Roslynator", "RCS1163:Unused parameter.", Justification = "Required for test")]
[SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Required for test")]
protected void MethodUnderTest(bool parameter)
{
// Empty method under test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
namespace Objectivity.AutoFixture.XUnit2.Core.Tests.Attributes
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

using FluentAssertions;

using global::AutoFixture;
using global::AutoFixture.Kernel;
using global::AutoFixture.Xunit2;

using Moq;

using Objectivity.AutoFixture.XUnit2.Core.Attributes;

using Xunit;

[Collection("ExceptAttribute")]
[Trait("Category", "Attributes")]
public class ExceptAttributeTests
{
public enum Numbers
{
None = 0,
One = 1,
Two = 2,
Three = 4,
Four = 8,
Five = 16,
}

public static IEnumerable<object[]> CustomizationUsageTestData { get; } = new[]
{
new object[] { 1 },
new object[] { 1.5f },
new object[] { "test" },
new object[] { false },
new object[] { Numbers.Five },
new object[] { DateTime.Now },
new object[] { ValueTuple.Create(5) },
new object[] { Tuple.Create(1, 2) },
};

[Fact(DisplayName = "GIVEN uninitialized argument WHEN constructor is invoked THEN exception is thrown")]
public void GivenUninitializedArgument_WhenConstructorIsInvoked_ThenExceptionIsThrown()
{
// Arrange
// Act
// Assert
Assert.Throws<ArgumentNullException>(() => new ExceptAttribute(null));
}

[Fact(DisplayName = "GIVEN no arguments WHEN constructor is invoked THEN exception is thrown")]
public void GivenNoArguments_WhenConstructorIsInvoked_ThenExceptionIsThrown()
{
// Arrange
// Act
// Assert
Assert.Throws<ArgumentException>(() => new ExceptAttribute());
}

[InlineData(1, 1)]
[InlineData("a", "a")]
[Theory(DisplayName = "GIVEN identical arguments WHEN constructor is invoked THEN unique parameters are properly assigned")]
public void GivenIdenticalArguments_WhenConstructorIsInvoked_ThenUniqueParametersAreProperlyAssigned(
object first,
object second)
{
// Arrange
var attribute = new ExceptAttribute(first, second);

// Assert
attribute.Values.Should().HaveCount(1).And.BeEquivalentTo(new[] { first });
}

[InlineData(typeof(int), 2)]
[InlineData(1, typeof(int))]
[Theory(DisplayName = "GIVEN incomparable argument WHEN constructor is invoked THEN parameters are properly assigned")]
public void GivenIncomparableArgument_WhenConstructorIsInvoked_ThenParametersAreProperlyAssigned(
object first,
object second)
{
// Arrange
// Act
var attribute = new ExceptAttribute(first, second);

// Assert
attribute.Values.Should().HaveCount(2).And.BeEquivalentTo(new[] { first, second });
}

[MemberData(nameof(CustomizationUsageTestData))]
[Theory(DisplayName = "GIVEN valid parameters WHEN customization is used THEN expected values are generated")]
public void GivenValidParameters_WhenCustomizationIsUsed_ThenExpectedValuesAreGenerated(
object item)
{
// Arrange
var attribute = new ExceptAttribute(item);
var request = new Mock<ParameterInfo>();
var expectedType = item.GetType();
request.SetupGet(x => x.ParameterType)
.Returns(expectedType);
IFixture fixture = new Fixture();
fixture.Customize(attribute.GetCustomization(request.Object));

// Act
var result = fixture.Create(request.Object, new SpecimenContext(fixture));

// Assert
result.Should().NotBeNull()
.And.BeOfType(expectedType)
.And.NotBe(item);
}

[AutoData]
[Theory(DisplayName = "GIVEN multiple values specified but one WHEN values populated THEN the one value is used")]
public void GivenMultipleValuesSpecifiedButOne_WhenValuesPopulated_ThenTheOneValueIsUsed(
[Except(
Numbers.None,
Numbers.One,
Numbers.Two,
Numbers.Three,
Numbers.Four)] Numbers[] targetValues)
{
// Arrange
// Act
// Assert
targetValues.Should().AllSatisfy(x => x.Should().Be(Numbers.Five));
}

[AutoData]
[Theory(DisplayName = "GIVEN values specified only for one set WHEN values populated THEN the other set is not impacted")]
public void GivenValuesSpecifiedOnlyForOneSet_WhenValuesPopulated_ThenTheOtherSetIsNotImpacted(
[Except(
Numbers.None,
Numbers.One,
Numbers.Two,
Numbers.Three,
Numbers.Four)] Numbers[] firstSet,
Numbers[] secondSet)
{
// Arrange
// Act
// Assert
firstSet.Should().AllSatisfy(x => x.Should().Be(Numbers.Five));
secondSet.Where(x => x != Numbers.Five).Should().HaveCountGreaterThan(1);
}

[AutoData]
[Theory(DisplayName = "GIVEN multiple values specified but one WHEN values populated THEN generated sets have no common part")]
public void GivenDifferentValuesSpecifiedForDifferentSets_WhenValuesPopulated_ThenGeneratedSetsHaveNoCommonPart(
[Except(
Numbers.None,
Numbers.One,
Numbers.Two)] Numbers[] firstSet,
[Except(
Numbers.Three,
Numbers.Four,
Numbers.Five)] Numbers[] secondSet)
{
// Arrange
// Act
// Assert
firstSet.Should().AllSatisfy(x => x.Should().BeOneOf(Numbers.Three, Numbers.Four, Numbers.Five));
secondSet.Should().AllSatisfy(x => x.Should().BeOneOf(Numbers.None, Numbers.One, Numbers.Two));
}

[AutoData]
[Theory(DisplayName = "GIVEN values specified only for one set WHEN values populated THEN the other set is not impacted")]
public void GivenValuesSpecifiedForFirstValueAndFrozen_WhenValuesPopulated_ThenBothValuesAreImpacted(
[Frozen] [Except(
Numbers.None,
Numbers.One,
Numbers.Two,
Numbers.Three,
Numbers.Four)] Numbers firstValue,
Numbers secondValue)
{
// Arrange
// Act
// Assert
firstValue.Should().Be(Numbers.Five);
secondValue.Should().Be(firstValue);
}
}
}
Loading