Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions RoyAppMaui.Tests/Converters/TimeSpanConverterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
using CsvHelper.Configuration;
using CsvHelper.TypeConversion;

using FakeItEasy;
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved to Usings.cs as global


using RoyAppMaui.Models;

namespace RoyAppMaui.Converters.Tests;
Expand Down Expand Up @@ -70,8 +68,12 @@ public void ConvertFromString_InvalidFormat_ThrowsFormatException()
var context = new CsvContext(reader);
A.CallTo(() => _readerRow.Context).Returns(context);

// Act & Assert
Assert.Throws<TypeConverterException>(() => converter.ConvertFromString("notatime", _readerRow, _memberMapData));
// Act
object action() => converter.ConvertFromString("notatime", _readerRow, _memberMapData);

// Assert
var exception = Assert.Throws<TypeConverterException>(action);
Assert.Contains("Invalid TimeSpan format", exception.Message);
Comment on lines -73 to +76
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exceptions assertions are awkward, but they can be separated from the Act portion by creating a local function, which I typically call action (Act => Action). This effectively treats your function you want to test as a variable, but doesn't invoke it yet.

The Assert.Throws does the invocation.

In addition, Assert.Throws return the exception itself, which you can further test, usually for the error message contents, which is usually a good idea to see this.

}

[Theory]
Expand Down
24 changes: 16 additions & 8 deletions RoyAppMaui.Tests/Extensions/SleepExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ public class SleepExtensionsTests
{
[Theory]
[MemberData(nameof(SleepAverageTheoryData))]
public void GetAverage_ReturnsCorrectAverage(List<Sleep> sleeps, Func<Sleep, decimal> selector, decimal expected)
public void GetAverage_ReturnsCorrectAverage(Sleep[] sleeps, Func<Sleep, decimal> selector, decimal expected)
{
// Act
var actual = sleeps.GetAverage(selector);
Expand Down Expand Up @@ -42,27 +42,35 @@ public void GetAverage_ReturnsZero_WhenSleepsIsNull()
Assert.Equal(expected, actual);
}

public static TheoryData<List<Sleep>, Func<Sleep, decimal>, decimal> SleepAverageTheoryData => new()
public static TheoryData<Sleep[], Func<Sleep, decimal>, decimal> SleepAverageTheoryData => new()
{
{
new List<Sleep>
{
[
new() { Bedtime = new TimeSpan(2, 0, 0) },
new() { Bedtime = new TimeSpan(3, 0, 0) }
},
],
Comment on lines -48 to +51
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because this is static, instead of dynamic data, we can declare this data as an array.

It's important to keep in mind that you only need a List if you are going to use its Add (or rarely, its Remove).
Arrays are a set size in memory. In the test, we have no plans to grow nor shrink the contents, so it's more efficient to use arrays.

s => s.BedtimeAsDecimal,
2.5m
},
{
new List<Sleep>
{
[
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
}
};

Expand Down
28 changes: 19 additions & 9 deletions RoyAppMaui.Tests/Extensions/StringExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@ public void ToBytes_ThrowsArgumentNullException_OnNullInput()
// Arrange
string? input = null;

// Act & Assert
Assert.Throws<ArgumentNullException>(() => input!.ToBytes());
// Act
byte[] action() => input!.ToBytes();

// Assert
var exception = Assert.Throws<ArgumentNullException>(action);
Assert.Equal("Input string cannot be null. (Parameter 'str')", exception.Message);
}

[Theory]
Expand All @@ -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.")]
Comment on lines +55 to +58
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a perfect example of why I like to see the Exception message, because occasionally you'll get variation.

public void ToTimeSpan_ThrowsFormatException_OnInvalid(string? time, string expected)
{
// Arrange & Act & Assert
Assert.Throws<FormatException>(() => time!.ToTimeSpan());
// Arrange - See InlineData

// Act
object? action() => time!.ToTimeSpan();

// Assert
var exception = Assert.Throws<FormatException>(action);
Assert.Equal(expected, exception.Message);
}
}
39 changes: 26 additions & 13 deletions RoyAppMaui.Tests/Models/SleepTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using RoyAppMaui.Models;

namespace RoyAppMaui.Tests.Models;
namespace RoyAppMaui.Models.Tests;
Comment on lines -3 to +1
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you were mostly good about this in the test classes but missed this in a few of them. It's understandable because, by default Visual Studio puts Tests in the middle. However, to take advantage of the inheritance on the using, we move the .Tests to the end. I like to do it to ensure I'm targeting the correct class.

public class SleepTests
{
[Fact]
Expand All @@ -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);
Comment on lines -12 to +14
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it's extra lines and this is nitpicky, but I always strive to keep my Act and Assert separate. I want it to be very obvious the method (or in this case Property) being tested.

}

[Fact]
Expand All @@ -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]
Expand All @@ -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]
Expand All @@ -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]
Expand All @@ -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);
}
}
38 changes: 25 additions & 13 deletions RoyAppMaui.Tests/Services/DataServiceTests.cs
Original file line number Diff line number Diff line change
@@ -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]
Expand All @@ -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);
Comment on lines -22 to +27
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used to do Contains a lot as well, but once raw string came out, as long as the string isn't excessively long (100s of lines), I prefer to see all of it.

With Contains, I have found that changes will happen, over time, as you make improvements, and you may not catch, right away, that you've broken your output, because Contains only verifies at a slice.

This, above, now verifies the whole thing.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting 🤔

}

[Fact]
Expand All @@ -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]
Expand All @@ -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);
}
}
41 changes: 27 additions & 14 deletions RoyAppMaui.Tests/Services/FileServiceTests.cs
Original file line number Diff line number Diff line change
@@ -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]
Expand Down Expand Up @@ -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;
Comment on lines -39 to +36
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Value was already a List so you didn't need to cast it.
FluentResults uses generics so these types of castings aren't needed.

Assert.Single(actual);
Assert.Equal(id, actual[0].Id);
Assert.Equal(bedtime.ToTimeSpan(), actual[0].Bedtime);
Expand All @@ -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]
Expand All @@ -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]
Expand All @@ -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]
Expand All @@ -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]
Expand Down Expand Up @@ -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]
Expand All @@ -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]
Expand Down Expand Up @@ -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]
Expand All @@ -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]
Expand All @@ -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);
}
}
1 change: 1 addition & 0 deletions RoyAppMaui.Tests/Usings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
global using FakeItEasy;
Loading