Как мне настроить вызов Equals с определенным типом, переопределяющим Equals в MoQ?

Работая с прекрасным mocking-framework MoQ, я натолкнулся на несколько неожиданный аспект (а я не люблю сюрпризов). Я издеваюсь над классом, который должен быть добавлен в коллекцию после вызова метода, например:

public class SomeClass{

}

public class Container {
    private List<SomeClass> classes = new List<SomeClass>();

    public IEnumerable<SomeClass> Classes {
        get {
            return classes;
        }
    }

    public void addSomeClass(SomeClass instance) {
        classes.Add(instance);
    }
}

[Test]
public void ContainerContainsAddedClassAfterAdd() {
    var mockSomeClass = new Mock<SomeClass>();  
    mockSomeClass.Setup(c => c.Equals(mockSomeClass.Object)).Return(true);

    var Container = new Container();
    Container.addSomeClass(mockSomeClass.Object);

    Assert(Container.Classes.Contains(mockSomeClass.Object));
}

Это работает хорошо, макет добавляется в коллекцию Container, а настройка метода Equals на макете гарантирует, что IEnumerable.Contains() вернет истину. Однако всегда есть сложности. Класс, над которым я насмехаюсь, не так прост, как наш SomeClass. Это примерно так:

public class SomeClassOverridingEquals{
    public virtual Equals(SomeClassOverridingEquals other) {
        return false;   
    }

    public override Equals(object obj) {
        var other = obj as SomeClassOverridingEquals;

        if (other != null) return Equals(other); // calls the override
        return false;
    }
}

[Test]
public void ContainerContainsAddedClassOverridingEqualsAfterAdd() {
    var mockSomeClass = new Mock<SomeClassOverridingEquals>();  
    mockSomeClass.Setup(c => c.Equals(mockSomeClass.Object)).Return(true);

    var Container = new Container();
    Container.addSomeClass(mockSomeClass.Object);

    Assert(Container.Classes.Contains(mockSomeClass.Object)); // fails
}

Класс содержит переопределение для метода Equals для его собственного конкретного типа, а метод Setup для макета, похоже, не может имитировать этот конкретный метод (только переопределяя более общий Equals(object)). Таким образом, тест не проходит.

Я до сих пор не нашел способа обойти этот довольно распространенный шаблон, кроме как переписать класс, чтобы не использовать переопределяющие равенства.

Мне это не нравится.

У кого-нибудь есть идеи?


person Tomas    schedule 25.09.2009    source источник


Ответы (4)


Я не думаю, что проблема связана с Moq, а скорее с методом расширения Contains. Несмотря на то, что вы перегрузили Equals более конкретной перегрузкой, Enumerable.Contains вызывает List<T>.Contains, потому что свойство Classes действительно поддерживается List<T>.

List<T>.Contains реализуется путем вызова EqualityComparer<T>.Default.Equals, и я думаю, что по умолчанию используется метод Equals, унаследованный от System.Object, то есть не тот, который переопределяет ваш макет, а тот, который принимает System.Object в качестве входных данных.

Просматривая реализацию EqualityComparer<T>.Default в Reflector, кажется, что у него есть особый случай для типов, реализующих IEquatable<T>, поэтому, если вы добавите этот интерфейс в свой класс (у него уже есть соответствующий метод), он может вести себя по-другому.

person Mark Seemann    schedule 25.09.2009
comment
+1. У Reflector всегда есть ответ. Я опубликовал следующий прием, чтобы обойти эту проблему здесь: stackoverflow.com/questions/1476233 / - person Anderson Imes; 28.09.2009

Марк Зееманн прав. Убедитесь, что вы намекаете Moq на правильную перегрузку:

mockSomeClass.Setup(c => c.Equals((object)mockSomeClass.Object)).Return(true);
person Anderson Imes    schedule 28.09.2009

Насколько сложно создать экземпляр SomeClass? Было бы разумнее просто использовать реальный объект? Если возможно, это было бы лучше, поскольку это не меняет конкретного поведения, которое является частью отношений между контейнером и классом.

В качестве альтернативы вы можете перебирать возвращенную коллекцию и просто искать объект, который совпадает с макетом экземпляра. Это позволило бы избежать необходимости обходить какое-либо специализированное поведение.

person Steve Freeman    schedule 04.10.2009

Создайте интерфейс ISomeClass и заставьте ваш контейнер использовать вместо него интерфейс. Таким образом вы достигнете двух вещей:

  1. Вы легко будете издеваться над ним с помощью Mock<ISomeClass> и фактически просто протестируете функциональность своего контейнера.
  2. Сделайте объединительный тест лучше, фактически отделив контейнер модульного теста от тестирования фактической реализации функциональности класса SomeClass.
person Robert Koritnik    schedule 25.09.2009
comment
Я обычно так и делаю, но интерфейсы не всегда то, что вам нужно. В конкретном случае, который привел меня к этому вопросу, я использую NHibernate для сохранения класса с ролью контейнера здесь. Мы не хотим, чтобы он содержал ISomeClass, но чтобы он содержал SomeClass. - person Tomas; 25.09.2009