diff --git a/RoyAppMaui.Tests/Converters/TimeSpanConverterTests.cs b/RoyAppMaui.Tests/Converters/TimeSpanConverterTests.cs index f318bee..3792693 100644 --- a/RoyAppMaui.Tests/Converters/TimeSpanConverterTests.cs +++ b/RoyAppMaui.Tests/Converters/TimeSpanConverterTests.cs @@ -2,8 +2,6 @@ using CsvHelper.Configuration; using CsvHelper.TypeConversion; -using FakeItEasy; - using RoyAppMaui.Models; namespace RoyAppMaui.Converters.Tests; @@ -70,8 +68,12 @@ public void ConvertFromString_InvalidFormat_ThrowsFormatException() var context = new CsvContext(reader); A.CallTo(() => _readerRow.Context).Returns(context); - // Act & Assert - Assert.Throws(() => converter.ConvertFromString("notatime", _readerRow, _memberMapData)); + // Act + object action() => converter.ConvertFromString("notatime", _readerRow, _memberMapData); + + // Assert + var exception = Assert.Throws(action); + Assert.Contains("Invalid TimeSpan format", exception.Message); } [Theory] diff --git a/RoyAppMaui.Tests/Extensions/SleepExtensionsTests.cs b/RoyAppMaui.Tests/Extensions/SleepExtensionsTests.cs index f83eeab..59acfb5 100644 --- a/RoyAppMaui.Tests/Extensions/SleepExtensionsTests.cs +++ b/RoyAppMaui.Tests/Extensions/SleepExtensionsTests.cs @@ -5,7 +5,7 @@ public class SleepExtensionsTests { [Theory] [MemberData(nameof(SleepAverageTheoryData))] - public void GetAverage_ReturnsCorrectAverage(List sleeps, Func selector, decimal expected) + public void GetAverage_ReturnsCorrectAverage(Sleep[] sleeps, Func selector, decimal expected) { // Act var actual = sleeps.GetAverage(selector); @@ -42,27 +42,35 @@ public void GetAverage_ReturnsZero_WhenSleepsIsNull() Assert.Equal(expected, actual); } - public static TheoryData, Func, decimal> SleepAverageTheoryData => new() + public static TheoryData, decimal> SleepAverageTheoryData => new() { { - new List - { + [ new() { Bedtime = new TimeSpan(2, 0, 0) }, new() { Bedtime = new TimeSpan(3, 0, 0) } - }, + ], s => s.BedtimeAsDecimal, 2.5m }, { - new List - { + [ new() { Waketime = new TimeSpan(6, 0, 0) }, new() { Waketime = new TimeSpan(6, 0, 0) }, new() { Waketime = new TimeSpan(6, 0, 0) }, new() { Waketime = new TimeSpan(6, 0, 0) } - }, + ], s => s.WaketimeAsDecimal, 6m + }, + { + [ + new() { Waketime = new TimeSpan(2, 15, 0) }, + new() { Waketime = new TimeSpan(3, 30, 0) }, + new() { Waketime = new TimeSpan(4, 45, 0) }, + new() { Waketime = new TimeSpan(5, 50, 0) } + ], + s => s.WaketimeAsDecimal, + 4.08m } }; diff --git a/RoyAppMaui.Tests/Extensions/StringExtensionsTests.cs b/RoyAppMaui.Tests/Extensions/StringExtensionsTests.cs index 5dcf66e..e14df2a 100644 --- a/RoyAppMaui.Tests/Extensions/StringExtensionsTests.cs +++ b/RoyAppMaui.Tests/Extensions/StringExtensionsTests.cs @@ -21,8 +21,12 @@ public void ToBytes_ThrowsArgumentNullException_OnNullInput() // Arrange string? input = null; - // Act & Assert - Assert.Throws(() => input!.ToBytes()); + // Act + byte[] action() => input!.ToBytes(); + + // Assert + var exception = Assert.Throws(action); + Assert.Equal("Input string cannot be null. (Parameter 'str')", exception.Message); } [Theory] @@ -48,13 +52,19 @@ public void ToTimeSpan_ParsesVariousFormats(string timeAsString, int hour, int m } [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - [InlineData("notatime")] - public void ToTimeSpan_ThrowsFormatException_OnInvalid(string? time) + [InlineData(null, "Input string is null or empty.")] + [InlineData("", "Input string is null or empty.")] + [InlineData(" ", "Input string is null or empty.")] + [InlineData("notatime", "String 'notatime' was not recognized as a valid DateTime.")] + public void ToTimeSpan_ThrowsFormatException_OnInvalid(string? time, string expected) { - // Arrange & Act & Assert - Assert.Throws(() => time!.ToTimeSpan()); + // Arrange - See InlineData + + // Act + object? action() => time!.ToTimeSpan(); + + // Assert + var exception = Assert.Throws(action); + Assert.Equal(expected, exception.Message); } } \ No newline at end of file diff --git a/RoyAppMaui.Tests/Models/SleepTests.cs b/RoyAppMaui.Tests/Models/SleepTests.cs index 3298d65..3f66ca6 100644 --- a/RoyAppMaui.Tests/Models/SleepTests.cs +++ b/RoyAppMaui.Tests/Models/SleepTests.cs @@ -1,6 +1,4 @@ -using RoyAppMaui.Models; - -namespace RoyAppMaui.Tests.Models; +namespace RoyAppMaui.Models.Tests; public class SleepTests { [Fact] @@ -9,8 +7,11 @@ public void BedtimeRec_ReturnsDecimalHours() // Arrange var sleep = new Sleep { Bedtime = new TimeSpan(22, 30, 0) }; - // Act & Assert - Assert.Equal(22.5m, sleep.BedtimeAsDecimal); + // Act + decimal actual = sleep.BedtimeAsDecimal; + + // Assert + Assert.Equal(22.5m, actual); } [Fact] @@ -19,8 +20,11 @@ public void WaketimeRec_ReturnsDecimalHours() // Arrange var sleep = new Sleep { Waketime = new TimeSpan(6, 15, 0) }; - // Act & Assert - Assert.Equal(6.25m, sleep.WaketimeAsDecimal); + // Act + decimal actual = sleep.WaketimeAsDecimal; + + // Assert + Assert.Equal(6.25m, actual); } [Fact] @@ -29,8 +33,11 @@ public void BedtimeDisplay_ReturnsFormattedString() // Arrange var sleep = new Sleep { Bedtime = new TimeSpan(22, 0, 0) }; - // Act & Assert - Assert.Equal("10:00 PM", sleep.BedtimeDisplay); + // Act + string actual = sleep.BedtimeDisplay; + + // Assert + Assert.Equal("10:00 PM", actual); } [Fact] @@ -39,8 +46,11 @@ public void WaketimeDisplay_ReturnsFormattedString() // Arrange var sleep = new Sleep { Waketime = new TimeSpan(6, 0, 0) }; - // Act & Assert - Assert.Equal("06:00 AM", sleep.WaketimeDisplay); + // Act + string actual = sleep.WaketimeDisplay; + + // Assert + Assert.Equal("06:00 AM", actual); } [Theory] @@ -58,7 +68,10 @@ public void Duration_CalculatesCorrectly(int bedHour, int bedMin, int wakeHour, Waketime = new TimeSpan(wakeHour, wakeMin, 0) }; - // Act & Assert - Assert.Equal(expected, sleep.Duration); + // Act + decimal actual = sleep.Duration; + + // Assert + Assert.Equal(expected, actual); } } diff --git a/RoyAppMaui.Tests/Services/DataServiceTests.cs b/RoyAppMaui.Tests/Services/DataServiceTests.cs index 930c732..6444606 100644 --- a/RoyAppMaui.Tests/Services/DataServiceTests.cs +++ b/RoyAppMaui.Tests/Services/DataServiceTests.cs @@ -1,10 +1,9 @@ using RoyAppMaui.Extensions; using RoyAppMaui.Models; -using RoyAppMaui.Services.Impl; using System.Text; -namespace RoyAppMaui.Tests.Services; +namespace RoyAppMaui.Services.Impl.Tests; public class DataServiceTests { [Fact] @@ -19,9 +18,13 @@ public void GetExportData_EmptyList_ReturnsHeaderAndAverages() // Assert var actual = Encoding.UTF8.GetString(result); - Assert.Contains("Id,Bedtime,Bedtime (as decimal),Waketime,Waketime (as decimal)", actual); - Assert.Contains("Bedtime Average: 0", actual); - Assert.Contains("Waketime Average: 0", actual); + Assert.Equal(""" + Id,Bedtime,Bedtime (as decimal),Waketime,Waketime (as decimal),Duration + Bedtime Average: 0 + Waketime Average: 0 + Duration Average: 0 + + """, actual); } [Fact] @@ -42,10 +45,14 @@ public void GetExportData_SingleSleep_ReturnsCorrectCsv() // Assert var actual = Encoding.UTF8.GetString(result); - Assert.Contains("Id,Bedtime,Bedtime (as decimal),Waketime,Waketime (as decimal)", actual); - Assert.Contains("1,10:00 PM,22.00,06:00 AM,06.00", actual); - Assert.Contains("Bedtime Average: 22", actual); - Assert.Contains("Waketime Average: 6", actual); + Assert.Equal(""" + Id,Bedtime,Bedtime (as decimal),Waketime,Waketime (as decimal),Duration + 1,10:00 PM,22.00,06:00 AM,06.00,08.00 + Bedtime Average: 22 + Waketime Average: 6 + Duration Average: 8 + + """, actual); } [Fact] @@ -64,9 +71,14 @@ public void GetExportData_MultipleSleeps_ReturnsCorrectAverages() // Assert var actual = Encoding.UTF8.GetString(result); - Assert.Contains("1,10:00 PM,22.00,06:00 AM,06.00", actual); - Assert.Contains("2,11:00 PM,23.00,07:00 AM,07.00", actual); - Assert.Contains("Bedtime Average: 22.5", actual); - Assert.Contains("Waketime Average: 6.5", actual); + Assert.Equal(""" + Id,Bedtime,Bedtime (as decimal),Waketime,Waketime (as decimal),Duration + 1,10:00 PM,22.00,06:00 AM,06.00,08.00 + 2,11:00 PM,23.00,07:00 AM,07.00,08.00 + Bedtime Average: 22.5 + Waketime Average: 6.5 + Duration Average: 8 + + """, actual); } } diff --git a/RoyAppMaui.Tests/Services/FileServiceTests.cs b/RoyAppMaui.Tests/Services/FileServiceTests.cs index d9f4a16..4cd9297 100644 --- a/RoyAppMaui.Tests/Services/FileServiceTests.cs +++ b/RoyAppMaui.Tests/Services/FileServiceTests.cs @@ -1,13 +1,10 @@ using CommunityToolkit.Maui.Storage; -using FakeItEasy; - using RoyAppMaui.Extensions; -using RoyAppMaui.Services.Impl; using System.IO.Abstractions.TestingHelpers; -namespace RoyAppMaui.Services.Tests; +namespace RoyAppMaui.Services.Impl.Tests; public class FileServiceTests { [Theory] @@ -36,7 +33,7 @@ public void GetSleepDataFromCsv_ParsesValidCsv_ReturnsSleepRecords(string id, st // Assert Assert.True(result.IsSuccess); - var actual = result.Value.ToList(); + var actual = result.Value; Assert.Single(actual); Assert.Equal(id, actual[0].Id); Assert.Equal(bedtime.ToTimeSpan(), actual[0].Bedtime); @@ -58,7 +55,7 @@ public void GetSleepDataFromCsv_FileNotFound_ReturnsFailure() // Assert Assert.True(actual.IsFailed); - Assert.Contains("not found", actual.Errors[0].Message, StringComparison.OrdinalIgnoreCase); + Assert.Equal("The file at nonexistent.csv was not found.", actual.Errors[0].Message); } [Fact] @@ -76,7 +73,7 @@ public void GetSleepDataFromCsv_EmptyFilePath_ReturnsFailure() // Assert Assert.True(actual.IsFailed); - Assert.Contains("null or empty", actual.Errors[0].Message, StringComparison.OrdinalIgnoreCase); + Assert.Equal("File path is null or empty.", actual.Errors[0].Message); } [Fact] @@ -98,7 +95,23 @@ public void GetSleepDataFromCsv_MalformedCsv_ReturnsFailure() // Assert Assert.True(actual.IsFailed); - Assert.Contains("CSV parsing error", actual.Errors[0].Message, StringComparison.OrdinalIgnoreCase); + Assert.Equal(""" + CSV parsing error: Invalid TimeSpan format: 'data'. + IReader state: + ColumnCount: 3 + CurrentIndex: 1 + HeaderRecord: + + IParser state: + ByteCount: 0 + CharCount: 13 + Row: 1 + RawRow: 1 + Count: 3 + RawRecord: + bad,data,here + + """, actual.Errors[0].Message); } [Fact] @@ -117,7 +130,7 @@ public void GetSleepDataFromCsv_UnexpectedException_ReturnsFailure() // Assert Assert.True(actual.IsFailed); - Assert.Contains("unexpected error", actual.Errors[0].Message, StringComparison.OrdinalIgnoreCase); + Assert.Equal("An unexpected error occurred while reading the file: Value cannot be null. (Parameter 'reader')", actual.Errors[0].Message); } [Fact] @@ -161,7 +174,7 @@ public async Task SaveBytesToFileAsync_SaveFails_ReturnsFailure() // Assert Assert.True(actual.IsFailed); - Assert.Contains("user canceled", actual.Errors[0].Message, StringComparison.OrdinalIgnoreCase); + Assert.Equal("user canceled", actual.Errors[0].Message); } [Fact] @@ -181,7 +194,7 @@ public async Task SaveBytesToFileAsync_UnexpectedException_ReturnsFailure() // Assert Assert.True(actual.IsFailed); - Assert.Contains("token ring", actual.Errors[0].Message, StringComparison.OrdinalIgnoreCase); + Assert.Equal("File save error: Oh no, my token ring!", actual.Errors[0].Message); } [Fact] @@ -221,7 +234,7 @@ public async Task SelectImportFileAsync_UserCancels_ReturnsFailure() // Assert Assert.True(actual.IsFailed); - Assert.Contains("user canceled", actual.Errors[0].Message, StringComparison.OrdinalIgnoreCase); + Assert.Equal("user canceled", actual.Errors[0].Message); } [Fact] @@ -240,7 +253,7 @@ public async Task SelectImportFileAsync_FileDoesNotExist_ReturnsFailure() // Assert Assert.True(actual.IsFailed); - Assert.Contains("does not exist", actual.Errors[0].Message, StringComparison.OrdinalIgnoreCase); + Assert.Equal("Selected file does not exist.", actual.Errors[0].Message); } [Fact] @@ -261,6 +274,6 @@ public async Task SelectImportFileAsync_FileIsNotCsv_ReturnsFailure() // Assert Assert.True(actual.IsFailed); - Assert.Contains("not a CSV", actual.Errors[0].Message, StringComparison.OrdinalIgnoreCase); + Assert.Equal("Selected file is not a CSV file.", actual.Errors[0].Message); } } diff --git a/RoyAppMaui.Tests/Usings.cs b/RoyAppMaui.Tests/Usings.cs new file mode 100644 index 0000000..002efae --- /dev/null +++ b/RoyAppMaui.Tests/Usings.cs @@ -0,0 +1 @@ +global using FakeItEasy; diff --git a/RoyAppMaui/Extensions/SleepExtensions.cs b/RoyAppMaui/Extensions/SleepExtensions.cs index d254ef6..9afc1d3 100644 --- a/RoyAppMaui/Extensions/SleepExtensions.cs +++ b/RoyAppMaui/Extensions/SleepExtensions.cs @@ -15,22 +15,7 @@ public static class SleepExtensions /// A function to select the decimal value from each Sleep. /// The average value, rounded to 2 decimals, or 0 if the collection is null or empty. public static decimal GetAverage(this IEnumerable sleeps, Func selector) - { - if (sleeps is null) - { - return 0m; - } - - var sum = 0m; - var count = 0; - foreach (var sleep in sleeps) - { - sum += selector(sleep); - count++; - } - - return count == 0 + => sleeps is null || !sleeps.Any() ? 0m - : decimal.Round(sum / count, 2); - } + : decimal.Round(sleeps.Average(selector), 2); }