0

I have a series of record types in my C# project that I use with JSON.Net to deserialize json data.

public record RadarrMetadata(
    IReadOnlyCollection<string> CustomFormats
);

public record SonarrMetadata(
    IReadOnlyCollection<string> ReleaseProfiles
);

public record JsonPaths(
    RadarrMetadata Radarr,
    SonarrMetadata Sonarr
);

public record RepoMetadata(
    JsonPaths JsonPaths
);

I need to "mock" instances of RepoMetadata to feed it into another object as part of its unit test suite. My solution right now is to create RepoMetadata objects directly, like so:

[Test, AutoMockData]
public void Directory_separators_are_normalized(
    [Frozen] IRepoMetadataParser metadataParser,
    RepoPathsFactory sut)
{
    var metadata = new RepoMetadata(
        new JsonPaths(
            new RadarrMetadata(new[] {"foo/bar\\dir"}),
            new SonarrMetadata(new[] {""})
        )
    );

    metadataParser.Deserialize().Returns(metadata);
    var result = sut.Create();
    result.RadarrCustomFormatPaths.Should().NotContain(x => !x.FullName.Contains(_oppositeSlash));

However, I do not like manually constructing RepoMetadata for these reasons:

  • If the surface of this object and its properties changes later, this test will break even though those particular properties have nothing to do with what this test is trying to verify.
  • Sort of a continuation of the above point: I have to create and pass in SonarrMetadata even though this type is inconsequential to the test.

One way I could solve this is by defining an interface for every record class, including nested types. Then I could use AutoFixture to create the object and just specify a return value for the properties I care about using NSubsitute:

[Test, AutoMockData]
public void Directory_separators_are_normalized(
    [Frozen] IRepoMetadata metadata,
    RepoPathsFactory sut)
{
    metadata.JsonPaths.Radarr.CustomFormats.Returns(new[] {"foo/bar\\dir"});
    var result = sut.Create();
    result.RadarrCustomFormatPaths.Should().NotContain(x => !x.FullName.Contains(_oppositeSlash));
}

However, it "feels wrong" to create this many interfaces (one per ORM type). I don't know if I'm on the right track. However, I really love how clean and concise the above alternative is when I introduce interfaces:

  • Test is cleaner / more concise
  • I don't need IRepoMetadataParser (or other intermediate objects)
  • I don't need to redundantly construct properties that I don't care about in RepoMetadata which makes this test less fragile when the data structure changes later to add more fields.

Is my approach with interfaces the best way? Should I go with mutable types instead (add setters to the properties)? The immutable types work great for the actual business logic, but the unit tests are troublesome because of it.

13
  • 1
    It takes two minutes to fix up your unit tests so that they function again. Commented Aug 19, 2022 at 12:38
  • Is AutoFixture not able to generate records directly?
    – Caleth
    Commented Aug 19, 2022 at 12:46
  • @RobertHarvey Sure, two minutes today. But what if I have dozens more of these tests? What if the object grows later and two minutes becomes 30 minutes? But beside all of that, your point is not relevant to my question. Commented Aug 19, 2022 at 12:50
  • @Caleth Yes, it can, but then how do I modify the properties I care about? There's no easy way to do that unless I make the properties mutable. There are workarounds, like those documented in the link posted below. But I wasn't particularly happy with those solutions. stackoverflow.com/questions/30938972/… Commented Aug 19, 2022 at 12:51
  • 4
    It actually is relevant. Software development is an exercise in tradeoffs; will it take more time to maintain your unit tests or to maintain those interfaces? Commented Aug 19, 2022 at 12:53

1 Answer 1

2

If the surface of this object and its properties changes later, this test will break even though those particular properties have nothing to do with what this test is trying to verify.

If you add a property, that amounts to a change of the public API of your method, so needing to change the unit test seem quite reasonable to me.

Sort of a continuation of the above point: I have to create and pass in SonarrMetadata even though this type is inconsequential to the test.

If the tested method does not use SonarrMetadata, why include it in as a (indirect) parameter? If it is used by the tested method the test should supply a valid value. After all, it would be perfectly reasonable for the method to start by validating all the parameters.

Your unit tests should test the public API of your 'unit'. Note that the unit might be a single method, a class, some set of classes etc. All the prerequisites of the public API should be respected. If the public API of the unit changes, then the unit tests also need to change. If the public API is unchanged, the unit test should not need changing.

Keep in mind that unit tests should provide value, if you spend more time maintaining the tests than the tests save you, you need to change the way you write tests. Different types of methods benefit more or less from unit tests. Take a sorting algorithm for example, a unit test is very helpful to find of-by-one errors and other mistakes, and it is really easy to check the result. UIs on the other hand is notoriously difficult to automatically test, after all, the acceptance criteria might be something fluffy like "is easy to use for the average user".

My personal opinion is that you should try to minimize the usage of automatically generated Mocks, since they tend to be fairly fragile and more difficult to understand. If you have simple records without complex behavior I see no reason not to use them.

Should I go with immutable types instead (add setters to the properties)?

Immutable types do not have setters, perhaps you mean mutable types? I would strongly prefer immutable types wherever practical, since they are easier to reason about, and records make immutable types fairly easy to use.

12
  • They are easier to reason about in multi-threaded or multi-core contexts, as they provide certain guarantees. There are other ways to get easier reasoning, like pure functions. Commented Aug 19, 2022 at 13:46
  • However, the rest of your post is good, and I upvoted it. Commented Aug 19, 2022 at 13:47
  • In my view, adding a property shouldn't trigger all relevant tests that use an object to change. I may be testing one aspect of the behavior of something that does indeed accept the full object but the internal behavior is applied uniformly to all properties. In other words, the same assertions I apply to CustomFormats would also apply to ReleaseProfiles, but I don't want to duplicate the test assertions. Adding a property in this case, assuming that property is also consistently verified, shouldn't trigger a test failure for maintainability reasons. Commented Aug 19, 2022 at 13:51
  • @RobertHarvey I would argue immutable objects are easier to reason about also in single threaded contexts, if for no other reason than they encourage things like pure functions. But 'wherever possible' might be to strong, perhaps 'wherever practical' would be better.
    – JonasH
    Commented Aug 19, 2022 at 13:52
  • 2
    @void.pointer and I'm not arguing that your unit tests do not provide value. I'm arguing against the 'every line of code needs to be covered by unit test'-mentality that are proposed by some (not necessarily you). If your tests helps you save time/cost, great! keep writing them!
    – JonasH
    Commented Aug 19, 2022 at 14:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.