Как назначить макет интерфейса для свойства с частным сеттером для модульного тестирования? - PullRequest
1 голос
/ 15 ноября 2011
[InheritedExport(typeof(MyAbstractClass))
public abstract class MyAbstractClass
{
    [Import] protected IFoo Foo { get; private set; }
}

public sealed class MyClass : MyAbstractClass
{
    public void DoSomething()
    {
        if (Foo == null) throw new Exception();

        var = Foo.GetBar();
        //etc.
    }
}

По сути, я использую MEF для экспорта классов и получения "общего" импорта.Когда я хочу протестировать эти классы, я могу создать фиктивные интерфейсы IFoo, но как мне на самом деле получить его с помощью приватного сеттера?MEF каким-то образом способен справиться с этим, и я не уверен, как еще я могу проверить мой метод DoSomething.

Ответы [ 6 ]

3 голосов
/ 15 ноября 2011

Если вы хотите сохранить импорт MEF, самый простой способ сделать это - использовать ImportingConstructorAttribute вместо ImportAttribute s для каждого свойства.

[InheritedExport(typeof(MyAbstractClass))
public abstract class MyAbstractClass
{
    [ImportingConstructor]
    protected MyAbstractClass(IFoo foo)
    {
        //BONUS! Null check here instead...
        if (foo == null) throw new NullArgumentException("foo");

        Foo = foo;
    }

    protected IFoo Foo { get; private set; }
}

public sealed MyClass : MyAbstractClass
{
    [ImportingConstructor]
    public MyClass(IFoo foo) : base(foo) { }

public void DoSomething()
{
    var = Foo.GetBar();
    //etc.
}

}

Решение вроде воняет, потому что теперь вам нужно, чтобы все классы, выходящие из MyAbstractClass, использовали ImportingConstructorAttribute и вызывали base().Это может стать довольно уродливым, если ваш абстрактный класс используется повсеместно, особенно если он решит добавить другое импортированное свойство ... теперь вам нужно изменить сигнатуру конструктора.

Я бы придерживался уродливого отражения ..лучше уродливые юнит-тесты, чем уродливый код.

1 голос
/ 15 ноября 2011

отражение - лучший способ сделать это. Мне нравится создавать метод расширения в базовой тестовой сборке с полезными функциями, такими как доступ / настройка приватных членов и т. д.

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

public static class ReflectionExtensions
{
    public static T GetPrivateFieldValue<T>(this object instance, string fieldName)
    {
        var field = instance.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
        if (field != null)
        {
            return (T) field.GetValue(instance);
        }
        throw new ArgumentException("The field specified could not be located.", "fieldName");
    }

    public static void SetReadonlyProperty(this object instance, string propertyName, object value)
    {
        instance.GetType().GetProperty(propertyName).SetValue(instance, value, null);
    }

    public static void SetStaticReadonlyProperty(this Type type, string propertyName, object value)
    {
        type.GetProperty(propertyName).GetSetMethod(true).Invoke(null, new[] { value });
    }
}
1 голос
/ 15 ноября 2011

Я полагаю, что вы можете сделать это с помощью среды MS Moles:

http://research.microsoft.com/en-us/projects/moles/

1 голос
/ 15 ноября 2011

MyAbstractClass зависит от IFoo, но вы не указываете это явно. Вы должны добавить конструктор, чтобы сделать зависимость явной:

public MyAbstractClass(IFoo foo) { this.Foo = foo; }

Теперь вы можете легко проверить это с помощью макета.

Итак, я бы переписал ваш класс так:

[InheritedExport(typeof(MyAbstractClass))
public abstract class MyAbstractClass {
    private readonly IFoo foo;
    public IFoo Foo {
        get {
            Contract.Ensures(Contract.Result<IFoo>() != null);  
            return this.foo;
        }
    }

    protected MyAbstractClass(IFoo foo) {
        Contract.Requires(foo != null);
        this.foo = foo;
    }
}

public class MyClass : MyAbstractClass
{
    [ImportingConstructor]
    public MyClass(IFoo foo) : base(foo) { }
}

В противном случае, вы должны использовать отражение, чтобы добраться до частного сеттера. Это отвратительно.

1 голос
/ 15 ноября 2011

Я думаю, что единственный способ - использовать отражение.

0 голосов
/ 15 ноября 2011

Попробуйте передать его в конструктор:

class MyClass : MyAbstractClass
{
    public MyClass (IFoo foo)
    {
        Foo = foo;
    }
}

и измените «частный набор» в вашем абстрактном классе на «защищенный набор».

...