Skip to content

Refactor and enhance test code for clarity and consistency#13

Merged
jdubar merged 4 commits intojdubar:masterfrom
bechtd:master
Nov 10, 2025
Merged

Refactor and enhance test code for clarity and consistency#13
jdubar merged 4 commits intojdubar:masterfrom
bechtd:master

Conversation

@bechtd
Copy link
Copy Markdown
Collaborator

@bechtd bechtd commented Nov 9, 2025

Refactor and enhance test code for clarity and consistency

  • Refactored exception assertions for better readability.
  • Updated test data structures to use arrays instead of lists, as test data is static.
  • Improved test assertions with precise validation and detailed messages.
  • Adjusted test namespaces to align with namespace of class tested.
  • Separated Act & Assert sections for adherence to the AAA pattern.
  • Added global using for FakeItEasy to simplify imports.

bechtd and others added 2 commits November 9, 2025 02:33
- Refactored exception assertions for better readability.
- Updated test data structures to use arrays instead of lists, as test data is static.
- Improved test assertions with precise validation and detailed messages.
- Adjusted test namespaces to align with namespace of class tested.
- Separated Act & Assert sections for adherence to the AAA pattern.
- Added global using for `FakeItEasy` to simplify imports.
Refactor and enhance test code for clarity and consistency
Copy link
Copy Markdown
Collaborator Author

@bechtd bechtd left a comment

Choose a reason for hiding this comment

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

Explanation of suggestions for changes for the Test files.

Overall, the tests look good.

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

Comment on lines -73 to +76
// 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);
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.

Comment on lines -48 to +51
new List<Sleep>
{
[
new() { Bedtime = new TimeSpan(2, 0, 0) },
new() { Bedtime = new TimeSpan(3, 0, 0) }
},
],
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.

Comment on lines +55 to +58
[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.")]
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.

Comment on lines -3 to +1
namespace RoyAppMaui.Tests.Models;
namespace RoyAppMaui.Models.Tests;
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.

Comment on lines -12 to +14
// Act & Assert
Assert.Equal(22.5m, sleep.BedtimeAsDecimal);
// Act
decimal actual = sleep.BedtimeAsDecimal;

// Assert
Assert.Equal(22.5m, actual);
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.

Comment on lines -22 to +27
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);
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 🤔

Comment on lines -39 to +36
var actual = result.Value.ToList();
var actual = result.Value;
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.

Refactored the calculation logic in `SleepExtensions` to use LINQ methods (`Sum` and `Count`) for improved readability and efficiency. Replaced the manual `foreach` loop with a more concise implementation.

Added a new test case in `SleepExtensionsTests` to validate the updated logic. The test includes multiple `Waketime` values and expects the result to be `4.08m` when using the `WaketimeAsDecimal` selector.
- Simplified the implementation of the `GetAverage` method in the
`SleepExtensions` class by replacing explicit null and empty
checks with a single condition using `sleeps is null ||
!sleeps.Any()`.
- Replaced manual average calculation using
`Sum` and `Count` with the `Average` method for improved
readability.
- Retained rounding logic to ensure results are
rounded to 2 decimal places.
Copy link
Copy Markdown
Owner

@jdubar jdubar left a comment

Choose a reason for hiding this comment

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

Thanks Dave! Appreciate the review!

Comment on lines -22 to +27
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);
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 🤔

@codecov-commenter
Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
RoyAppMaui/Extensions/SleepExtensions.cs 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment on lines -18 to +20
{
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);
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.

sorry about this change.

this was caused by the difficulty with using a fork.
I meant for this to be in a follow-up PR ... BUT ... here it is.

I added an extra test (wherein the average would gone beyond 2 decimal places), but this should still be equivalent to what you had, BUT use the LINQ (functional programming) library to skip "doing it manual," and getting more comfortable with the Functional programming ways of thinking:

  1. Map (Select in C#)
  2. Filter (Where in C#)
  3. Fold (AKA Reduce, Aggregate in C# ... though Count, Sum, and Average or subsets/specialists of Aggregate)

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.

  1. Count is aggregating, over a foreach, increase a count by 1
  2. Sum is aggregate, over a foreach, adding on the item at the index to the current value
  3. Average is aggregate, over a foreach, wherein it is doing both Sum and Count, and then dividing

under the surface, this is doing, arguably, the same exact thing you are doing ... but these functional methods are tried and true, reliable. Once you get used to them, you write less code, and arguably have less to test ... less chance of a silly bug (loops being a common dev "dropping the ball" situation).

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.

AND, every major language has these (Map, Filter, Fold ... usually by those exact words), so once you get used to the concept, you can pick up other languages faster

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.

for example, I recall the first time being shown JavaScript's "lodash" library, and it suddenly hitting me "oh, this is like LINQ!" and then the greater realization hit me "RIGHT! because this is functional programming!" And suddenly I went from confused by it, to "I love this!," and I started finding and fixing existing code in our codebase that wasn't using lodash, but should have.

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.

it's because so much of our work tends to revolve around lists (arrays). And we are doing some form of transformation on the list. Projecting it onto something else (Map), or shrinking the size of the result set (Filter), or converting the "vector" (the list/array/dimension) down to a single "scalar" value (Fold/Reduce).

Once you get used to it, there's an elegance/simplicity to it.

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.

Love this - I really appreciate the details.

As soon as we started discussing your suggestion, I totally got it. I love seeing "the long version" of code be transformed into effectively shorthand and work EXACTLY THE SAME. So awesome.

@jdubar jdubar added refactor cleanup and improve code coverage added or modified coverage tests labels Nov 10, 2025
@jdubar jdubar merged commit 751c67b into jdubar:master Nov 10, 2025
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

coverage added or modified coverage tests refactor cleanup and improve code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants