diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fece077..a1a7083 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,5 +12,5 @@ jobs: tests: uses: PANiXiDA-Infrastructure/ci-cd/.github/workflows/dotnet-tests.yml@main with: - coverage_assembly_filters: "+TestDataGenerator*;-*Tests*" - coverage_threshold: "100" + coverage_assembly_filters: "+DataGenerator*;-*Tests*" + coverage_threshold: "90" diff --git a/DataGenerator/Configuration/DataGenerationCustomization.cs b/DataGenerator/Configuration/DataGenerationCustomization.cs index bd8e8e8..de4021b 100644 --- a/DataGenerator/Configuration/DataGenerationCustomization.cs +++ b/DataGenerator/Configuration/DataGenerationCustomization.cs @@ -4,7 +4,7 @@ namespace DataGenerator.Configuration; -internal sealed class DataGenerationCustomization(string locale = "ru", Action? configureFaker = null, int recursionDepth = 2) : ICustomization +internal sealed class DataGenerationCustomization(string locale, int seed, Action? configureFaker, int recursionDepth) : ICustomization { public void Customize(IFixture fixture) { @@ -13,6 +13,6 @@ public void Customize(IFixture fixture) fixture.Behaviors.Remove(behavior); } fixture.Behaviors.Add(new OmitOnRecursionBehavior(recursionDepth)); - fixture.Customizations.Insert(0, new Core.DataGenerator(locale, configureFaker)); + fixture.Customizations.Insert(0, new Core.DataGenerator(locale, seed, configureFaker)); } } diff --git a/DataGenerator/Configuration/FixtureFactory.cs b/DataGenerator/Configuration/FixtureFactory.cs index 9eb7be3..3841b0d 100644 --- a/DataGenerator/Configuration/FixtureFactory.cs +++ b/DataGenerator/Configuration/FixtureFactory.cs @@ -23,6 +23,7 @@ public static IFixture Create(string locale = "ru", int? seed = null, int recurs fixture.Customize(new DataGenerationCustomization( locale, + usedSeed, faker => { faker.Random = new Randomizer(usedSeed); configureFaker?.Invoke(faker); }, recursionDepth)); diff --git a/DataGenerator/Core/DataGenerator.cs b/DataGenerator/Core/DataGenerator.cs index f02da3b..bcced65 100644 --- a/DataGenerator/Core/DataGenerator.cs +++ b/DataGenerator/Core/DataGenerator.cs @@ -11,14 +11,17 @@ namespace DataGenerator.Core; internal sealed class DataGenerator : ISpecimenBuilder { + private const float NullProbability = 0.2f; + private readonly IReadOnlyList _generators; private readonly Faker _faker; + private readonly NullabilityInfoContext _nullabilityContext; - public DataGenerator(string locale, Action? configureFaker) + public DataGenerator(string locale, int seed, Action? configureFaker) { _faker = new Faker(locale) { - DateTimeReference = new DateTime(2030, 1, 1, 0, 0, 0, DateTimeKind.Utc) + DateTimeReference = DateTime.UnixEpoch.AddSeconds(seed) }; configureFaker?.Invoke(_faker); @@ -36,6 +39,8 @@ public DataGenerator(string locale, Action? configureFaker) new EnumerableGenerator(_faker), new DictionaryGenerator(_faker), ]; + + _nullabilityContext = new(); } public object Create(object request, ISpecimenContext context) @@ -57,6 +62,10 @@ private object CreateForProperty(PropertyInfo propertyInfo, ISpecimenContext con var type = propertyInfo.PropertyType; var name = propertyInfo.Name; + if (ShouldGenerateNull(propertyInfo, type)) + { + return null!; + } if (TryGenerateKnown(type, name, context, out var value)) { return value!; @@ -75,20 +84,49 @@ private object CreateForType(Type type, ISpecimenContext context) return new NoSpecimen(); } - private bool TryGenerateKnown(Type type, string? name, ISpecimenContext context, out object? result) + private bool ShouldGenerateNull(PropertyInfo property, Type type) + { + if (TryGenerateNullForNullableValueType(type)) + { + return true; + } + if (TryGenerateNullForNullableReferenceType(property, type)) + { + return true; + } + + return false; + } + + private bool TryGenerateNullForNullableValueType(Type type) { var underlying = Nullable.GetUnderlyingType(type); if (underlying != null) { - if (_faker.Random.Bool(0.2f)) - { - result = null; - return true; - } + return _faker.Random.Bool(NullProbability); + } + + return false; + } - type = underlying; + private bool TryGenerateNullForNullableReferenceType(PropertyInfo property, Type type) + { + if (type.IsValueType) + { + return false; } + var info = _nullabilityContext.Create(property); + if (info.ReadState == NullabilityState.Nullable) + { + return _faker.Random.Bool(NullProbability); + } + + return false; + } + + private bool TryGenerateKnown(Type type, string? name, ISpecimenContext context, out object? result) + { foreach (var generator in _generators) { if (generator.TryGenerate(type, name, context, out result)) diff --git a/DataGenerator/DataFacade.cs b/DataGenerator/DataFacade.cs index 2c68af7..6f7dc80 100644 --- a/DataGenerator/DataFacade.cs +++ b/DataGenerator/DataFacade.cs @@ -25,10 +25,12 @@ public DataFacade( int? seed = null, Action? configureFaker = null) { + var normalizedDepth = Math.Max(recursionDepth, 1); + var scopeHash = string.IsNullOrEmpty(scope) ? 0 : CryptoHelper.GetHash(scope); var usedSeed = (seed ?? FixtureFactory.DefaultSeed) ^ scopeHash; - Fixture = FixtureFactory.Create(locale, usedSeed, recursionDepth, configureFaker); + Fixture = FixtureFactory.Create(locale, usedSeed, normalizedDepth, configureFaker); } public T Create() diff --git a/DataGenerator/Generators/Implementations/ArrayGenerator.cs b/DataGenerator/Generators/Implementations/ArrayGenerator.cs index 2c1614a..2a78f9d 100644 --- a/DataGenerator/Generators/Implementations/ArrayGenerator.cs +++ b/DataGenerator/Generators/Implementations/ArrayGenerator.cs @@ -16,6 +16,11 @@ public bool TryGenerate(Type type, string? name, ISpecimenContext context, out o { return false; } + if (context.Resolve(elemType!) is OmitSpecimen or NoSpecimen) + { + value = Array.CreateInstance(elemType!, 0); + return true; + } var count = faker.Random.Int(2, 6); var array = Array.CreateInstance(elemType!, count); diff --git a/DataGenerator/Generators/Implementations/BoolGenerator.cs b/DataGenerator/Generators/Implementations/BoolGenerator.cs index 9883db5..c9601c5 100644 --- a/DataGenerator/Generators/Implementations/BoolGenerator.cs +++ b/DataGenerator/Generators/Implementations/BoolGenerator.cs @@ -25,14 +25,14 @@ private bool GenerateSmartBool(string propertyName) { var name = propertyName.ToLowerInvariant(); - if (name.StartsWith("is") || name.StartsWith("has") || name.Contains("enabled") || name.Contains("active")) - { - return faker.Random.Bool(0.7f); - } if (name.Contains("deleted") || name.Contains("disabled") || name.Contains("blocked")) { return faker.Random.Bool(0.1f); } + if (name.StartsWith("is") || name.StartsWith("has") || name.Contains("enabled") || name.Contains("active")) + { + return faker.Random.Bool(0.7f); + } return faker.Random.Bool(); } diff --git a/DataGenerator/Generators/Implementations/DictionaryGenerator.cs b/DataGenerator/Generators/Implementations/DictionaryGenerator.cs index 23a7c74..f509ddd 100644 --- a/DataGenerator/Generators/Implementations/DictionaryGenerator.cs +++ b/DataGenerator/Generators/Implementations/DictionaryGenerator.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Collections.Concurrent; using AutoFixture.Kernel; @@ -18,45 +19,43 @@ public bool TryGenerate(Type type, string? name, ISpecimenContext context, out o { return false; } + if (ShouldReturnEmptyDictionary(keyType!, valueType!, context)) + { + value = CreateEmptyDictionary(type, keyType!, valueType!); + return true; + } var count = faker.Random.Int(2, 5); - var dictionaryType = typeof(Dictionary<,>).MakeGenericType(keyType!, valueType!); - var dictionary = (IDictionary)Activator.CreateInstance(dictionaryType)!; + var dictionaryInstance = CreateEmptyDictionary(type, keyType!, valueType!)!; - int safety = 0; - while (dictionary.Count < count && safety < count * 4) + if (dictionaryInstance.GetType().IsGenericType && dictionaryInstance.GetType().GetGenericTypeDefinition() == typeof(ConcurrentDictionary<,>)) { - var key = GenerateKey(keyType!, context); - if (key is null) - { - safety++; - continue; - } - - var val = context.Resolve(valueType!); - - if (!dictionary.Contains(key)) - { - dictionary.Add(key, val); - } - - safety++; + FillConcurrent((dynamic)dictionaryInstance, keyType!, valueType!, context, count); + value = dictionaryInstance; + return true; + } + else + { + var dictionary = (IDictionary)dictionaryInstance; + FillDictionary(dictionary, keyType!, valueType!, context, count); + value = dictionary; } - value = dictionary; return true; } - private static bool TryGetDictionaryTypes(Type type, out Type? keyType, out Type? valueType) + public static bool TryGetDictionaryTypes(Type type, out Type? keyType, out Type? valueType) { keyType = valueType = null; if (type.IsGenericType) { var genericTypeDefinition = type.GetGenericTypeDefinition(); - if (genericTypeDefinition == typeof(IDictionary<,>) || - genericTypeDefinition == typeof(Dictionary<,>) || - genericTypeDefinition == typeof(IReadOnlyDictionary<,>)) + if (genericTypeDefinition == typeof(Dictionary<,>) || + genericTypeDefinition == typeof(IDictionary<,>) || + genericTypeDefinition == typeof(IReadOnlyDictionary<,>) || + genericTypeDefinition == typeof(SortedDictionary<,>) || + genericTypeDefinition == typeof(ConcurrentDictionary<,>)) { var args = type.GetGenericArguments(); keyType = args[0]; @@ -75,6 +74,7 @@ private static bool TryGetDictionaryTypes(Type type, out Type? keyType, out Type var args = @interface.GetGenericArguments(); keyType = args[0]; valueType = args[1]; + return true; } } @@ -85,11 +85,13 @@ private static bool TryGetDictionaryTypes(Type type, out Type? keyType, out Type private object? GenerateKey(Type keyType, ISpecimenContext context) { - var t = Nullable.GetUnderlyingType(keyType) ?? keyType; + var type = Nullable.GetUnderlyingType(keyType) ?? keyType; - var gen = CreateScalarGenerator(t); - if (gen is not null && gen.TryGenerate(t, name: null, context, out var value)) + var generator = CreateScalarGenerator(type); + if (generator is not null && generator.TryGenerate(type, name: null, context, out var value)) + { return value; + } return context.Resolve(keyType); } @@ -134,4 +136,97 @@ private static bool TryGetDictionaryTypes(Type type, out Type? keyType, out Type return null; } + + private static bool ShouldReturnEmptyDictionary(Type keyType, Type valueType, ISpecimenContext context) + { + var keyProbe = context.Resolve(keyType); + if (keyProbe is OmitSpecimen or NoSpecimen) + { + return true; + } + + var valProbe = context.Resolve(valueType); + return valProbe is OmitSpecimen or NoSpecimen; + } + + private static object CreateEmptyDictionary(Type requestType, Type keyType, Type valueType) + { + if (!requestType.IsInterface && !requestType.IsAbstract) + { + return Activator.CreateInstance(requestType)!; + } + if (requestType.GetGenericTypeDefinition() == typeof(SortedDictionary<,>)) + { + return Activator.CreateInstance(typeof(SortedDictionary<,>).MakeGenericType(keyType, valueType))!; + } + if (requestType.GetGenericTypeDefinition() == typeof(ConcurrentDictionary<,>)) + { + return Activator.CreateInstance(typeof(ConcurrentDictionary<,>).MakeGenericType(keyType, valueType))!; + } + if (requestType.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>)) + { + return Activator.CreateInstance(typeof(Dictionary<,>).MakeGenericType(keyType, valueType))!; + } + if (requestType.GetGenericTypeDefinition() == typeof(IDictionary<,>)) + { + return Activator.CreateInstance(typeof(Dictionary<,>).MakeGenericType(keyType, valueType))!; + } + + return Activator.CreateInstance(typeof(Dictionary<,>).MakeGenericType(keyType, valueType))!; + } + + private void FillDictionary(IDictionary dictionary, Type keyType, Type valueType, ISpecimenContext context, int count) + { + FillDictionaryCore( + getCount: () => dictionary.Count, + generateKey: () => GenerateKey(keyType, context), + generateValue: () => context.Resolve(valueType), + tryAdd: (key, value) => + { + if (!dictionary.Contains(key)) + { + dictionary.Add(key, value); + return true; + } + return false; + }, + count: count + ); + } + + private void FillConcurrent(ConcurrentDictionary dictionary, Type keyType, Type valueType, ISpecimenContext context, int count) + where TKey : notnull + { + FillDictionaryCore( + getCount: () => dictionary.Count, + generateKey: () => GenerateKey(keyType, context), + generateValue: () => context.Resolve(valueType), + tryAdd: (key, value) => dictionary.TryAdd((TKey)key!, (TValue)value!), + count: count + ); + } + + private static void FillDictionaryCore( + Func getCount, + Func generateKey, + Func generateValue, + Func tryAdd, + int count) + { + int safety = 0; + while (getCount() < count && safety < count * 4) + { + var key = generateKey(); + if (key is null) + { + safety++; + continue; + } + + var value = generateValue(); + tryAdd(key, value); + + safety++; + } + } } diff --git a/DataGenerator/Generators/Implementations/EnumerableGenerator.cs b/DataGenerator/Generators/Implementations/EnumerableGenerator.cs index 0f8c5fc..f0aeec5 100644 --- a/DataGenerator/Generators/Implementations/EnumerableGenerator.cs +++ b/DataGenerator/Generators/Implementations/EnumerableGenerator.cs @@ -18,10 +18,19 @@ public bool TryGenerate(Type type, string? name, ISpecimenContext context, out o { return false; } + if (DictionaryGenerator.TryGetDictionaryTypes(type, out var _, out var _)) + { + return false; + } if (!TryGetEnumerableElementType(type, out var elemType)) { return false; } + if (ShouldReturnEmptyCollection(elemType!, context)) + { + value = CreateEmptyListOf(elemType!); + return true; + } var count = faker.Random.Int(2, 6); var listType = typeof(List<>).MakeGenericType(elemType!); @@ -30,7 +39,10 @@ public bool TryGenerate(Type type, string? name, ISpecimenContext context, out o for (int i = 0; i < count; i++) { var item = context.Resolve(elemType!); - if (item is null && !elemType!.IsValueType) continue; + if (item is null && !elemType!.IsValueType) + { + continue; + } list.Add(item); } value = list; @@ -70,4 +82,15 @@ private static bool TryGetEnumerableElementType(Type type, out Type? elementType return false; } + + private static bool ShouldReturnEmptyCollection(Type elemType, ISpecimenContext context) + { + var probe = context.Resolve(elemType); + return probe is OmitSpecimen or NoSpecimen; + } + + private static object CreateEmptyListOf(Type elemType) + { + return (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(elemType))!; + } } diff --git a/DataGenerator/Generators/Implementations/NumericGenerator.cs b/DataGenerator/Generators/Implementations/NumericGenerator.cs index ae5180d..c04760a 100644 --- a/DataGenerator/Generators/Implementations/NumericGenerator.cs +++ b/DataGenerator/Generators/Implementations/NumericGenerator.cs @@ -17,7 +17,7 @@ public bool TryGenerate(Type type, string? name, ISpecimenContext context, out o } if (type == typeof(uint)) { - value = name is not null ? GenerateSmartInt(name) : faker.Random.Int(0, 10_000); + value = name is not null ? (uint)GenerateSmartInt(name) : faker.Random.UInt(0, 10_000); return true; } if (type == typeof(long)) @@ -27,17 +27,17 @@ public bool TryGenerate(Type type, string? name, ISpecimenContext context, out o } if (type == typeof(ulong)) { - value = (ulong)faker.Random.Long(0, long.MaxValue); + value = faker.Random.ULong(0, long.MaxValue); return true; } if (type == typeof(short)) { - value = (short)faker.Random.Int(short.MinValue, short.MaxValue); + value = faker.Random.Short(short.MinValue, short.MaxValue); return true; } if (type == typeof(ushort)) { - value = (ushort)faker.Random.Int(0, ushort.MaxValue); + value = faker.Random.UShort(0, ushort.MaxValue); return true; } if (type == typeof(byte)) @@ -47,7 +47,7 @@ public bool TryGenerate(Type type, string? name, ISpecimenContext context, out o } if (type == typeof(sbyte)) { - value = (sbyte)faker.Random.Int(sbyte.MinValue, sbyte.MaxValue); + value = faker.Random.SByte(sbyte.MinValue, sbyte.MaxValue); return true; } if (type == typeof(double)) @@ -57,7 +57,7 @@ public bool TryGenerate(Type type, string? name, ISpecimenContext context, out o } if (type == typeof(float)) { - value = (float)Math.Round(faker.Random.Double(0, 100_000), 4); + value = (float)Math.Round(faker.Random.Float(0, 100_000), 4); return true; } if (type == typeof(decimal)) diff --git a/Tests/MutateTests.cs b/Tests/MutateTests.cs deleted file mode 100644 index 8d07c42..0000000 --- a/Tests/MutateTests.cs +++ /dev/null @@ -1,60 +0,0 @@ -using DataGenerator; - -using FluentAssertions; - -using Tests.TestModels; - -using Xunit; - -namespace Tests; - -public sealed class MutateTests -{ - [Fact(DisplayName = "Mutate should change at least one property")] - public void Mutate_ChangesAtLeastOneProperty() - { - var facade = new DataFacade(); - var user = facade.Create(); - - var mutated = facade.Mutate(user); - - mutated.Should().NotBeEquivalentTo(user); - } - - [Fact(DisplayName = "Mutate should not change system fields (Id, CreatedAt, UpdatedAt, DeletedAt)")] - public void Mutate_DoesNotChangeSystemFields() - { - var facade = new DataFacade(); - var user = facade.Create(); - - var mutated = facade.Mutate(user); - - mutated.Id.Should().Be(user.Id); - mutated.CreatedAt.Should().Be(user.CreatedAt); - mutated.UpdatedAt.Should().Be(user.UpdatedAt); - mutated.DeletedAt.Should().Be(user.DeletedAt); - } - - [Fact(DisplayName = "Mutate should respect explicitly excluded properties")] - public void Mutate_RespectsExcludeProperties() - { - var facade = new DataFacade(); - var user = facade.Create(); - - var mutated = facade.Mutate(user, excludeProperties: [nameof(User.Email)]); - - mutated.Email.Should().Be(user.Email); - } - - [Fact(DisplayName = "Mutate should always change Login and Password")] - public void Mutate_AlwaysChangesLoginAndPassword() - { - var facade = new DataFacade(); - var user = facade.Create(); - - var mutated = facade.Mutate(user); - - mutated.Login.Should().NotBe(user.Login); - mutated.Password.Should().NotBe(user.Password); - } -} diff --git a/Tests/TestModels/ArrayTestModel.cs b/Tests/TestModels/ArrayTestModel.cs new file mode 100644 index 0000000..2f2309a --- /dev/null +++ b/Tests/TestModels/ArrayTestModel.cs @@ -0,0 +1,21 @@ +using Tests.TestModels.Common; +using Tests.TestModels.Common.Enums; + +namespace Tests.TestModels; + +public sealed class ArrayTestModel +{ + public required int[] Ints { get; set; } = []; + public required double[] Doubles { get; set; } = []; + public required bool[] Bools { get; set; } = []; + public required char[] Chars { get; set; } = []; + public required Guid[] Guids { get; set; } = []; + public required Uri[] Uris { get; set; } = []; + public required DateTime[] Dates { get; set; } = []; + public required DateOnly[] DateOnlys { get; set; } = []; + public required TimeOnly[] TimeOnlys { get; set; } = []; + public required TimeSpan[] TimeSpans { get; set; } = []; + public required Sample[] Enums { get; set; } = []; + public required string[] Strings { get; set; } = []; + public required SimpleItem[] Items { get; set; } = []; +} diff --git a/Tests/TestModels/BoolTestModel.cs b/Tests/TestModels/BoolTestModel.cs new file mode 100644 index 0000000..6300c5a --- /dev/null +++ b/Tests/TestModels/BoolTestModel.cs @@ -0,0 +1,12 @@ +namespace Tests.TestModels; + +public sealed class BoolTestModel +{ + public required bool IsActive { get; set; } + public required bool HasAccess { get; set; } + public required bool FeatureEnabled { get; set; } + public required bool IsDeleted { get; set; } + public required bool IsDisabled { get; set; } + public required bool IsBlocked { get; set; } + public required bool RandomFlag { get; set; } +} diff --git a/Tests/TestModels/CharTestModel.cs b/Tests/TestModels/CharTestModel.cs new file mode 100644 index 0000000..9781787 --- /dev/null +++ b/Tests/TestModels/CharTestModel.cs @@ -0,0 +1,8 @@ +namespace Tests.TestModels; + +public sealed class CharTestModel +{ + public required char Letter { get; set; } + public required char Digit { get; set; } + public required char Any { get; set; } +} diff --git a/Tests/TestModels/Common/Enums/Basic.cs b/Tests/TestModels/Common/Enums/Basic.cs new file mode 100644 index 0000000..574c4d4 --- /dev/null +++ b/Tests/TestModels/Common/Enums/Basic.cs @@ -0,0 +1,9 @@ +namespace Tests.TestModels.Common.Enums; + +public enum Basic +{ + Unknown = 0, + First, + Second, + Third +} diff --git a/Tests/TestModels/Common/Enums/NoZero.cs b/Tests/TestModels/Common/Enums/NoZero.cs new file mode 100644 index 0000000..88550d4 --- /dev/null +++ b/Tests/TestModels/Common/Enums/NoZero.cs @@ -0,0 +1,8 @@ +namespace Tests.TestModels.Common.Enums; + +public enum NoZero +{ + A = 1, + B = 2, + C = 3 +} diff --git a/Tests/TestModels/Common/Enums/Sample.cs b/Tests/TestModels/Common/Enums/Sample.cs new file mode 100644 index 0000000..b480227 --- /dev/null +++ b/Tests/TestModels/Common/Enums/Sample.cs @@ -0,0 +1,8 @@ +namespace Tests.TestModels.Common.Enums; + +public enum Sample +{ + Unknown = 0, + Active, + Disabled +} diff --git a/Tests/TestModels/Common/Enums/Weird.cs b/Tests/TestModels/Common/Enums/Weird.cs new file mode 100644 index 0000000..753b7a8 --- /dev/null +++ b/Tests/TestModels/Common/Enums/Weird.cs @@ -0,0 +1,8 @@ +namespace Tests.TestModels.Common.Enums; + +public enum Weird +{ + Zero = 0, + Ten = 10, + Minus = -5 +} diff --git a/Tests/TestModels/Common/SimpleItem.cs b/Tests/TestModels/Common/SimpleItem.cs new file mode 100644 index 0000000..b8b9871 --- /dev/null +++ b/Tests/TestModels/Common/SimpleItem.cs @@ -0,0 +1,6 @@ +namespace Tests.TestModels.Common; + +public sealed class SimpleItem +{ + public required string Name { get; set; } +} diff --git a/Tests/TestModels/DateTimeTestModel.cs b/Tests/TestModels/DateTimeTestModel.cs new file mode 100644 index 0000000..ca1be47 --- /dev/null +++ b/Tests/TestModels/DateTimeTestModel.cs @@ -0,0 +1,20 @@ +namespace Tests.TestModels; + +public sealed class DateTimeTestModel +{ + public required DateTime CreatedAt { get; set; } + public required DateTime UpdatedAt { get; set; } + public required DateTime ModifiedAt { get; set; } + public required DateTime? DeletedAt { get; set; } + + public required DateTime Birthday { get; set; } + public required DateTime Dob { get; set; } + + public required DateTime RegisteredAt { get; set; } + public required DateTime SignupDate { get; set; } + + public required TimeSpan Duration { get; set; } + + public required DateOnly BirthDateOnly { get; set; } + public required TimeOnly WakeUpTime { get; set; } +} diff --git a/Tests/TestModels/DictionaryTestModel.cs b/Tests/TestModels/DictionaryTestModel.cs new file mode 100644 index 0000000..c328ae7 --- /dev/null +++ b/Tests/TestModels/DictionaryTestModel.cs @@ -0,0 +1,28 @@ +using System.Collections.Concurrent; + +using Tests.TestModels.Common; +using Tests.TestModels.Common.Enums; + +namespace Tests.TestModels; + +public sealed class DictionaryTestModel +{ + public required Dictionary IntStringDictionary { get; set; } = []; + public required Dictionary DoubleStringDictionary { get; set; } = []; + public required Dictionary StringGuidDictionary { get; set; } = []; + public required Dictionary CharBoolDictionary { get; set; } = []; + public required Dictionary BoolDoubleDictionary { get; set; } = []; + public required Dictionary UriIntDictionary { get; set; } = []; + public required Dictionary DateStringDictionary { get; set; } = []; + public required Dictionary DateOnlyStringDictionary { get; set; } = []; + public required Dictionary TimeOnlyStringDictionary { get; set; } = []; + public required Dictionary TimeSpanStringDictionary { get; set; } = []; + public required Dictionary EnumCharDictionary { get; set; } = []; + public required Dictionary StringComplexDictionary { get; set; } = []; + + public required SortedDictionary SortedStringIntDictionary { get; set; } = []; + public required ConcurrentDictionary ConcurrentStringDoubleDictionary { get; set; } = []; + + public required IDictionary GuidIntDictionary { get; set; } = new Dictionary(); + public required IReadOnlyDictionary StringBoolDictionary { get; set; } = new Dictionary(); +} diff --git a/Tests/TestModels/EnumTestModel.cs b/Tests/TestModels/EnumTestModel.cs new file mode 100644 index 0000000..a15049b --- /dev/null +++ b/Tests/TestModels/EnumTestModel.cs @@ -0,0 +1,10 @@ +using Tests.TestModels.Common.Enums; + +namespace Tests.TestModels; + +public sealed class EnumTestModel +{ + public required Basic Basic { get; set; } + public required NoZero NoZero { get; set; } + public required Weird Weird { get; set; } +} diff --git a/Tests/TestModels/EnumerableTestModel.cs b/Tests/TestModels/EnumerableTestModel.cs new file mode 100644 index 0000000..836c613 --- /dev/null +++ b/Tests/TestModels/EnumerableTestModel.cs @@ -0,0 +1,27 @@ +using Tests.TestModels.Common; +using Tests.TestModels.Common.Enums; + +namespace Tests.TestModels; + +public sealed class EnumerableTestModel +{ + public required IEnumerable IntEnumerable { get; set; } = []; + public required IEnumerable DoubleEnumerable { get; set; } = []; + public required IEnumerable BoolEnumerable { get; set; } = []; + public required IEnumerable CharEnumerable { get; set; } = []; + public required IEnumerable GuidEnumerable { get; set; } = []; + public required IEnumerable UriEnumerable { get; set; } = []; + public required IEnumerable DateEnumerable { get; set; } = []; + public required IEnumerable DateOnlyEnumerable { get; set; } = []; + public required IEnumerable TimeOnlyEnumerable { get; set; } = []; + public required IEnumerable TimeSpanEnumerable { get; set; } = []; + public required IEnumerable EnumEnumerable { get; set; } = []; + public required IEnumerable StringEnumerable { get; set; } = []; + public required IEnumerable ItemEnumerable { get; set; } = []; + + public required ICollection StringCollection { get; set; } = []; + public required IList GuidList { get; set; } = []; + public required IReadOnlyCollection BoolReadOnlyCollection { get; set; } = []; + public required IReadOnlyList EnumReadOnlyList { get; set; } = []; + public required List ItemList { get; set; } = []; +} diff --git a/Tests/TestModels/GuidTestModel.cs b/Tests/TestModels/GuidTestModel.cs new file mode 100644 index 0000000..3a60d98 --- /dev/null +++ b/Tests/TestModels/GuidTestModel.cs @@ -0,0 +1,8 @@ +namespace Tests.TestModels; + +public sealed class GuidTestModel +{ + public required Guid Id { get; set; } + public required Guid ExternalId { get; set; } + public required Guid CorrelationId { get; set; } +} diff --git a/Tests/TestModels/NullableModel.cs b/Tests/TestModels/NullableModel.cs new file mode 100644 index 0000000..91763e4 --- /dev/null +++ b/Tests/TestModels/NullableModel.cs @@ -0,0 +1,7 @@ +namespace Tests.TestModels; + +public sealed class NullableModel +{ + public string? MiddleName { get; set; } + public DateTime? DeletedAt { get; set; } +} diff --git a/Tests/TestModels/NumericTestModel.cs b/Tests/TestModels/NumericTestModel.cs new file mode 100644 index 0000000..b10542b --- /dev/null +++ b/Tests/TestModels/NumericTestModel.cs @@ -0,0 +1,26 @@ +namespace Tests.TestModels; + +public sealed class NumericTestModel +{ + public required int Id { get; set; } + public required int UserId { get; set; } + public required int Age { get; set; } + public required int ItemCount { get; set; } + public required int Qty { get; set; } + public required int TotalNum { get; set; } + public required int Price { get; set; } + public required int OrderAmount { get; set; } + public required int TotalSum { get; set; } + public required int ReleaseYear { get; set; } + + public required uint UIntValue { get; set; } + public required long LongValue { get; set; } + public required ulong ULongValue { get; set; } + public required short ShortValue { get; set; } + public required ushort UShortValue { get; set; } + public required byte ByteValue { get; set; } + public required sbyte SByteValue { get; set; } + public required double DoubleValue { get; set; } + public required float FloatValue { get; set; } + public required decimal DecimalValue { get; set; } +} diff --git a/Tests/TestModels/SampleModel.cs b/Tests/TestModels/SampleModel.cs new file mode 100644 index 0000000..a76b64a --- /dev/null +++ b/Tests/TestModels/SampleModel.cs @@ -0,0 +1,45 @@ +using System.Collections.Concurrent; + +using DataGenerator.MutationProperties; + +using Tests.TestModels.Common.Enums; + +namespace Tests.TestModels; + +public sealed class SampleModel : IMutationIgnoreProperties, IMutationForceProperties +{ + public required int Id { get; set; } + public required DateTime CreatedAt { get; set; } + public required DateTime UpdatedAt { get; set; } + public DateTime? DeletedAt { get; set; } + + public required string Login { get; set; } + public required string Password { get; set; } + + public required Guid ExternalId { get; set; } + public required string Email { get; set; } + public required string DisplayName { get; set; } + public required int Age { get; set; } + public required bool IsActive { get; set; } + public required Uri AvatarUrl { get; set; } + public required Sample Status { get; set; } + + public required int[] Scores { get; set; } = []; + + public required SampleModel Parent { get; set; } = null!; + + public required SampleModel[] Siblings { get; set; } = []; + + public required List Children { get; set; } = []; + public required IEnumerable EnumerableChildren { get; set; } = []; + public required ICollection CollectionChildren { get; set; } = []; + public required IList ListChildren { get; set; } = []; + public required IReadOnlyCollection ReadOnlyCollectionChildren { get; set; } = []; + public required IReadOnlyList ReadOnlyListChildren { get; set; } = []; + + public required Dictionary Related { get; set; } = []; + public required IReadOnlyDictionary ReadOnlyRelated { get; set; } = new Dictionary(); + public required IDictionary InterfaceDictionaryRelated { get; set; } = new Dictionary(); + public required SortedDictionary SortedRelated { get; set; } = []; + public required ConcurrentDictionary ConcurrentRelated { get; set; } = []; +} \ No newline at end of file diff --git a/Tests/TestModels/StringTestModel.cs b/Tests/TestModels/StringTestModel.cs new file mode 100644 index 0000000..6339306 --- /dev/null +++ b/Tests/TestModels/StringTestModel.cs @@ -0,0 +1,52 @@ +namespace Tests.TestModels; + +public sealed class StringTestModel +{ + public required string EmailAddress { get; set; } + + public required string Phone { get; set; } + public required string MobilePhone { get; set; } + + public required string FullName { get; set; } + public required string Name { get; set; } + public required string Fio { get; set; } + + public required string FirstName { get; set; } + public required string LastName { get; set; } + public required string Surname { get; set; } + + public required string Username { get; set; } + public required string Login { get; set; } + + public required string Password { get; set; } + public required string PwdHash { get; set; } + + public required string WebsiteUrl { get; set; } + public required string ProfileLink { get; set; } + + public required string Avatar { get; set; } + + public required string Country { get; set; } + public required string City { get; set; } + public required string StreetAddress { get; set; } + + public required string Postcode { get; set; } + public required string PostalCode { get; set; } + public required string ZipCode { get; set; } + + public required string Title { get; set; } + public required string SubjectTitle { get; set; } + + public required string Description { get; set; } + public required string MessageText { get; set; } + public required string ContentString { get; set; } + + public required string Company { get; set; } + + public required string Iban { get; set; } + public required string AccountNumber { get; set; } + + public required string Currency { get; set; } + + public required string RandomString { get; set; } +} diff --git a/Tests/TestModels/UriTestModel.cs b/Tests/TestModels/UriTestModel.cs new file mode 100644 index 0000000..7bee592 --- /dev/null +++ b/Tests/TestModels/UriTestModel.cs @@ -0,0 +1,8 @@ +namespace Tests.TestModels; + +public sealed class UriTestModel +{ + public required Uri WebsiteUri { get; set; } + public required Uri AvatarUri { get; set; } + public required Uri ApiEndpointUri { get; set; } +} diff --git a/Tests/TestModels/User.cs b/Tests/TestModels/User.cs deleted file mode 100644 index 50372d1..0000000 --- a/Tests/TestModels/User.cs +++ /dev/null @@ -1,17 +0,0 @@ -using DataGenerator.MutationProperties; - -namespace Tests.TestModels; - -public sealed class User : IMutationIgnoreProperties, IMutationForceProperties -{ - public int Id { get; set; } - public DateTime CreatedAt { get; set; } - public DateTime UpdatedAt { get; set; } - public DateTime? DeletedAt { get; set; } - - public required string Login { get; set; } - public required string Password { get; set; } - - public required string Email { get; set; } - public required string DisplayName { get; set; } -} diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 68420ed..33d7ff4 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -7,6 +7,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Tests/Tests/ArrayGeneratorTests.cs b/Tests/Tests/ArrayGeneratorTests.cs new file mode 100644 index 0000000..8ebfa95 --- /dev/null +++ b/Tests/Tests/ArrayGeneratorTests.cs @@ -0,0 +1,148 @@ +using DataGenerator; + +using FluentAssertions; + +using Tests.TestModels; +using Tests.TestModels.Common.Enums; + +using Xunit; + +namespace Tests.Tests; + +public sealed class ArrayGeneratorTests +{ + [Fact(DisplayName = "ArrayGenerator should generate not empty int[]")] + public void Create_IntArray() + { + var generated = new DataFacade().Create(); + + generated.Ints.Should().NotBeNull(); + generated.Ints.Should().NotBeEmpty(); + generated.Ints.Should().OnlyContain(item => item != default); + } + + [Fact(DisplayName = "ArrayGenerator should generate not empty double[]")] + public void Create_DoubleArray() + { + var generated = new DataFacade().Create(); + + generated.Doubles.Should().NotBeNull(); + generated.Doubles.Should().NotBeEmpty(); + generated.Doubles.Should().OnlyContain(item => !double.IsNaN(item) && !double.IsInfinity(item)); + generated.Doubles.Should().Contain(item => Math.Abs(item) > 0.000001); + } + + [Fact(DisplayName = "ArrayGenerator should generate not empty bool[]")] + public void Create_BoolArray() + { + var generated = new DataFacade().Create(); + + generated.Bools.Should().NotBeNull(); + generated.Bools.Should().NotBeEmpty(); + + generated.Bools.Should().Contain(item => item).And.Contain(item => !item); + } + + [Fact(DisplayName = "ArrayGenerator should generate not empty char[]")] + public void Create_CharArray() + { + var generated = new DataFacade().Create(); + + generated.Chars.Should().NotBeNull(); + generated.Chars.Should().NotBeEmpty(); + + generated.Chars.Should().OnlyContain(item => + char.IsLetter(item) || char.IsDigit(item) || char.IsSymbol(item) || char.IsPunctuation(item)); + } + + [Fact(DisplayName = "ArrayGenerator should generate not empty Guid[] with valid values")] + public void Create_GuidArray() + { + var generated = new DataFacade().Create(); + + generated.Guids.Should().NotBeNull(); + generated.Guids.Should().NotBeEmpty(); + generated.Guids.Should().OnlyContain(item => item != Guid.Empty); + } + + [Fact(DisplayName = "ArrayGenerator should generate not empty Uri[]")] + public void Create_UriArray() + { + var generated = new DataFacade().Create(); + + generated.Uris.Should().NotBeNull(); + generated.Uris.Should().NotBeEmpty(); + generated.Uris.Should().OnlyContain(item => item != null); + } + + [Fact(DisplayName = "ArrayGenerator should generate not empty DateTime[]")] + public void Create_DateArray() + { + var generated = new DataFacade().Create(); + + generated.Dates.Should().NotBeNull(); + generated.Dates.Should().NotBeEmpty(); + generated.Dates.Should().OnlyContain(item => item != default); + } + + [Fact(DisplayName = "ArrayGenerator should generate not empty DateOnly[]")] + public void Create_DateOnlyArray() + { + var generated = new DataFacade().Create(); + + generated.DateOnlys.Should().NotBeNull(); + generated.DateOnlys.Should().NotBeEmpty(); + generated.DateOnlys.Should().OnlyContain(item => item != default); + } + + [Fact(DisplayName = "ArrayGenerator should generate not empty TimeOnly[]")] + public void Create_TimeOnlyArray() + { + var generated = new DataFacade().Create(); + + generated.TimeOnlys.Should().NotBeNull(); + generated.TimeOnlys.Should().NotBeEmpty(); + generated.TimeOnlys.Should().OnlyContain(item => item != default); + } + + [Fact(DisplayName = "ArrayGenerator should generate not empty TimeSpan[]")] + public void Create_TimeSpanArray() + { + var generated = new DataFacade().Create(); + + generated.TimeSpans.Should().NotBeNull(); + generated.TimeSpans.Should().NotBeEmpty(); + generated.TimeSpans.Should().OnlyContain(item => item != default); + } + + + [Fact(DisplayName = "ArrayGenerator should generate not empty enum[] without Unknown")] + public void Create_EnumArray() + { + var generated = new DataFacade().Create(); + + generated.Enums.Should().NotBeNull(); + generated.Enums.Should().NotBeEmpty(); + generated.Enums.Should().OnlyContain(item => item != Sample.Unknown); + } + + [Fact(DisplayName = "ArrayGenerator should generate not empty string[] with non-empty values")] + public void Create_StringArray() + { + var generated = new DataFacade().Create(); + + generated.Strings.Should().NotBeNull(); + generated.Strings.Should().NotBeEmpty(); + generated.Strings.Should().OnlyContain(item => !string.IsNullOrWhiteSpace(item)); + } + + [Fact(DisplayName = "ArrayGenerator should generate not empty SimpleItem[]")] + public void Create_ObjectArray() + { + var generated = new DataFacade().Create(); + + generated.Items.Should().NotBeNull(); + generated.Items.Should().NotBeEmpty(); + generated.Items.Should().OnlyContain(item => item != null && !string.IsNullOrWhiteSpace(item.Name)); + } +} diff --git a/Tests/Tests/BoolGeneratorTests.cs b/Tests/Tests/BoolGeneratorTests.cs new file mode 100644 index 0000000..660241e --- /dev/null +++ b/Tests/Tests/BoolGeneratorTests.cs @@ -0,0 +1,53 @@ +using DataGenerator; + +using FluentAssertions; + +using Tests.TestModels; + +using Xunit; + +namespace Tests.Tests; + +public sealed class BoolGeneratorTests +{ + private const int SampleCount = 50; + private const int MajorityThreshold = SampleCount / 2; + + [Fact(DisplayName = "BoolGenerator should generate mostly true for Is*/Has*/Enabled/Active names")] + public void Create_MostlyPositiveFlags() + { + var facade = new DataFacade(); + var generated = Enumerable.Range(0, SampleCount) + .Select(_ => facade.Create()) + .ToList(); + + generated.Count(item => item.IsActive).Should().BeGreaterThan(MajorityThreshold); + generated.Count(item => item.HasAccess).Should().BeGreaterThan(MajorityThreshold); + generated.Count(item => item.FeatureEnabled).Should().BeGreaterThan(MajorityThreshold); + } + + [Fact(DisplayName = "BoolGenerator should generate mostly false for Deleted/Disabled/Blocked names")] + public void Create_MostlyNegativeFlags() + { + var facade = new DataFacade(); + var generated = Enumerable.Range(0, SampleCount) + .Select(_ => facade.Create()) + .ToList(); + + generated.Count(item => item.IsDeleted).Should().BeLessThan(MajorityThreshold); + generated.Count(item => item.IsDisabled).Should().BeLessThan(MajorityThreshold); + generated.Count(item => item.IsBlocked).Should().BeLessThan(MajorityThreshold); + } + + [Fact(DisplayName = "BoolGenerator should generate both true and false for flags without naming meaning")] + public void Create_RandomFlag_ShouldContainTrueAndFalse() + { + var facade = new DataFacade(); + var generated = Enumerable.Range(0, SampleCount) + .Select(_ => facade.Create().RandomFlag) + .ToList(); + + generated.Should().Contain(true); + generated.Should().Contain(false); + } +} diff --git a/Tests/Tests/CharGeneratorTests.cs b/Tests/Tests/CharGeneratorTests.cs new file mode 100644 index 0000000..6d2780c --- /dev/null +++ b/Tests/Tests/CharGeneratorTests.cs @@ -0,0 +1,58 @@ +using DataGenerator; + +using FluentAssertions; + +using Tests.TestModels; + +using Xunit; + +namespace Tests.Tests; + +public sealed class CharGeneratorTests +{ + private const int SampleCount = 200; + private const int MajorityThreshold = SampleCount * 60 / 100; + + [Fact(DisplayName = "CharGenerator should generate mostly lowercase letters")] + public void Create_MostlyLetters() + { + var facade = new DataFacade(); + var generated = Enumerable.Range(0, SampleCount) + .Select(_ => facade.Create().Letter) + .ToList(); + + var lettersCount = generated.Count(item => item is >= 'a' and <= 'z'); + lettersCount.Should().BeGreaterThan(MajorityThreshold); + } + + [Fact(DisplayName = "CharGenerator should generate some digits")] + public void Create_ShouldGenerateDigits() + { + var facade = new DataFacade(); + var generated = Enumerable.Range(0, SampleCount) + .Select(_ => facade.Create().Digit) + .ToList(); + + var digitsCount = generated.Count(item => item is >= '0' and <= '9'); + digitsCount.Should().BeGreaterThan(0); + digitsCount.Should().BeLessThan(MajorityThreshold); + } + + [Fact(DisplayName = "CharGenerator should generate some printable non-alphanumeric symbols")] + public void Create_ShouldGenerateSpecialCharacters() + { + var facade = new DataFacade(); + var generated = Enumerable.Range(0, SampleCount) + .Select(_ => facade.Create().Any) + .ToList(); + + var specialCount = generated.Count(item => + item is >= '\u0021' and <= '\u007E' + && (item < 'a' || item > 'z') + && (item < '0' || item > '9') + ); + + specialCount.Should().BeGreaterThan(0); + specialCount.Should().BeLessThan(SampleCount / 3); + } +} diff --git a/Tests/Tests/DataGeneratorRecursionTests.cs b/Tests/Tests/DataGeneratorRecursionTests.cs new file mode 100644 index 0000000..d6af83a --- /dev/null +++ b/Tests/Tests/DataGeneratorRecursionTests.cs @@ -0,0 +1,114 @@ +using DataGenerator; + +using FluentAssertions; + +using Tests.TestModels; + +using Xunit; + +namespace Tests.Tests; + +public sealed class DataGeneratorRecursionTests +{ + [Fact(DisplayName = "Recursion depth default = 3")] + public void Create_RecursionDepth_Default() + { + var generated = new DataFacade().Create(); + + CountDepth(generated).Should().Be(3); + + AssertNotEmptyCollections(generated); + AssertNotEmptyDictionaries(generated); + } + + [Fact(DisplayName = "Recursion depth = 2")] + public void Create_RecursionDepth_2() + { + var generated = new DataFacade(recursionDepth: 2).Create(); + + CountDepth(generated).Should().Be(2); + + generated.Parent.Should().NotBeNull(); + generated.Parent.Parent.Should().BeNull(); + + AssertNotEmptyCollections(generated); + AssertNotEmptyDictionaries(generated); + } + + [Fact(DisplayName = "Recursion depth = 1 (root only)")] + public void Create_RecursionDepth_1() + { + var generated = new DataFacade(recursionDepth: 1).Create(); + + CountDepth(generated).Should().Be(1); + generated.Parent.Should().BeNull(); + + AssertEmptyCollections(generated); + AssertEmptyDictionaries(generated); + } + + [Fact(DisplayName = "Recursion depth = 0 behaves exactly as depth = 1")] + public void Create_RecursionDepth_0() + { + var generated = new DataFacade(recursionDepth: 0).Create(); + + generated.Parent.Should().BeNull(); + + AssertEmptyCollections(generated); + AssertEmptyDictionaries(generated); + } + + private static int CountDepth(SampleModel model) + { + var depth = 1; + var current = model; + + while (current.Parent is not null) + { + depth++; + current = current.Parent; + } + + return depth; + } + + private static void AssertNotEmptyCollections(SampleModel generated) + { + generated.Children.Should().NotBeNull().And.NotBeEmpty(); + generated.Siblings.Should().NotBeNull().And.NotBeEmpty(); + generated.EnumerableChildren.Should().NotBeNull().And.NotBeEmpty(); + generated.CollectionChildren.Should().NotBeNull().And.NotBeEmpty(); + generated.ListChildren.Should().NotBeNull().And.NotBeEmpty(); + generated.ReadOnlyCollectionChildren.Should().NotBeNull().And.NotBeEmpty(); + generated.ReadOnlyListChildren.Should().NotBeNull().And.NotBeEmpty(); + } + + private static void AssertEmptyCollections(SampleModel generated) + { + generated.Children.Should().NotBeNull().And.BeEmpty(); + generated.Siblings.Should().NotBeNull().And.BeEmpty(); + generated.EnumerableChildren.Should().NotBeNull().And.BeEmpty(); + generated.CollectionChildren.Should().NotBeNull().And.BeEmpty(); + generated.ListChildren.Should().NotBeNull().And.BeEmpty(); + generated.ReadOnlyCollectionChildren.Should().NotBeNull().And.BeEmpty(); + generated.ReadOnlyListChildren.Should().NotBeNull().And.BeEmpty(); + } + + private static void AssertNotEmptyDictionaries(SampleModel generated) + { + generated.Related.Should().NotBeNull().And.NotBeEmpty(); + generated.ReadOnlyRelated.Should().NotBeNull().And.NotBeEmpty(); + generated.InterfaceDictionaryRelated.Should().NotBeNull().And.NotBeEmpty(); + generated.SortedRelated.Should().NotBeNull().And.NotBeEmpty(); + generated.ConcurrentRelated.Should().NotBeNull().And.NotBeEmpty(); + } + + private static void AssertEmptyDictionaries(SampleModel generated) + { + generated.Related.Should().NotBeNull().And.BeEmpty(); + generated.ReadOnlyRelated.Should().NotBeNull().And.BeEmpty(); + generated.InterfaceDictionaryRelated.Should().NotBeNull().And.BeEmpty(); + generated.SortedRelated.Should().NotBeNull().And.BeEmpty(); + generated.ConcurrentRelated.Should().NotBeNull().And.BeEmpty(); + } +} \ No newline at end of file diff --git a/Tests/Tests/DateTimeGeneratorTests.cs b/Tests/Tests/DateTimeGeneratorTests.cs new file mode 100644 index 0000000..8756db7 --- /dev/null +++ b/Tests/Tests/DateTimeGeneratorTests.cs @@ -0,0 +1,61 @@ +using DataGenerator; + +using FluentAssertions; + +using Tests.TestModels; + +using Xunit; + +namespace Tests.Tests; + +public sealed class DateTimeGeneratorTests +{ + private static readonly DateTime Now = DateTime.UtcNow; + + [Fact(DisplayName = "DateTimeGenerator should generate dates based on naming conventions")] + public void Create_GeneratesExpectedDateTimeValues() + { + var facade = new DataFacade(); + var generated = facade.Create(); + + generated.CreatedAt.Should().BeBefore(Now); + generated.UpdatedAt.Should().BeBefore(Now); + generated.ModifiedAt.Should().BeBefore(Now); + + if (generated.DeletedAt is not null) + { + generated.DeletedAt.Value.Should().BeBefore(Now); + } + + (Now.Year - generated.Birthday.Year).Should().BeInRange(18, 120); + (Now.Year - generated.Dob.Year).Should().BeInRange(18, 120); + + generated.RegisteredAt.Should().BeBefore(Now); + generated.SignupDate.Should().BeBefore(Now); + } + + [Fact(DisplayName = "DateTimeGenerator should generate TimeSpan values")] + public void Create_GeneratesTimeSpan() + { + var generated = new DataFacade().Create(); + + generated.Duration.Should().NotBe(default); + generated.Duration.TotalSeconds.Should().BeGreaterThan(0); + } + + [Fact(DisplayName = "DateTimeGenerator should generate DateOnly values")] + public void Create_GeneratesDateOnly() + { + var generated = new DataFacade().Create(); + generated.BirthDateOnly.Should().NotBe(default); + } + + [Fact(DisplayName = "DateTimeGenerator should generate TimeOnly values")] + public void Create_GeneratesTimeOnly() + { + var generated = new DataFacade().Create(); + + generated.WakeUpTime.Hour.Should().BeInRange(0, 23); + generated.WakeUpTime.Minute.Should().BeInRange(0, 59); + } +} diff --git a/Tests/Tests/DictionaryGeneratorTests.cs b/Tests/Tests/DictionaryGeneratorTests.cs new file mode 100644 index 0000000..0be985a --- /dev/null +++ b/Tests/Tests/DictionaryGeneratorTests.cs @@ -0,0 +1,173 @@ +using DataGenerator; + +using FluentAssertions; + +using Tests.TestModels; +using Tests.TestModels.Common.Enums; + +using Xunit; + +namespace Tests.Tests; + +public sealed class DictionaryGeneratorTests +{ + [Fact(DisplayName = "DictionaryGenerator should generate not empty Dictionary")] + public void Create_IntStringDictionary() + { + var generated = new DataFacade().Create(); + + generated.IntStringDictionary.Should().NotBeNull().And.NotBeEmpty(); + generated.IntStringDictionary.Keys.Should().OnlyContain(item => item != default); + generated.IntStringDictionary.Values.Should().OnlyContain(item => !string.IsNullOrWhiteSpace(item)); + } + + [Fact(DisplayName = "DictionaryGenerator should generate not empty Dictionary")] + public void Create_DoubleStringDictionary() + { + var generated = new DataFacade().Create(); + + generated.DoubleStringDictionary.Should().NotBeNull().And.NotBeEmpty(); + generated.DoubleStringDictionary.Keys.Should().OnlyContain(item => !double.IsNaN(item) && !double.IsInfinity(item)); + generated.DoubleStringDictionary.Values.Should().OnlyContain(item => !string.IsNullOrWhiteSpace(item)); + } + + [Fact(DisplayName = "DictionaryGenerator should generate not empty Dictionary")] + public void Create_StringGuidDictionary() + { + var generated = new DataFacade().Create(); + + generated.StringGuidDictionary.Should().NotBeNull().And.NotBeEmpty(); + generated.StringGuidDictionary.Keys.Should().OnlyContain(item => !string.IsNullOrWhiteSpace(item)); + generated.StringGuidDictionary.Values.Should().OnlyContain(item => item != Guid.Empty); + } + + [Fact(DisplayName = "DictionaryGenerator should generate not empty Dictionary")] + public void Create_CharBoolDictionary() + { + var generated = new DataFacade().Create(); + + generated.CharBoolDictionary.Should().NotBeNull().And.NotBeEmpty(); + generated.CharBoolDictionary.Keys.Should().OnlyContain(item => item != default); + generated.CharBoolDictionary.Values.Should().OnlyContain(item => item || !item); + } + + [Fact(DisplayName = "DictionaryGenerator should generate not empty Dictionary")] + public void Create_BoolDoubleDictionary() + { + var generated = new DataFacade().Create(); + + generated.BoolDoubleDictionary.Should().NotBeNull().And.NotBeEmpty(); + generated.BoolDoubleDictionary.Keys.Should().Contain(true).And.Contain(false); + generated.BoolDoubleDictionary.Values.Should().OnlyContain(item => !double.IsNaN(item) && !double.IsInfinity(item)); + } + + [Fact(DisplayName = "DictionaryGenerator should generate not empty Dictionary")] + public void Create_UriIntDictionary() + { + var generated = new DataFacade().Create(); + + generated.UriIntDictionary.Should().NotBeNull().And.NotBeEmpty(); + generated.UriIntDictionary.Keys.Should().OnlyContain(item => item != null); + generated.UriIntDictionary.Values.Should().OnlyContain(item => item != default); + } + + [Fact(DisplayName = "DictionaryGenerator should generate not empty Dictionary")] + public void Create_DateStringDictionary() + { + var generated = new DataFacade().Create(); + + generated.DateStringDictionary.Should().NotBeNull().And.NotBeEmpty(); + generated.DateStringDictionary.Keys.Should().OnlyContain(item => item != default); + generated.DateStringDictionary.Values.Should().OnlyContain(item => !string.IsNullOrWhiteSpace(item)); + } + + [Fact(DisplayName = "DictionaryGenerator should generate not empty Dictionary")] + public void Create_DateOnlyStringDictionary() + { + var generated = new DataFacade().Create(); + + generated.DateOnlyStringDictionary.Should().NotBeNull().And.NotBeEmpty(); + generated.DateOnlyStringDictionary.Keys.Should().OnlyContain(item => item != default); + generated.DateOnlyStringDictionary.Values.Should().OnlyContain(item => !string.IsNullOrWhiteSpace(item)); + } + + [Fact(DisplayName = "DictionaryGenerator should generate not empty Dictionary")] + public void Create_TimeOnlyStringDictionary() + { + var generated = new DataFacade().Create(); + + generated.TimeOnlyStringDictionary.Should().NotBeNull().And.NotBeEmpty(); + generated.TimeOnlyStringDictionary.Keys.Should().OnlyContain(item => item != default); + generated.TimeOnlyStringDictionary.Values.Should().OnlyContain(item => !string.IsNullOrWhiteSpace(item)); + } + + [Fact(DisplayName = "DictionaryGenerator should generate not empty Dictionary")] + public void Create_TimeSpanStringDictionary() + { + var generated = new DataFacade().Create(); + + generated.TimeSpanStringDictionary.Should().NotBeNull().And.NotBeEmpty(); + generated.TimeSpanStringDictionary.Keys.Should().OnlyContain(item => item != default); + generated.TimeSpanStringDictionary.Values.Should().OnlyContain(item => !string.IsNullOrWhiteSpace(item)); + } + + [Fact(DisplayName = "DictionaryGenerator should generate not empty Dictionary")] + public void Create_EnumCharDictionary() + { + var generated = new DataFacade().Create(); + + generated.EnumCharDictionary.Should().NotBeNull().And.NotBeEmpty(); + generated.EnumCharDictionary.Keys.Should().OnlyContain(item => item != Sample.Unknown); + generated.EnumCharDictionary.Values.Should().OnlyContain(item => item != default); + } + + [Fact(DisplayName = "DictionaryGenerator should generate not empty Dictionary with valid values")] + public void Create_StringComplexDictionary() + { + var generated = new DataFacade().Create(); + + generated.StringComplexDictionary.Should().NotBeNull().And.NotBeEmpty(); + generated.StringComplexDictionary.Keys.Should().OnlyContain(item => !string.IsNullOrWhiteSpace(item)); + generated.StringComplexDictionary.Values.Should().OnlyContain(item => item != null && !string.IsNullOrWhiteSpace(item.Name)); + } + + [Fact(DisplayName = "DictionaryGenerator should generate not empty SortedDictionary")] + public void Create_SortedStringIntDictionary() + { + var generated = new DataFacade().Create(); + + generated.SortedStringIntDictionary.Should().NotBeNull().And.NotBeEmpty(); + generated.SortedStringIntDictionary.Keys.Should().OnlyContain(item => !string.IsNullOrWhiteSpace(item)); + generated.SortedStringIntDictionary.Values.Should().OnlyContain(item => item != default); + } + + [Fact(DisplayName = "DictionaryGenerator should generate not empty ConcurrentDictionary")] + public void Create_ConcurrentStringDoubleDictionary() + { + var generated = new DataFacade().Create(); + + generated.ConcurrentStringDoubleDictionary.Should().NotBeNull().And.NotBeEmpty(); + generated.ConcurrentStringDoubleDictionary.Keys.Should().OnlyContain(item => !string.IsNullOrWhiteSpace(item)); + generated.ConcurrentStringDoubleDictionary.Values.Should().OnlyContain(item => !double.IsNaN(item) && !double.IsInfinity(item)); + } + + [Fact(DisplayName = "DictionaryGenerator should generate not empty IDictionary")] + public void Create_GuidIntDictionary() + { + var generated = new DataFacade().Create(); + + generated.GuidIntDictionary.Should().NotBeNull().And.NotBeEmpty(); + generated.GuidIntDictionary.Keys.Should().OnlyContain(item => item != Guid.Empty); + generated.GuidIntDictionary.Values.Should().OnlyContain(item => item != default); + } + + [Fact(DisplayName = "DictionaryGenerator should generate not empty IReadOnlyDictionary")] + public void Create_StringBoolDictionary() + { + var generated = new DataFacade().Create(); + + generated.StringBoolDictionary.Should().NotBeNull().And.NotBeEmpty(); + generated.StringBoolDictionary.Keys.Should().OnlyContain(item => !string.IsNullOrWhiteSpace(item)); + generated.StringBoolDictionary.Values.Should().Contain(item => item).And.Contain(item => !item); + } +} \ No newline at end of file diff --git a/Tests/Tests/EnumGeneratorTests.cs b/Tests/Tests/EnumGeneratorTests.cs new file mode 100644 index 0000000..fecab1d --- /dev/null +++ b/Tests/Tests/EnumGeneratorTests.cs @@ -0,0 +1,50 @@ +using DataGenerator; + +using FluentAssertions; + +using Tests.TestModels; +using Tests.TestModels.Common.Enums; + +using Xunit; + +namespace Tests.Tests; + +public sealed class EnumGeneratorTests +{ + [Fact(DisplayName = "EnumGenerator should never generate zero-value for enums starting with Unknown/None")] + public void Create_ShouldSkipZeroValue_WhenEnumStartsWithZero() + { + var generated = new DataFacade().Create(); + + generated.Basic.Should().NotBe(Basic.Unknown); + } + + [Fact(DisplayName = "EnumGenerator should generate valid values for enums without zero")] + public void Create_ShouldGenerate_WhenEnumHasNoZero() + { + var generated = new DataFacade().Create(); + + generated.NoZero.Should().BeOneOf(NoZero.A, NoZero.B, NoZero.C); + } + + [Fact(DisplayName = "EnumGenerator should generate any value except zero entry when zero exists but is not 'Unknown' conceptually")] + public void Create_ShouldSkipZero_WhenZeroExistsButNotMeaningful() + { + var generated = new DataFacade().Create(); + + generated.Weird.Should().BeOneOf(Weird.Ten, Weird.Minus); + } + + [Fact(DisplayName = "EnumGenerator should produce different values statistically over multiple generations")] + public void Create_ShouldProduceDiversity() + { + var facade = new DataFacade(); + + var results = Enumerable.Range(0, 50) + .Select(_ => facade.Create().Basic) + .Distinct() + .ToList(); + + results.Count.Should().BeGreaterThan(1); + } +} diff --git a/Tests/Tests/EnumerableGeneratorTests.cs b/Tests/Tests/EnumerableGeneratorTests.cs new file mode 100644 index 0000000..3eb7f63 --- /dev/null +++ b/Tests/Tests/EnumerableGeneratorTests.cs @@ -0,0 +1,176 @@ +using DataGenerator; + +using FluentAssertions; + +using Tests.TestModels; +using Tests.TestModels.Common.Enums; + +using Xunit; + +namespace Tests.Tests; + +public sealed class EnumerableGeneratorTests +{ + [Fact(DisplayName = "EnumerableGenerator should generate not empty IEnumerable")] + public void Create_IntEnumerable() + { + var generated = new DataFacade().Create(); + + generated.IntEnumerable.Should().NotBeNull().And.NotBeEmpty(); + generated.IntEnumerable.Should().OnlyContain(item => item != default); + } + + [Fact(DisplayName = "EnumerableGenerator should generate not empty IEnumerable")] + public void Create_DoubleEnumerable() + { + var generated = new DataFacade().Create(); + + generated.DoubleEnumerable.Should().NotBeNull().And.NotBeEmpty(); + generated.DoubleEnumerable.Should().OnlyContain(item => !double.IsNaN(item) && !double.IsInfinity(item)); + generated.DoubleEnumerable.Should().Contain(item => Math.Abs(item) > 0.000001); + } + + [Fact(DisplayName = "EnumerableGenerator should generate not empty IEnumerable")] + public void Create_BoolEnumerable() + { + var generated = new DataFacade().Create(); + + generated.BoolEnumerable.Should().NotBeNull().And.NotBeEmpty(); + generated.BoolEnumerable.Should().Contain(item => item).And.Contain(item => !item); + } + + [Fact(DisplayName = "EnumerableGenerator should generate not empty IEnumerable")] + public void Create_CharEnumerable() + { + var generated = new DataFacade().Create(); + + generated.CharEnumerable.Should().NotBeNull().And.NotBeEmpty(); + generated.CharEnumerable.Should().OnlyContain(item => char.IsLetter(item) || char.IsDigit(item) || char.IsSymbol(item) || char.IsPunctuation(item)); + } + + [Fact(DisplayName = "EnumerableGenerator should generate not empty IEnumerable")] + public void Create_GuidEnumerable() + { + var generated = new DataFacade().Create(); + + generated.GuidEnumerable.Should().NotBeNull().And.NotBeEmpty(); + generated.GuidEnumerable.Should().OnlyContain(item => item != Guid.Empty); + } + + [Fact(DisplayName = "EnumerableGenerator should generate not empty IEnumerable")] + public void Create_UriEnumerable() + { + var generated = new DataFacade().Create(); + + generated.UriEnumerable.Should().NotBeNull().And.NotBeEmpty(); + generated.UriEnumerable.Should().OnlyContain(item => item != null); + } + + [Fact(DisplayName = "EnumerableGenerator should generate not empty IEnumerable")] + public void Create_DateEnumerable() + { + var generated = new DataFacade().Create(); + + generated.DateEnumerable.Should().NotBeNull().And.NotBeEmpty(); + generated.DateEnumerable.Should().OnlyContain(item => item != default); + } + + [Fact(DisplayName = "EnumerableGenerator should generate not empty IEnumerable")] + public void Create_DateOnlyEnumerable() + { + var generated = new DataFacade().Create(); + + generated.DateOnlyEnumerable.Should().NotBeNull().And.NotBeEmpty(); + generated.DateOnlyEnumerable.Should().OnlyContain(item => item != default); + } + + [Fact(DisplayName = "EnumerableGenerator should generate not empty IEnumerable")] + public void Create_TimeOnlyEnumerable() + { + var generated = new DataFacade().Create(); + + generated.TimeOnlyEnumerable.Should().NotBeNull().And.NotBeEmpty(); + generated.TimeOnlyEnumerable.Should().OnlyContain(item => item != default); + } + + [Fact(DisplayName = "EnumerableGenerator should generate not empty IEnumerable")] + public void Create_TimeSpanEnumerable() + { + var generated = new DataFacade().Create(); + + generated.TimeSpanEnumerable.Should().NotBeNull().And.NotBeEmpty(); + generated.TimeSpanEnumerable.Should().OnlyContain(item => item != default); + } + + [Fact(DisplayName = "EnumerableGenerator should generate not empty IEnumerable")] + public void Create_EnumEnumerable() + { + var generated = new DataFacade().Create(); + + generated.EnumEnumerable.Should().NotBeNull().And.NotBeEmpty(); + generated.EnumEnumerable.Should().OnlyContain(item => item != Sample.Unknown); + } + + [Fact(DisplayName = "EnumerableGenerator should generate not empty IEnumerable")] + public void Create_StringEnumerable() + { + var generated = new DataFacade().Create(); + + generated.StringEnumerable.Should().NotBeNull().And.NotBeEmpty(); + generated.StringEnumerable.Should().OnlyContain(item => !string.IsNullOrWhiteSpace(item)); + } + + [Fact(DisplayName = "EnumerableGenerator should generate not empty IEnumerable with valid content")] + public void Create_ItemEnumerable() + { + var generated = new DataFacade().Create(); + + generated.ItemEnumerable.Should().NotBeNull().And.NotBeEmpty(); + generated.ItemEnumerable.Should().OnlyContain(item => item != null && !string.IsNullOrWhiteSpace(item.Name)); + } + + [Fact(DisplayName = "EnumerableGenerator should generate not empty ICollection")] + public void Create_StringCollection() + { + var generated = new DataFacade().Create(); + + generated.StringCollection.Should().NotBeNull().And.NotBeEmpty(); + generated.StringCollection.Should().OnlyContain(item => !string.IsNullOrWhiteSpace(item)); + } + + [Fact(DisplayName = "EnumerableGenerator should generate not empty IList")] + public void Create_GuidList() + { + var generated = new DataFacade().Create(); + + generated.GuidList.Should().NotBeNull().And.NotBeEmpty(); + generated.GuidList.Should().OnlyContain(item => item != Guid.Empty); + } + + [Fact(DisplayName = "EnumerableGenerator should generate not empty IReadOnlyCollection")] + public void Create_BoolReadOnlyCollection() + { + var generated = new DataFacade().Create(); + + generated.BoolReadOnlyCollection.Should().NotBeNull().And.NotBeEmpty(); + generated.BoolReadOnlyCollection.Should().OnlyContain(item => item || !item); + } + + [Fact(DisplayName = "EnumerableGenerator should generate not empty IReadOnlyList")] + public void Create_EnumReadOnlyList() + { + var generated = new DataFacade().Create(); + + generated.EnumReadOnlyList.Should().NotBeNull().And.NotBeEmpty(); + generated.EnumReadOnlyList.Should().OnlyContain(item => item != Sample.Unknown); + } + + [Fact(DisplayName = "EnumerableGenerator should generate not empty List with valid values")] + public void Create_ItemList() + { + var generated = new DataFacade().Create(); + + generated.ItemList.Should().NotBeNull().And.NotBeEmpty(); + generated.ItemList.Should().OnlyContain(item => item != null && !string.IsNullOrWhiteSpace(item.Name)); + } +} \ No newline at end of file diff --git a/Tests/Tests/GuidGeneratorTests.cs b/Tests/Tests/GuidGeneratorTests.cs new file mode 100644 index 0000000..9507eda --- /dev/null +++ b/Tests/Tests/GuidGeneratorTests.cs @@ -0,0 +1,34 @@ +using DataGenerator; + +using FluentAssertions; + +using Tests.TestModels; + +using Xunit; + +namespace Tests.Tests; + +public sealed class GuidGeneratorTests +{ + [Fact(DisplayName = "GuidGenerator should generate non-empty Guid values")] + public void Create_GeneratesNonEmptyGuids() + { + var generated = new DataFacade().Create(); + + generated.Id.Should().NotBe(Guid.Empty); + generated.ExternalId.Should().NotBe(Guid.Empty); + generated.CorrelationId.Should().NotBe(Guid.Empty); + } + + [Fact(DisplayName = "GuidGenerator should generate different Guid values across multiple samples")] + public void Create_GeneratesDifferentValues_Statistically() + { + var facade = new DataFacade(); + + var results = Enumerable.Range(0, 30) + .Select(_ => facade.Create().Id) + .ToList(); + + results.Distinct().Count().Should().BeGreaterThan(1); + } +} diff --git a/Tests/Tests/MutateTests.cs b/Tests/Tests/MutateTests.cs new file mode 100644 index 0000000..f83f749 --- /dev/null +++ b/Tests/Tests/MutateTests.cs @@ -0,0 +1,60 @@ +using DataGenerator; + +using FluentAssertions; + +using Tests.TestModels; + +using Xunit; + +namespace Tests.Tests; + +public sealed class MutateTests +{ + [Fact(DisplayName = "Mutate should change at least one property")] + public void Mutate_ChangesAtLeastOneProperty() + { + var facade = new DataFacade(); + var generated = facade.Create(); + + var mutated = facade.Mutate(generated); + + mutated.Should().NotBeEquivalentTo(generated); + } + + [Fact(DisplayName = "Mutate should not change system fields (Id, CreatedAt, UpdatedAt, DeletedAt)")] + public void Mutate_DoesNotChangeSystemFields() + { + var facade = new DataFacade(); + var generated = facade.Create(); + + var mutated = facade.Mutate(generated); + + mutated.Id.Should().Be(generated.Id); + mutated.CreatedAt.Should().Be(generated.CreatedAt); + mutated.UpdatedAt.Should().Be(generated.UpdatedAt); + mutated.DeletedAt.Should().Be(generated.DeletedAt); + } + + [Fact(DisplayName = "Mutate should respect explicitly excluded properties")] + public void Mutate_RespectsExcludeProperties() + { + var facade = new DataFacade(); + var generated = facade.Create(); + + var mutated = facade.Mutate(generated, excludeProperties: [nameof(SampleModel.Email)]); + + mutated.Email.Should().Be(generated.Email); + } + + [Fact(DisplayName = "Mutate should always change Login and Password")] + public void Mutate_AlwaysChangesLoginAndPassword() + { + var facade = new DataFacade(); + var generated = facade.Create(); + + var mutated = facade.Mutate(generated); + + mutated.Login.Should().NotBe(generated.Login); + mutated.Password.Should().NotBe(generated.Password); + } +} diff --git a/Tests/Tests/NullableTests.cs b/Tests/Tests/NullableTests.cs new file mode 100644 index 0000000..6b63c92 --- /dev/null +++ b/Tests/Tests/NullableTests.cs @@ -0,0 +1,34 @@ +using DataGenerator; + +using FluentAssertions; + +using Tests.TestModels; + +using Xunit; + +namespace Tests.Tests; + +public sealed class NullableTests +{ + [Fact(DisplayName = "Nullable fields should sometimes be null and sometimes have values")] + public void Nullable_Generation_CanBeNull_And_NotNull() + { + var facade = new DataFacade(); + + var middleNames = new List(); + var deletedDates = new List(); + + for (int i = 0; i < 10; i++) + { + var generated = facade.Create(); + middleNames.Add(generated.MiddleName); + deletedDates.Add(generated.DeletedAt); + } + + middleNames.Should().Contain(item => item == null); + middleNames.Should().Contain(item => item != null); + + deletedDates.Should().Contain(item => item == null); + deletedDates.Should().Contain(item => item != null); + } +} diff --git a/Tests/Tests/NumericGeneratorTests.cs b/Tests/Tests/NumericGeneratorTests.cs new file mode 100644 index 0000000..ad125aa --- /dev/null +++ b/Tests/Tests/NumericGeneratorTests.cs @@ -0,0 +1,63 @@ +using DataGenerator; + +using FluentAssertions; + +using Tests.TestModels; + +using Xunit; + +namespace Tests.Tests; + +public sealed class NumericGeneratorTests +{ + [Fact(DisplayName = "NumericGenerator should generate correct int values based on naming rules")] + public void Create_IntNamingPatterns() + { + var generated = new DataFacade().Create(); + + generated.Id.Should().Be(0); + generated.UserId.Should().Be(0); + + generated.Age.Should().BeInRange(18, 70); + + generated.ItemCount.Should().BeInRange(0, 1000); + generated.Qty.Should().BeInRange(0, 1000); + generated.TotalNum.Should().BeInRange(0, 1000); + + generated.Price.Should().BeInRange(1, 100_000); + generated.OrderAmount.Should().BeInRange(1, 100_000); + generated.TotalSum.Should().BeInRange(1, 100_000); + + generated.ReleaseYear.Should().BeLessThanOrEqualTo(DateTime.UtcNow.Year); + } + + [Fact(DisplayName = "NumericGenerator should generate correct numeric values for all numeric types")] + public void Create_NumericTypes() + { + var generated = new DataFacade().Create(); + + generated.UIntValue.Should().BeGreaterThanOrEqualTo(0u); + generated.LongValue.Should().BeGreaterThanOrEqualTo(0); + generated.ULongValue.Should().BeGreaterThanOrEqualTo(0ul); + generated.ShortValue.Should().BeInRange(short.MinValue, short.MaxValue); + generated.UShortValue.Should().BeInRange(ushort.MinValue, ushort.MaxValue); + generated.ByteValue.Should().BeInRange(byte.MinValue, byte.MaxValue); + generated.SByteValue.Should().BeInRange(sbyte.MinValue, sbyte.MaxValue); + + generated.DoubleValue.Should().BeGreaterThan(0).And.BeLessThanOrEqualTo(100_000); + generated.FloatValue.Should().BeGreaterThan(0).And.BeLessThanOrEqualTo(100_000); + generated.DecimalValue.Should().BeGreaterThan(0).And.BeLessThanOrEqualTo(100_000); + } + + [Fact(DisplayName = "NumericGenerator should produce statistically diverse values")] + public void Create_ShouldProduceDiversity() + { + var facade = new DataFacade(); + + var values = Enumerable.Range(0, 50) + .Select(_ => facade.Create().DoubleValue) + .ToList(); + + values.Distinct().Count().Should().BeGreaterThan(1); + } +} diff --git a/Tests/Tests/StringGeneratorTests.cs b/Tests/Tests/StringGeneratorTests.cs new file mode 100644 index 0000000..6c79ca0 --- /dev/null +++ b/Tests/Tests/StringGeneratorTests.cs @@ -0,0 +1,67 @@ +using DataGenerator; + +using FluentAssertions; + +using Tests.TestModels; + +using Xunit; + +namespace Tests.Tests; + +public sealed class StringGeneratorTests +{ + [Fact(DisplayName = "StringGenerator should generate valid values for all naming rules")] + public void Create_AllSmartStringPatterns() + { + var generated = new DataFacade().Create(); + + generated.EmailAddress.Should().Contain("@"); + + generated.Phone.Should().MatchRegex(@"\d"); + generated.MobilePhone.Should().MatchRegex(@"\d"); + + generated.FullName.Should().Contain(" "); + generated.Name.Should().Contain(" "); + generated.Fio.Should().Contain(" "); + + generated.FirstName.Should().NotBeNullOrWhiteSpace(); + generated.LastName.Should().NotBeNullOrWhiteSpace(); + generated.Surname.Should().NotBeNullOrWhiteSpace(); + + generated.Username.Should().NotContain(" "); + generated.Login.Should().NotContain(" "); + + generated.Password.Any(char.IsDigit).Should().BeTrue(); + generated.PwdHash.Any(char.IsDigit).Should().BeTrue(); + + Uri.TryCreate(generated.WebsiteUrl, UriKind.Absolute, out _).Should().BeTrue(); + Uri.TryCreate(generated.ProfileLink, UriKind.Absolute, out _).Should().BeTrue(); + + Uri.TryCreate(generated.Avatar, UriKind.Absolute, out _).Should().BeTrue(); + + generated.Country.Should().NotBeNullOrWhiteSpace(); + generated.City.Should().NotBeNullOrWhiteSpace(); + generated.StreetAddress.Should().NotBeNullOrWhiteSpace(); + + generated.Postcode.Should().NotBeNullOrWhiteSpace(); + generated.PostalCode.Should().NotBeNullOrWhiteSpace(); + generated.ZipCode.Should().NotBeNullOrWhiteSpace(); + + generated.Title.Should().NotBeNullOrWhiteSpace(); + generated.SubjectTitle.Should().NotBeNullOrWhiteSpace(); + + generated.Description.Should().NotBeNullOrWhiteSpace(); + generated.MessageText.Should().NotBeNullOrWhiteSpace(); + generated.ContentString.Should().NotBeNullOrWhiteSpace(); + + generated.Company.Should().NotBeNullOrWhiteSpace(); + + generated.Iban.Should().MatchRegex(@"[A-Z]{2}\w+"); + generated.AccountNumber.Should().MatchRegex(@"[A-Z]{2}\w+"); + + generated.Currency.Should().MatchRegex(@"^[A-Z]{3}$"); + + generated.RandomString.Should().NotBeNullOrWhiteSpace(); + generated.RandomString.Length.Should().BeInRange(8, 16); + } +} diff --git a/Tests/TestDataFacadeTests.cs b/Tests/Tests/TestDataFacadeTests.cs similarity index 63% rename from Tests/TestDataFacadeTests.cs rename to Tests/Tests/TestDataFacadeTests.cs index 2e4730d..3ce0b49 100644 --- a/Tests/TestDataFacadeTests.cs +++ b/Tests/Tests/TestDataFacadeTests.cs @@ -6,7 +6,7 @@ using Xunit; -namespace Tests; +namespace Tests.Tests; public sealed class TestDataFacadeTests { @@ -16,10 +16,10 @@ public void Create_WithSameSeed_ProducesSameData() var a = new DataFacade(seed: 123); var b = new DataFacade(seed: 123); - var user1 = a.Create(); - var user2 = b.Create(); + var generated1 = a.Create(); + var generated2 = b.Create(); - user1.Should().BeEquivalentTo(user2); + generated1.Should().BeEquivalentTo(generated2); } [Fact(DisplayName = "Different seeds should produce different results")] @@ -28,9 +28,9 @@ public void Create_WithDifferentSeed_ProducesDifferentData() var a = new DataFacade(seed: 1); var b = new DataFacade(seed: 2); - var user1 = a.Create(); - var user2 = b.Create(); + var generated1 = a.Create(); + var generated2 = b.Create(); - user1.Should().NotBeEquivalentTo(user2); + generated1.Should().NotBeEquivalentTo(generated2); } } diff --git a/Tests/Tests/UriGeneratorTests.cs b/Tests/Tests/UriGeneratorTests.cs new file mode 100644 index 0000000..6cf819f --- /dev/null +++ b/Tests/Tests/UriGeneratorTests.cs @@ -0,0 +1,26 @@ +using DataGenerator; + +using FluentAssertions; + +using Tests.TestModels; + +using Xunit; + +namespace Tests.Tests; + +public sealed class UriGeneratorTests +{ + [Fact(DisplayName = "UriGenerator should generate valid absolute URIs")] + public void Create_UriProperties() + { + var generated = new DataFacade().Create(); + + generated.WebsiteUri.Should().NotBeNull(); + generated.AvatarUri.Should().NotBeNull(); + generated.ApiEndpointUri.Should().NotBeNull(); + + Uri.IsWellFormedUriString(generated.WebsiteUri.ToString(), UriKind.Absolute).Should().BeTrue(); + Uri.IsWellFormedUriString(generated.AvatarUri.ToString(), UriKind.Absolute).Should().BeTrue(); + Uri.IsWellFormedUriString(generated.ApiEndpointUri.ToString(), UriKind.Absolute).Should().BeTrue(); + } +}