Object validation in tests

Any application abounds with methods that return reference types or change their state at some point. At the time of testing of such methods, a developer is faced with a pretty unpleasant task - it is necessary to examine all the data in asserts. In simple cases with primitive types, the entire checking is reduced to only one assert. On the other hand, an object can have dozens of primitives, and in especially terrible cases - other objects (constantly, if we talk about industrial programming, to say the truth).

Problem statement: it is necessary to test the following method

public async Task<int> Create(Article article)
{
    await _context.AddAsync(article);
    await _context.SaveChangesAsync();
    return article.Id;
}

The Article class is described by the following model:

public class Article
{
    public string Text { get; set; }
    public string Title { get; set; }
    public string Preview { get; set; }
    public DateTime Created { get; set; }
    public bool Published { get; set; }
}
Раздуваем тест

The easiest and the most straightforward approach, and one that many, many people take, is to write one test with many asserts, like this:

[Fact]
public async Task Create_Success ()
{
    var article = FakeFactory.Fixture.Create<Article>();
    //
    var result = await _repository.Create(article);
    //
    var expected = _context.Articles.First(e => e.Text == article.Text);
    Assert.True(result == expected.Id);
    Assert.True(article.Text == expected.Text);
    Assert.True(article.Title == expected.Title);
    Assert.True(article.Created == expected.Created);
    Assert.True(article.Preview == expected.Preview);
    Assert.True(article.Published == expected.Published);
}

*If you have no idea what the FakeFactory is and how it works, I recommend you to look at the [previous article] (https://artstesh.ru/articles/1/article/10).

By and large, it doesn't look so scary) That is why such an approach sometimes fixed in some small projects; or becomes a norm for individual developers. As long as our models have less than a dozen of fields, it is possible to coexist with that somehow. The situation abruptly becomes catastrophic at the moment of appearance of the composition/aggregation or swelling of objects up to dozens of fields.

It is getting too hard to write tests for such things. Nevertheless, it also happens that especially stubborn enthusiasts write templates for auto-creation of assertions, based on types...

Reading of this also becomes a very dubious pleasure... And there are no life-hacks here - if you have to use the scroll while reading a test, it is time to panic.

Of course, any change in the model leads to the mandatory correction of all tests like that. And, that is much more important, it is easy to forget to add a new field to the tests, and they will pass anyway! This is how the team lose confidence in a test project...

Bottom line: it is hard to write, it is hard to read, and it is hard to maintain.

Lots and lots of tests

Option number two is to break a test, described above, into a number of smaller ones, leaving a few assertions per one test. I'm not going to show an example for that scenario - the idea is obvious. The main question here is, have we achieved something by such a move.

The only possible motivation here is an attempt of following of the principle of brevity. An attempt, which is a successful one, but senseless...

Did it become easier to read tests? Not at all, now we have much more of them, and scrolling will definitely be needed.

Did it become easier to write? Nothing changed, the same time is needed for writing, and even more, probably.

Maintenance? And again, no changes.

Invading functional code

Irritating inconvenience of the described approaches, leads us to the root of the problem: our task is to check the equality of objects; and every programmer know that the Object class has the Equals method... Coincidence? I don't think so!

The only thing we need, is to override the Equals method in the Article class:

public bool Equals(Article other)
{
    if (ReferenceEquals(null, other)) return false;
    if (ReferenceEquals(this, other)) return true;
    return string.Equals(Text, other.Text) &amp;&amp;
           string.Equals(Title, other.Title) &amp;&amp;
           string.Equals(Preview, other.Preview) &amp;&amp;
           Created.Equals(other.Created) &amp;&amp;
           Published == other.Published;
}

public override bool Equals(object obj)
{
    if (ReferenceEquals(null, obj)) return false;
    if (ReferenceEquals(this, obj)) return true;
    if (obj.GetType() != this.GetType()) return false;
    return Equals((Article) obj);
}

The test is alone again, and it looks much neater.

[Fact]
public async Task Create_Success ()
{
    var article = FakeFactory.Fixture.Create<Article>();
    //
    var result = await _repository.Create(article);
    //
    var expected = _context.Articles.First(e => e.Text == article.Text);
    Assert.True(result == expected.Id);
    Assert.True(article.Equals(expected));
}

So, what have we achieved?

Readability has grown drastically, the test is short and understandable at a glance.

It's enough to write the Equals method for a model once, and use it in any test you want. Actually, the writing process has also reached a new level.

Tests became easy to maintain - all the main data are concentrated compactly. Moreover, it is much more difficult to forget about adding of a new field into the Equals(), which lies at the same Article class, than somewhere in the testing code.

Have we found a 'silver bullet'?) Not at all. One of the most important, however, not written, principles of testing prohibits interference in the code being tested. And this is exactly what we are doing in this scenario - we've moved methods serving testing to the functional code! At first glance this may not seem significant, maybe even at the second one too... Many questions will arise at the moment when business requirements force us to use the Equals method)

