Нулевые параметры модульного теста в конструкторе

Я закодировал конструктор для класса, и я проверяю, что каждый параметр равен нулю. См. пример ниже:

public MyClass(IObjectA objA, IObjectB objB) : IMyClass
{
    if (objA == null)
    {
        throw new ArgumentNullException("objA");
    }

    if (objB == null)
    {
        throw new ArgumentNullException("objB");
    }

    ...
}

Обычно я тестирую модуль (используя Moq), имитируя IObjectA и IObjectB и передавая их. В приведенном выше примере будет создано 2 модульных теста для тестирования каждого сценария.

У меня проблема, когда в конструктор передается третий параметр. Это требует, чтобы я изменил свои предыдущие тесты, так как я внезапно получаю исключение типа «Нет конструктора для MyClass имеет 2 параметра».

Я также использую AutoMockContainer. По сути, я хотел бы иметь возможность протестировать конструктор, зарегистрировав нулевой объект в контейнере. Например:

[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void ConstructionThrowsExceptionForNullObjA()
{
    // Arrange.
    var container = new AutoMockContainer(new MockRepository(MockBehavior.Default));

    container.Register<IObjectA>(null);

    // Act.
    var sut = container.Create<MyClass>();
}

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

К сожалению, приведенный выше модульный тест проходит. Но по неправильной причине. Метод Register<T>() выдает ArgumentNullException не тот код, который выполняется в разделе «Действие».

Есть ли у кого-нибудь предложения по тестированию параметров конструктора и необходимости повторного посещения модульного теста при добавлении новых параметров?


person Adrian Thompson Phillips    schedule 26.01.2015    source источник
comment
Если вы явно тестируете конструктор, то тест должен измениться при изменении контракта конструктора. То же самое касается любой тестируемой функции. Боюсь, это жизнь.   -  person Benjamin Hodgson♦    schedule 26.01.2015
comment
Используете ли вы контейнер IoC в своем приложении для внедрения зависимостей в этот класс? если это так, подумайте о ценности написания (и, следовательно, необходимости поддерживать) множества тестов (их может быть сотни), которые проверяют вашу инфраструктуру. В одном решении, над которым я работал, было много тысяч тестов, все из которых проверяли аргументы null ctor, и поддерживать его стало кошмаром.   -  person Matt    schedule 26.01.2015
comment
@Matt, к сожалению, мой клиент хочет 100% охват тестами. Я сам не беспокоюсь о статистике тестового покрытия и обычно не тестирую такие вещи или проводку. Мне нравится программировать в обороне, но иногда, когда я работаю в местах, пытаясь достичь 100% охвата, я чувствую, что просто усложняю себе жизнь.   -  person Adrian Thompson Phillips    schedule 26.01.2015


Ответы (1)


Вы можете частично облегчить это бремя, используя шаблоны фабрики или построителя для создания своих объектов.

Упрощенный пример шаблона построителя:

public class Foo
{
    public string Prop1 { get; private set; }

    public Foo(string prop1)
    {
        this.Prop1 = prop1;
    }
}

[TestClass]
public class FooTests
{
    [TestMethod]
    public void SomeTestThatRequiresAFoo()
    {
        Foo f = new Foo("a");
        // testy stuff
    }

    [TestMethod]
    public void SomeTestThatRequiresAFooUtilizingBuilderPattern()
    {
        Foo f = new FooBuilder().Build();
    }

    [TestMethod]
    public void SomeTestThatRequiresAFooUtilizingBuilderPatternOverrideDefaultValue()
    {
        Foo f = new FooBuilder()
           .WithProp1("different than default")
           .Build();
    }
}

internal class FooBuilder
{

    public string Prop1 { get; private set; }

    // default constructor, provide default values to Foo object
    public FooBuilder()
    {
        this.Prop1 = "test";
    }

    // Sets the "Prop1" value and returns this, done this way to create a "Fluent API"
    public FooBuilder WithProp1(string prop1)
    {
        this.Prop1 = prop1;
        return this;
    }

    // Builds the Foo object by utilizing the properties created as BuilderConstruction and/or the "With[PropName]" methods.
    public Foo Build()
    {
        return new Foo(
            this.Prop1
        );
    }
}

Таким образом, если/когда ваш объект Foo изменится, будет немного проще обновить ваши модульные тесты, чтобы учесть изменения.

Рассмотреть возможность:

public class Foo
{
    public string Prop1 { get; private set; }
    public string Prop2 { get; private set; }    

    public Foo(string prop1, string prop2)
    {
        this.Prop1 = prop1;
        this.Prop2 = prop2
    }
}

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

internal class FooBuilder
{

    public string Prop1 { get; private set; }
    public string Prop2 { get; private set; }

    // default constructor, provide default values to Foo object
    public FooBuilder()
    {
        this.Prop1 = "test";
        this.Prop2 = "another value";
    }

    // Sets the "Prop1" value and returns this, done this way to create a "Fluent API"
    public FooBuilder WithProp1(string prop1)
    {
        this.Prop1 = prop1;
        return this;
    }

    // Similar to the "WithProp1"
    public FooBuilder WithProp2(string prop2)
    {
        this.Prop2 = prop2;
        return this;
    }

    // Builds the Foo object by utilizing the properties created as BuilderConstruction and/or the "With[PropName]" methods.
    public Foo Build()
    {
        return new Foo(
            this.Prop1,
            this.Prop2
        );
    }
}

С этой новой реализацией Foo и FooBuilder единственный модульный тест, который может сломаться, — это тот, который создал Foo вручную, FooBuilder, использующий модульные тесты, по-прежнему будет работать без ошибок.

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

В вашем примере для модульного тестирования нуля в конструкторе вы можете написать модульный тест, используя шаблон построителя как таковой:

[TestMethod]
public void TestWithNullInFirstParam()
{
    Foo f = new FooBuilder()
        .WithProp1(null)
        .Build()

    // in this case "f" will have Prop1 = null, prop2 = "another value"
}  
person Kritner    schedule 26.01.2015
comment
Очень интересно, определенно есть что переварить и обдумать. - person Adrian Thompson Phillips; 26.01.2015