简体   繁体   中英

ExpectedObjects compares concrete objects instead of only interfaces

I have an xUnit unit test project utilizing ExpectedObjects . Right now I'm trying to test a service that returns objects conforming to an interface. For the sake of the question let's take an example interface

public interface INamed
{
    string Name { get; }
}

and two different implementations:

public class ComplexEntity : INamed
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Domain { get; set; }

    /* Many other properties follow... */
}
public class SimpleEntity : INamed
{
    public int Id { get; set; }
    public string Name { get; set; }
}

The method under test has a signature of

public IEnumerable<INamed> GetNames();

All that I care about is that the correct names are returned. So after mocking whatever dependencies the service might take I construct an expected result set as follows:

IEnumerable<INamed> expected = new []
{
    new SimpleEntity { Name = "NAM1" },
    new SimpleEntity { Name = "NAM2" },
    ...
}

And I compare them as follows:

// ACT
var result = systemUnderTest.GetNames();

// ASSERT
expected.ToExpectedObject().ShouldMatch(result);

This will fail. The collection that is actually returned by the GetNames under test is a mixed collection of both SimpleEntities and ComplexEntities , and none will match, because the Id properties of returned objects are non-zero, and other properties from ComplexEntity that I don't care about are not even present in the expected set.

The ExpectedObjects docs give me no guidance on this. The ShouldMatch is supposed to work on anonymous types, so this should (and does) work:

expected.Select(e => new { e.Name }).ToExpectedObject().ShouldMatch(result);

But I see this as an inflexible workaround. There are many other methods with contracts including only interfaces where I don't care about the underlying types. I would have to manually select the same properties from an interface in every such test and if that interface ever changed to include more properties the tests would still pass even though they wouldn't be correctly checking the contract . For example if I added the Id property to INamed , the test would still pass and would never even test for Id , allowing errors to go on silently.

Is there an out-of-the-box way to make ExpectedObjects compare only the public interfaces? I guess I could manually write a contrived, dynamic method creating an anonymous type out of an interface, make it generic and roll with it, something with usage like:

expected.ToExpectedObject().ShouldMatchInterface<INamed>(result);

but that makes me question using the library in the first place, as the effort put into ShouldMatchInterface would probably be significant.

It doesn't look like you're setting up the Expected Object correctly.

For example, with a GetNames() implementation like this:

public class Foo
{
    public IEnumerable<INamed> GetNames()
    {
        return new[] {
            new ComplexEntity() { Id = 1, Name = "NAM1", Domain = "DOM1" },
            new ComplexEntity() { Id = 2, Name = "NAM2", Domain = "DOM2" }
        };
    }
}

The following xUnit + ExpectedObjects test will pass:

using ConsoleProject;
using ExpectedObjects;
using Xunit;

namespace ConsoleProject_Tests
{
    public class ExpectedObjectsTests
    {
        [Fact]
        public void GetNames_should_return_INamed_ExpectedObjects_Style()
        {
            var expected = new[]
            {
                new { Name = "NAM1" },
                new { Name = "NAM2" }
            }.ToExpectedObject();

            var systemUnderTest = new Foo();
            var actual = systemUnderTest.GetNames();

            expected.ShouldMatch(actual);
        }
    }
}

Note that ToExpectedObject() is being fed anonymous objects, not concrete classes.

Now compare that with the old way of doing things by implementing a custom IEqualityComparer<INamed> which just happens to be on the test class...

using ConsoleProject;
using System;
using System.Collections.Generic;
using Xunit;

namespace ConsoleProject_Tests
{
    public class ClassicXUnitTests : IEqualityComparer<INamed>
    {
        bool IEqualityComparer<INamed>.Equals(INamed x, INamed y)
        {
            if (x == null && y == null) return true;
            if (x == null || y == null) return false;
            return String.Equals(x.Name, y.Name);
        }

        int IEqualityComparer<INamed>.GetHashCode(INamed obj)
        {
            return obj.Name.GetHashCode();
        }

        [Fact]
        public void GetNames_should_return_INamed_xUnit_Style()
        {
            var expected = new[]
            {
                new SimpleEntity() { Name = "NAM1" },
                new SimpleEntity() { Name = "NAM2" }
            };

            var systemUnderTest = new Foo();
            var actual = systemUnderTest.GetNames();

            Assert.Equal<INamed>(expected, actual, this);
        }
    }
}

It still needs a concrete class that implements INamed because you can't just create a new abstract class or interface.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM