From afcb91cf4fc6e9493a1d9f559784381e8a70111a Mon Sep 17 00:00:00 2001 From: Breno RdV Date: Sun, 23 Jun 2024 17:04:14 -0400 Subject: [PATCH] Added more tests + removed dead code. --- .../ExtensionMethods/ContainerExtensions.cs | 38 ---- .../Converters/EntityConverterTests.cs | 88 ++++++++ .../Entities/GlucoseReadingTests.cs | 22 ++ .../Converters/EntityConverter.cs | 3 +- .../Exceptions/NightScoutException.cs | 5 +- .../ExtensionMethods/DictionaryExtensions.cs | 12 - Raccoon.Ninja.TestHelpers/Generators.cs | 205 +++++++++++++++--- Raccoon.Ninja.TestHelpers/TestUtils.cs | 8 + 8 files changed, 293 insertions(+), 88 deletions(-) delete mode 100644 Raccoon.Ninja.AzFn.ScheduledTasks/ExtensionMethods/ContainerExtensions.cs create mode 100644 Raccoon.Ninja.Domain.Core.Tests/Converters/EntityConverterTests.cs create mode 100644 Raccoon.Ninja.Domain.Core.Tests/Entities/GlucoseReadingTests.cs delete mode 100644 Raccoon.Ninja.Domain.Core/ExtensionMethods/DictionaryExtensions.cs diff --git a/Raccoon.Ninja.AzFn.ScheduledTasks/ExtensionMethods/ContainerExtensions.cs b/Raccoon.Ninja.AzFn.ScheduledTasks/ExtensionMethods/ContainerExtensions.cs deleted file mode 100644 index 78796c3..0000000 --- a/Raccoon.Ninja.AzFn.ScheduledTasks/ExtensionMethods/ContainerExtensions.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Azure.Cosmos; -using Raccoon.Ninja.Domain.Core.Entities; -using Raccoon.Ninja.Domain.Core.Enums; - -namespace Raccoon.Ninja.AzFn.ScheduledTasks.ExtensionMethods; - -public static class ContainerExtensions -{ - public static async Task> GetLatestAggregationDataPointsAsync( - this Container container) - { - var result = new Dictionary(); - foreach (var docType in GetAllAggregationDocTypes()) - { - var query = new QueryDefinition( - "SELECT TOP 1 * FROM c WHERE c.docType = @docType ORDER BY c.createdAt DESC") - .WithParameter("@docType", (int)docType); - var iterator = container.GetItemQueryIterator(query); - var fetched = await iterator.ReadNextAsync(); - var doc = fetched.FirstOrDefault(); - if (doc is null) continue; - result.Add(docType, doc); - } - - return result; - } - - private static DocumentType[] GetAllAggregationDocTypes() - { - var values = (DocumentType[])Enum.GetValues(typeof(DocumentType)); - var result = values.Skip(1).ToArray(); - return result; - } -} \ No newline at end of file diff --git a/Raccoon.Ninja.Domain.Core.Tests/Converters/EntityConverterTests.cs b/Raccoon.Ninja.Domain.Core.Tests/Converters/EntityConverterTests.cs new file mode 100644 index 0000000..073a1b6 --- /dev/null +++ b/Raccoon.Ninja.Domain.Core.Tests/Converters/EntityConverterTests.cs @@ -0,0 +1,88 @@ +using Raccoon.Ninja.Domain.Core.Calculators; +using Raccoon.Ninja.Domain.Core.Converters; +using Raccoon.Ninja.Domain.Core.Entities.StatisticalDataPoint; +using Raccoon.Ninja.Domain.Core.Enums; +using Raccoon.Ninja.TestHelpers; + +namespace Raccoon.Ninja.Domain.Core.Tests.Converters; + +public class EntityConverterTests +{ + private static readonly DateOnly ReferenceDate = DateOnly.FromDateTime(DateTime.UtcNow); + private static readonly StatisticalDataPoint PreviousCalculation = Generators.StatisticalDataPointMockSingle(); + + [Fact] + public void ToStatisticDataPoint_ShouldReturnNull_WhenInputIsNull() + { + //Arrange + CalculationData input = null; + + //Act + var actual = EntityConverter.ToStatisticDataPoint(input, ReferenceDate, PreviousCalculation); + + //Assert + actual.Should().BeNull(); + } + + [Fact] + public void ToStatisticDataPoint_ShouldReturnError_WhenInputHasFailedStatus() + { + //Arrange + const string failedStep = "Some step"; + const string failedMessage = "Some message"; + + var input = new CalculationData + { + Status = new CalculationDataStatus + { + Success = false, + FailedAtStep = failedStep, + Message = failedMessage + } + }; + + //Act + var actual = EntityConverter.ToStatisticDataPoint(input, ReferenceDate, PreviousCalculation); + + //Assert + actual.Should().NotBeNull(); + actual.Status.Should().Be(StatisticalDataPointDocStatus.Error); + actual.Error.FailedStep.Should().Be(failedStep); + actual.Error.ErrorMessage.Should().Be(failedMessage); + } + + [Fact] + public void ToStatisticDataPoint_ShouldReturnConverted_WhenInputIsSuccess() + { + //Arrange + var glucoseValues = Generators.ListWithFloats(30).ToList(); + var input = Generators.CalculationDataMockSingle(glucoseValues); + + //Act + var actual = EntityConverter.ToStatisticDataPoint(input, ReferenceDate, PreviousCalculation); + + //Assert + actual.Should().NotBeNull(); + actual.Status.Should().Be(StatisticalDataPointDocStatus.Success); + actual.ReferenceDate.Should().Be(ReferenceDate); + actual.Average.Value.Should().Be(input.Average); + actual.Median.Value.Should().Be(input.Median); + actual.Min.Value.Should().Be(input.Min); + actual.Max.Value.Should().Be(input.Max); + actual.Mage.Threshold10.Value.Should().Be(input.Mage.Threshold10.Value); + actual.Mage.Threshold20.Value.Should().Be(input.Mage.Threshold20.Value); + actual.Mage.Absolute.Value.Should().Be(input.Mage.Absolute.Value); + actual.StandardDeviation.Value.Should().Be(input.StandardDeviation); + actual.CoefficientOfVariation.Value.Should().Be(input.CoefficientOfVariation); + actual.HbA1C.Value.Should().Be(input.CurrentHbA1C.Value); + actual.TimeInRange.Low.Value.Should().Be(input.TimeInRange.Low); + actual.TimeInRange.Normal.Value.Should().Be(input.TimeInRange.Normal); + actual.TimeInRange.High.Value.Should().Be(input.TimeInRange.High); + actual.TimeInRange.VeryHigh.Value.Should().Be(input.TimeInRange.VeryHigh); + actual.Percentile.P10.Value.Should().Be(input.Percentile.P10); + actual.Percentile.P25.Value.Should().Be(input.Percentile.P25); + actual.Percentile.P75.Value.Should().Be(input.Percentile.P75); + actual.Percentile.P90.Value.Should().Be(input.Percentile.P90); + actual.Percentile.Iqr.Value.Should().Be(input.Percentile.Iqr); + } +} \ No newline at end of file diff --git a/Raccoon.Ninja.Domain.Core.Tests/Entities/GlucoseReadingTests.cs b/Raccoon.Ninja.Domain.Core.Tests/Entities/GlucoseReadingTests.cs new file mode 100644 index 0000000..4a0417b --- /dev/null +++ b/Raccoon.Ninja.Domain.Core.Tests/Entities/GlucoseReadingTests.cs @@ -0,0 +1,22 @@ +using Raccoon.Ninja.Domain.Core.Entities; +using Raccoon.Ninja.Domain.Core.Enums; + +namespace Raccoon.Ninja.Domain.Core.Tests.Entities; + +public class GlucoseReadingTests +{ + [Fact] + public void Ctor_WhenInstantiated_ShouldHaveDefaultPropertyValues() + { + //Arrange + var expectedTrend = Trend.TripleUp; + const long expectedReadTimestampUtc = 0; + + //Act + var sut = new GlucoseReading(); + + //Assert + sut.Trend.Should().Be(expectedTrend); + sut.ReadTimestampUtc.Should().Be(expectedReadTimestampUtc); + } +} \ No newline at end of file diff --git a/Raccoon.Ninja.Domain.Core/Converters/EntityConverter.cs b/Raccoon.Ninja.Domain.Core/Converters/EntityConverter.cs index 748a187..ec289ee 100644 --- a/Raccoon.Ninja.Domain.Core/Converters/EntityConverter.cs +++ b/Raccoon.Ninja.Domain.Core/Converters/EntityConverter.cs @@ -11,7 +11,8 @@ public static StatisticalDataPoint ToStatisticDataPoint( DateOnly referenceDate, StatisticalDataPoint previousCalculations) { - ArgumentNullException.ThrowIfNull(from); + if (from is null) + return null; if (!from.Status.Success) return StatisticalDataPoint.FromError(referenceDate, from.Status.FailedAtStep, from.Status.Message); diff --git a/Raccoon.Ninja.Domain.Core/Exceptions/NightScoutException.cs b/Raccoon.Ninja.Domain.Core/Exceptions/NightScoutException.cs index d6793cb..b38b343 100644 --- a/Raccoon.Ninja.Domain.Core/Exceptions/NightScoutException.cs +++ b/Raccoon.Ninja.Domain.Core/Exceptions/NightScoutException.cs @@ -1,5 +1,8 @@ -namespace Raccoon.Ninja.Domain.Core.Exceptions; +using System.Diagnostics.CodeAnalysis; +namespace Raccoon.Ninja.Domain.Core.Exceptions; + +[ExcludeFromCodeCoverage] public class NightScoutException : Exception { public NightScoutException() diff --git a/Raccoon.Ninja.Domain.Core/ExtensionMethods/DictionaryExtensions.cs b/Raccoon.Ninja.Domain.Core/ExtensionMethods/DictionaryExtensions.cs deleted file mode 100644 index 3534be5..0000000 --- a/Raccoon.Ninja.Domain.Core/ExtensionMethods/DictionaryExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Raccoon.Ninja.Domain.Core.Entities; -using Raccoon.Ninja.Domain.Core.Enums; - -namespace Raccoon.Ninja.Domain.Core.ExtensionMethods; - -public static class DictionaryExtensions -{ - public static float? CalculateDelta(this Dictionary previousCalculations, DocumentType type, float currentValue) - { - return currentValue - previousCalculations.GetValueOrDefault(type)?.Value; - } -} \ No newline at end of file diff --git a/Raccoon.Ninja.TestHelpers/Generators.cs b/Raccoon.Ninja.TestHelpers/Generators.cs index 5c867ed..99fc5a6 100644 --- a/Raccoon.Ninja.TestHelpers/Generators.cs +++ b/Raccoon.Ninja.TestHelpers/Generators.cs @@ -2,6 +2,7 @@ using MongoDB.Bson; using Raccoon.Ninja.Domain.Core.Calculators; using Raccoon.Ninja.Domain.Core.Entities; +using Raccoon.Ninja.Domain.Core.Entities.StatisticalDataPoint; using Raccoon.Ninja.Domain.Core.Enums; using Raccoon.Ninja.Domain.Core.Models; using Raccoon.Ninja.Extensions.MongoDb.Models; @@ -17,17 +18,18 @@ public enum GeneratorSpikeDirection public static class Generators { /// - /// This will generate a list of floats representing a blood sugar (glucose) value. - /// The values will go from min to max several times until the quantity of numbers generated - /// reach qty. - /// This will represent a timeline where the blood sugar of a person was spiking. + /// This will generate a list of floats representing a blood sugar (glucose) value. + /// The values will go from min to max several times until the quantity of numbers generated + /// reach qty. + /// This will represent a timeline where the blood sugar of a person was spiking. /// /// /// /// /// /// - public static IEnumerable GlucoseTimelineWithSpikes(int qty, float min=30, float max=350, int numberOfSpikes = 10, GeneratorSpikeDirection spikeDirection=GeneratorSpikeDirection.Up) + public static IEnumerable GlucoseTimelineWithSpikes(int qty, float min = 30, float max = 350, + int numberOfSpikes = 10, GeneratorSpikeDirection spikeDirection = GeneratorSpikeDirection.Up) { if (spikeDirection == GeneratorSpikeDirection.Up) { @@ -38,10 +40,7 @@ public static IEnumerable GlucoseTimelineWithSpikes(int qty, float min=30 { yield return currentValue; currentValue += increment; - if (currentValue >= max) - { - currentValue = min; - } + if (currentValue >= max) currentValue = min; } } else if (spikeDirection == GeneratorSpikeDirection.Down) @@ -53,40 +52,85 @@ public static IEnumerable GlucoseTimelineWithSpikes(int qty, float min=30 { yield return currentValue; currentValue -= decrement; - if (currentValue < min) - { - currentValue = max; - } + if (currentValue < min) currentValue = max; } } - } - public static IEnumerable ListWithFloats(int qty, float? exactValue = null, float? minValue = null, float? maxValue = null) + + public static IEnumerable ListWithFloats(int qty, float? exactValue = null, float? minValue = null, + float? maxValue = null) { var faker = new Faker(); - for (var i = 0; i < qty; i++) - { - yield return exactValue ?? faker.Random.Float(minValue ?? 0f, maxValue ?? 100f); - } + for (var i = 0; i < qty; i++) yield return exactValue ?? faker.Random.Float(minValue ?? 0f, maxValue ?? 100f); } - public static IEnumerable ListWithIntegers(int qty, int? exactValue = null, int? minValue = null, int? maxValue = null) + public static IEnumerable ListWithIntegers(int qty, int? exactValue = null, int? minValue = null, + int? maxValue = null) { var faker = new Faker(); - for (var i = 0; i < qty; i++) - { - yield return exactValue ?? faker.Random.Int(minValue ?? 0, maxValue ?? 100); - } + for (var i = 0; i < qty; i++) yield return exactValue ?? faker.Random.Int(minValue ?? 0, maxValue ?? 100); } - public static CalculationData CalculationDataMockSingle(IList glucoseValues) + public static CalculationData CalculationDataMockSingle(IList glucoseValues, bool success = true, string failedAtStep = "Step 42", string errorMessage = "Error in step 42") { + //TODO: Improve this. return new CalculationData { GlucoseValues = glucoseValues, - Average = glucoseValues is null || glucoseValues.Count == 0? 0f : glucoseValues.Average() + Average = glucoseValues is null || glucoseValues.Count == 0 ? 0f : glucoseValues.Average(), + Median = glucoseValues is null || glucoseValues.Count == 0 ? 0f : TestUtils.CalculateMedian(glucoseValues), + Min = glucoseValues is null || glucoseValues.Count == 0 ? 0f : glucoseValues.Min(), + Max = glucoseValues is null || glucoseValues.Count == 0 ? 0f : glucoseValues.Max(), + StandardDeviation = glucoseValues is null || glucoseValues.Count == 0 + ? 0f + : TestUtils.CalculateStandardDeviation(glucoseValues), + CoefficientOfVariation = glucoseValues is null || glucoseValues.Count == 0 ? 0f : 0.1f, + Mage = new CalculationDataMage + { + Threshold10 = new CalculationDataMageResult + { + Value = 0.1f, + ExcursionsDetected = true + }, + Threshold20 = new CalculationDataMageResult + { + Value = 0.2f, + ExcursionsDetected = true + }, + Absolute = new CalculationDataMageResult + { + Value = 0.3f, + ExcursionsDetected = true + } + }, + CurrentHbA1C = new CalculationDataHbA1C + { + Value = 5.5f, + Status = HbA1CCalculationStatus.Complete + }, + TimeInRange = new CalculationDataTimeInRange + { + Low = 0.1f, + Normal = 0.2f, + High = 0.3f, + VeryHigh = 0.4f + }, + Percentile = new CalculationDataPercentile + { + P10 = 0.1f, + P25 = 0.2f, + P75 = 0.3f, + P90 = 0.4f, + Iqr = 0.5f + }, + Status = new CalculationDataStatus + { + Success = success, + FailedAtStep = failedAtStep, + Message = errorMessage + } }; } @@ -107,12 +151,12 @@ public static IList NightScoutMongoDocumentMockList(int return faker.Generate(qty).OrderBy(doc => doc.ReadingTimestamp).ToList(); } - + public static IList GlucoseReadingMockList(int qty, float? value = null) { var faker = new Faker() .RuleFor(x => x.Id, f => f.Random.Guid().ToString()) - .RuleFor(x => x.Value, f => value ?? f.Random.Number(60, 399)) + .RuleFor(x => x.Value, f => value ?? f.Random.Number(60, 399)) .RuleFor(x => x.Trend, f => f.Random.Enum()) .RuleFor(x => x.ReadTimestampUtc, f => f.Date.Past().ToUnixTimestamp()); @@ -124,7 +168,7 @@ public static GlucoseReading GlucoseReadingMockSingle(float? value = null) return GlucoseReadingMockList(1, value)[0]; } - public static IList HbA1CCalculationResponseMockList(int qty, float? value = null, + public static IList HbA1CCalculationResponseMockList(int qty, float? value = null, float? delta = null, HbA1CCalculationStatus? status = null, string error = null) { var faker = new Faker() @@ -145,8 +189,8 @@ public static HbA1CCalculationResponse HbA1CCalculationResponseMockSingle(float? { return HbA1CCalculationResponseMockList(1, value, delta, status, error)[0]; } - - public static IList HbA1CCalculationMockList(int qty, HbA1CCalculationStatus? status = null, + + public static IList HbA1CCalculationMockList(int qty, HbA1CCalculationStatus? status = null, string error = null) { var faker = new Faker() @@ -158,21 +202,110 @@ public static IList HbA1CCalculationMockList(int qty, HbA1 return faker.Generate(qty); } - - public static AggregationDataPoint HbA1CCalculationMockSingle(HbA1CCalculationStatus? status = null, + + public static AggregationDataPoint HbA1CCalculationMockSingle(HbA1CCalculationStatus? status = null, string error = null) { return HbA1CCalculationMockList(1, status, error)[0]; } - - private static HbA1CCalculationStatus GetRandomHbA1CCalculationStatusWithinReason(Faker f, HbA1CCalculationStatus? inputStatus, + + private static HbA1CCalculationStatus GetRandomHbA1CCalculationStatusWithinReason(Faker f, + HbA1CCalculationStatus? inputStatus, string error) { if (inputStatus.HasValue) return inputStatus.Value; - return string.IsNullOrWhiteSpace(error) + return string.IsNullOrWhiteSpace(error) ? f.Random.Enum() : HbA1CCalculationStatus.Error; } + + public static StatisticSimpleFloatValue StatisticSimpleFloatValueMockSingle(float? value = null, + float? previousValue = null) + { + var faker = new Faker(); + return new StatisticSimpleFloatValue + { + Value = value ?? faker.Random.Float(0, 100), + Delta = value - previousValue + }; + } + + public static StatisticMageValue StatisticMageValueMockSingle( + bool t10Excursions = true, + bool t20Excursions = true, + bool absoluteExcursions = true) + { + return new StatisticMageValue + { + Threshold10 = StatisticMageValueResult.FromSimpleFloatValue( + StatisticSimpleFloatValueMockSingle(), + t10Excursions), + Threshold20 = StatisticMageValueResult.FromSimpleFloatValue( + StatisticSimpleFloatValueMockSingle(), + t20Excursions), + Absolute = StatisticMageValueResult.FromSimpleFloatValue( + StatisticSimpleFloatValueMockSingle(), + absoluteExcursions) + }; + } + + public static StatisticHbA1CValue StatisticHbA1CValueMockSingle(float? value = null, + float? delta = null, + HbA1CCalculationStatus? status = null) + { + return new StatisticHbA1CValue + { + Value = value ?? new Faker().Random.Float(42, 300), + Delta = delta ?? new Faker().Random.Float(-1, 2), + Status = status ?? HbA1CCalculationStatus.Complete + }; + } + + public static CalculationDataHbA1C CalculationDataHbA1CMockSingle(float? value = null, + HbA1CCalculationStatus? status = null) + { + return new CalculationDataHbA1C + { + Value = value ?? new Faker().Random.Float(42, 300), + Status = status ?? HbA1CCalculationStatus.Complete + }; + } + + public static StatisticalDataPoint StatisticalDataPointMockSingle( + DateOnly? referenceDate = null, + StatisticalDataPointDocStatus status = StatisticalDataPointDocStatus.Success) + { + var faker = new Faker(); + + return new StatisticalDataPoint + { + ReferenceDate = referenceDate ?? DateOnly.FromDateTime(faker.Date.Past()), + Status = status, + Average = StatisticSimpleFloatValueMockSingle(), + Median = StatisticSimpleFloatValueMockSingle(), + Min = StatisticSimpleFloatValueMockSingle(), + Max = StatisticSimpleFloatValueMockSingle(), + StandardDeviation = StatisticSimpleFloatValueMockSingle(), + CoefficientOfVariation = StatisticSimpleFloatValueMockSingle(), + Mage = StatisticMageValueMockSingle(), + HbA1C = StatisticHbA1CValueMockSingle(), + TimeInRange = new StatisticTimeInRangeValue + { + Low = StatisticSimpleFloatValueMockSingle(), + Normal = StatisticSimpleFloatValueMockSingle(), + High = StatisticSimpleFloatValueMockSingle(), + VeryHigh = StatisticSimpleFloatValueMockSingle() + }, + Percentile = new StatisticPercentileValue + { + P10 = StatisticSimpleFloatValueMockSingle(), + P25 = StatisticSimpleFloatValueMockSingle(), + P75 = StatisticSimpleFloatValueMockSingle(), + P90 = StatisticSimpleFloatValueMockSingle(), + Iqr = StatisticSimpleFloatValueMockSingle() + } + }; + } } \ No newline at end of file diff --git a/Raccoon.Ninja.TestHelpers/TestUtils.cs b/Raccoon.Ninja.TestHelpers/TestUtils.cs index 53b21e1..dd9ece7 100644 --- a/Raccoon.Ninja.TestHelpers/TestUtils.cs +++ b/Raccoon.Ninja.TestHelpers/TestUtils.cs @@ -14,4 +14,12 @@ public static float CalculateStandardDeviation(IList glucoseValues) var standardDeviation = (float)Math.Sqrt(glucoseValues.Sum(r => Math.Pow(r - average, 2)) / glucoseValues.Count); return standardDeviation; } + + public static float CalculateMedian(IList glucoseValues) + { + var count = glucoseValues.Count; + var sortedList = glucoseValues.OrderBy(v => v).ToList(); + var mid = count / 2; + return count % 2 != 0 ? sortedList[mid] : (sortedList[mid - 1] + sortedList[mid]) / 2; + } } \ No newline at end of file