I have the record
type below.
public record CustomFormatGroupItem(string GroupName, string CfName, string CfAnchor);
I have a method that returns a collection of these, followed by two test assertions:
ICollection<CustomFormatGroupItem> result = sut.Parse();
result.Select(x => x.GroupName).Distinct()
.Should().BeEquivalentTo(
"Audio Advanced #1",
"Audio Advanced #2",
"Movie Versions",
"Unwanted"
);
result.Where(x => x.GroupName == "Audio Advanced #1").Select(x => (x.CfName, x.CfAnchor))
.Should().BeEquivalentTo(new[]
{
("TrueHD ATMOS", "truehd-atmos"),
("DTS X", "dts-x"),
("ATMOS (undefined)", "atmos-undefined"),
("DD+ ATMOS", "dd-atmos"),
("TrueHD", "truehd"),
("DTS-HD MA", "dts-hd-ma"),
("DD+", "ddplus"),
("DTS-ES", "dts-es"),
("DTS", "dts")
});
I'm concerned with my usage of LINQ to massage the data into a format I can run a fluent assertions condition against. My understanding of the philosophy of Fluent Assertions is that you should basically not manipulate the data before comparing it. I know that FA provides a suite of methods that can be used to compare data in different ways, but I'm not sure if it can manipulate / test the data in the way I am above.
Basically I'm testing two different things:
GroupName
at least once (there may be duplicates, but that's OK)GroupName
property equals a specific value, a list of values is matched collectively against the CfName
and CfAnchor
properties.This is sort-of like a dictionary of lists, but not exactly. So I can't use things like ContainsKey()
, AFAIK. Is it inappropriate to be using LINQ the way I am here, and if so, how should I represent this better using Fluent Assertions to match its philosophy?
To avoid having to remove duplicates there's BeSubsetOf
result.Select(x => x.GroupName).Should().BeSubsetOf(new[]
{
"Audio Advanced #1",
"Audio Advanced #2",
"Movie Versions",
"Unwanted"
});
To avoid having to only select the GroupName
property we can utilize that BeEquivalentTo
uses the expectation to select the members to compare by.
result.DistinctBy(x => x.GroupName).Should().BeEquivalentTo(new[]
{
new { GroupName = "Audio Advanced #1" },
new { GroupName = "Audio Advanced #2" },
new { GroupName = "Movie Versions" },
new { GroupName = "Unwanted" }
});
There's nothing built-in in FluentAssertions to do both of the above, but we can create an extension method for that.
result.Should().OnlyContainEquivalentsOf(new[]
{
new { GroupName = "Audio Advanced #1" },
new { GroupName = "Audio Advanced #2" },
new { GroupName = "Movie Versions" },
new { GroupName = "Unwanted" }
});
internal static class GenericCollectionAssertionExtensions
{
[CustomAssertion]
public static AndConstraint<TAssertions> OnlyContainEquivalentsOf<TCollection, T, TAssertions, TExpectation>(
this GenericCollectionAssertions<TCollection, T, TAssertions> parent,
IEnumerable<TExpectation> expectations,
Func<EquivalencyAssertionOptions<TExpectation>, EquivalencyAssertionOptions<TExpectation>> config,
string because = "",
params object[] becauseArgs)
where TCollection : IEnumerable<T>
where TAssertions : GenericCollectionAssertions<TCollection, T, TAssertions>
{
foreach (T subject in parent.Subject)
{
bool foundMatch = expectations.Any(expectation =>
{
using var scope = new AssertionScope();
subject.Should().BeEquivalentTo(expectation, opt => config(opt));
string[] failures = scope.Discard();
return failures.Length == 0;
});
Execute.Assertion
.ForCondition(foundMatch)
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context:collection} {0} to only contain equivalents of {1}{reason}, but {2} did not.",
parent.Subject, expectations, subject);
}
return new((TAssertions)parent);
}
[CustomAssertion]
public static AndConstraint<TAssertions> OnlyContainEquivalentsOf<TCollection, T, TAssertions, TExpectation>(
this GenericCollectionAssertions<TCollection, T, TAssertions> parent,
IEnumerable<TExpectation> expectations,
string because = "",
params object[] becauseArgs)
where TCollection : IEnumerable<T>
where TAssertions : GenericCollectionAssertions<TCollection, T, TAssertions>
{
return parent.OnlyContainEquivalentsOf(expectations, opt => opt, because, becauseArgs);
}
}
For the second assertion in question, the only adjustment I see to make it use anonymous types like the second example above.
result.Where(x => x.GroupName == "Audio Advanced #1").Should().BeEquivalentTo(new[]
{
new { CfName = "TrueHD ATMOS", CfAnchor = "truehd-atmos" },
new { CfName = "DTS X", CfAnchor = "dts-x" },
new { CfName = "ATMOS (undefined)", CfAnchor = "atmos-undefined" },
new { CfName = "DD+ ATMOS", CfAnchor = "dd-atmos" },
new { CfName = "TrueHD", CfAnchor = "truehd" },
new { CfName = "DTS-HD MA", CfAnchor = "dts-hd-ma" },
new { CfName = "DD+", CfAnchor = "ddplus" },
new { CfName = "DTS-ES", CfAnchor = "dts-es" },
new { CfName = "DTS", CfAnchor = "dts" }
});