At the end, we have to note that such an approach has the right to exist and may be convenient... if we have no alternatives. But canons are invented not for fun, and if there is any alternative, it is better to use it.

Special classes in a test project

Creating extensions for models in the testing project is one of the alternatives. Special comparison methods are created for every model:

public static class ArticleComparer
{
    public static bool IsEqual(this Article article, Article other)
    {
        if (article == null || other == null) return false;
        return string.Equals(article.Text, other.Text) &amp;&amp;
               string.Equals(article.Title, other.Title) &amp;&amp;
               string.Equals(article.Preview, other.Preview) &amp;&amp;
               article.Created.Equals(other.Created) &amp;&amp;
               article.Published == other.Published;
    }
}

The test, in this case, has almost no differences with the previous example:

[Fact]
public async Task Create_Success ()
{
    var article = FakeFactory.Fixture.Create<Article>();
    //
    var result = await _repository.Create(article);
    //
    var expected = _context.Articles.First(e => e.Text == article.Text);
    Assert.True(result == expected.Id);
    Assert.True(article.IsEqual(expected));
}

The functional code has no information about them, of course, and they have no influence on the app's work. We still retain all the benefits of overriding of the Equals method, but don't violate fundamental principles, moreover.

The minus is the same - every model requires a separate extension class, and for big projects it gradually becomes a problem. Though, in comparison with previous options, this approach seems perfect.

Bottom line: easy to write, easy to read, and enow easy to maintain.

Automation

And the last option for us is using of special libraries; my own choice is SemanticComparison (it is included in the "gentleman's set", of course).

This library completely saves us from necessity of manual creation of any implementations of comparison methods. All the work is being done on the fly, and the Equals method is being overridden during the test execution. Are there are looses in speed? Sure. But they are not significant, more often not noticeable, but the benefits of using are colossal.

As it was said previously, we don't have to implement any special methods, let's just write a test:

[Fact]
public async Task Create_Success()
{
    var article = FakeFactory.Fixture.Create<Article>();
    //
    var result = await _repository.Create(article);
    //
    var expected = _context.Articles.First(e => e.Text == article.Text);
    var source = expected.AsSource().OfLikeness<Article>().Without(e => e.Created);
    Assert.True(result == expected.Id);
    Assert.True(source.Equals(article));
}

The only difference is the additional object named 'source', and its definition contains all the magic.

And yet another pleasurable moment - the ability to exclude some fields from comparison in specific situations. For example, the comparison of an ID or a date may be executed in other parts of the system; and during the full comparison of objects, it will be found out that they aren't the same. But maybe they aren't the same because of business requirements/TOR or the architecture? SemanticComparison allows you to exclude from comparison these fields with a simple configuration. In the above example, we have excluded the date of creation (Created).

Result?

Readability is no worse and no better than the previous two options (is it possible to be better?).

Ease of writing... Obviously, it is a step forward, comparing to the creation of special methods - there is less code, that is especially significant when you have 100+ models in the project.

Maintenance. You will not forget to add a new field to the comparison method, for sure. If something changes in a model, the tests would find in out immediately and fall down loudly)

Minuses? Imho, there aren't serious minuses here, and this approach is the most preferable.