Модульное тестирование и внедрение зависимостей с глубоко вложенными зависимостями - PullRequest
7 голосов
/ 10 ноября 2010

Предположим, унаследованный класс и структура методов, как показано ниже

public class Foo
{
    public void Frob(int a, int b)
    {
        if (a == 1)
        {
            if (b == 1)
            {
                // does something
            }
            else
            {
                if (b == 2)
                {
                    Bar bar = new Bar();
                    bar.Blah(a, b);
                }
            }
        }
        else
        {
            // does something
        }
    }
}

public class Bar
{
    public void Blah(int a, int b)
    {
        if (a == 0)
        {
            // does something
        }
        else
        {
            if (b == 0)
            {
                // does something
            }
            else
            {
                Baz baz = new Baz();
                baz.Save(a, b);
            }
        }
    }
}

public class Baz
{
    public void Save(int a, int b)
    {
        // saves data to file, database, whatever
    }
}

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

Я могу быть сторонником буквального толкования, но я думаю, что фраза «модульное тестирование» что-то значит.Это не означает, например, что при заданных входах 1 и 2 модульный тест Foo.Frob должен пройти успешно, только если 1 и 2 сохранены в базе данных.Основываясь на том, что я прочитал, я полагаю, что в конечном итоге это означает на основе входов 1 и 2, Frob вызвано Bar.Blah.Не важно, сделал ли Bar.Blah то, что должен был сделать, это не моя непосредственная задача.Если меня интересует тестирование всего процесса, я считаю, что для этого есть другой термин, верно?Функциональное тестирование?Сценарий тестирования?Без разницы.Поправьте меня, если я слишком жесток, пожалуйста!

На данный момент придерживаясь моей жесткой интерпретации, давайте предположим, что я хочу попробовать использовать внедрение зависимостей, с одним преимуществом в том, что я могу издеваться над моими классами такчто я могу, например, , а не сохранить мои тестовые данные в базе данных или файле или в любом другом случае.В этом случае Foo.Frob нужно IBar, IBar нужно IBaz, IBaz может потребоваться база данных.Где эти зависимости должны быть введены?В Foo?Или Foo просто нужно IBar, а затем Foo отвечает за создание экземпляра IBaz?

Когда вы попадаете во вложенную структуру, такую ​​как эта, вы можете быстро увидеть, что можетбыть несколько зависимостей необходимо.Каков предпочтительный или принятый метод выполнения такой инъекции?

Ответы [ 6 ]

7 голосов
/ 10 ноября 2010

Давайте начнем с вашего последнего вопроса.Где вводятся зависимости: Обычный подход заключается в использовании инжектора конструктора (как описано Фаулером ).Таким образом, Foo вводится с IBar в конструкторе.Конкретная реализация IBar, Bar в свою очередь содержит IBaz, введенную в его конструктор.И, наконец, реализация IBaz (Baz) содержит IDatabase (или что-то еще).Если вы используете инфраструктуру DI, такую ​​как Castle Project , вы просто попросите контейнер DI разрешить для вас экземпляр Foo.Затем он будет использовать все, что вы настроили, чтобы определить, какую реализацию IBar вы используете.Если он определит, что ваша реализация IBar равна Bar, он затем определит, какую реализацию IBaz вы используете и т. Д.

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

Чтобы прокомментировать ваши опасения по поводу слишком жесткой и т. д., могу сказать лишь одно: вы, по моему мнению, выбираетеправильный путь.Тем не менее, руководство может удивиться, когда им станет очевидна реальная стоимость внедрения всех этих тестов.

Надеюсь, это поможет.

2 голосов
/ 10 ноября 2010

тип теста, который вы описали в первой части своего поста (когда вы пробуете все части вместе), он обычно определяется как интеграционный тест. Хорошей практикой в ​​вашем решении должен быть проект модульного тестирования и проект интеграционного тестирования. Чтобы внедрить зависимости в ваш код, первое и самое важное правило - это кодирование с использованием интерфейсов. Предположим, что, скажем, ваш класс содержит интерфейс в качестве члена, и вы хотите внедрить / смоделировать его: вы можете либо представить его как свойство, либо передать реализацию, используя конструктор класса. Я предпочитаю использовать свойства для раскрытия зависимостей, поэтому конструктор не станет слишком многословным. Я предлагаю вам использовать NUnit или MBunit в качестве среды тестирования, а Moq - среду для моделирования (более понятные результаты, чем проверки Rhino) Вот документация с некоторыми примерами того, как издеваться над Moq http://code.google.com/p/moq/wiki/QuickStart

Надеюсь, это поможет

2 голосов
/ 10 ноября 2010

Я не думаю, что есть один «предпочтительный» метод для решения этой проблемы, но одна из ваших основных проблем, по-видимому, заключается в том, что при внедрении зависимостей при создании Foo необходимо также создать Baz, который может быть ненужным Одним из простых способов решения этой проблемы является то, что Bar зависит не от IBaz, а от Lazy<IBaz> или Func<IBaz>, что позволяет контейнеру IoC создавать экземпляр Bar без немедленного создания Baz.

Например:

public interface IBar
{
    void Blah(int a, int b);
}

public interface IBaz
{
    void Save(int a, int b);
}

public class Foo
{
    Func<IBar> getBar;
    public Foo(Func<IBar> getBar)
    {
        this.getBar = getBar;
    }

    public void Frob(int a, int b)
    {
        if (a == 1)
        {
            if (b == 1)
            {
                // does something
            }
            else
            {
                if (b == 2)
                {                        
                    getBar().Blah(a, b);
                }
            }
        }
        else
        {
            // does something
        }
    }
}



public class Bar : IBar
{
    Func<IBaz> getBaz;

    public Bar(Func<IBaz> getBaz)
    {
        this.getBaz = getBaz;
    }

    public void Blah(int a, int b)
    {
        if (a == 0)
        {
            // does something
        }
        else
        {
            if (b == 0)
            {
                // does something
            }
            else
            {
                getBaz().Save(a, b);
            }
        }
    }
}

public class Baz: IBaz
{
    public void Save(int a, int b)
    {
        // saves data to file, database, whatever
    }
}
1 голос
/ 10 ноября 2010

Я бы сказал, что вы правы насчет модульного тестирования, оно должно охватывать довольно маленький «блок» кода, хотя сколько именно нужно обсудить.Однако, если это касается базы данных, это почти наверняка не модульный тест - я бы назвал это интеграционным тестом.

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

Но да, вставляйте свой IBaz в свой IBar, когда он появитсясоздал и вставил свой IBar в свой Foo.Это можно сделать в конструкторе или в установщике.Конструктор (IMO) лучше, поскольку он приводит к созданию только допустимых объектов.Один из вариантов, который вы можете сделать (известный как DI бедного человека), - это перегрузить конструктор, чтобы вы могли передать IBar для тестирования и создать Bar в конструкторе без параметров, используемом в коде.Вы теряете хорошие преимущества дизайна, но стоит задуматься.

Когда вы все это решите, попробуйте контейнер IoC, такой как Ninject , который может облегчить вашу жизнь.

(Также рассмотрите такие инструменты, как TypeMock или Moles , которые могут высмеивать вещи без интерфейса - но имейте в виду, что это обман, и вы не получите улучшенный дизайн, поэтомудолжно быть последним средством).

0 голосов
/ 10 ноября 2010

Звучит так, будто здесь немного борьбы за:

  1. иметь дело с устаревшим кодом
  2. продолжать писать поддерживаемый код
  3. протестируйте свой код (действительно, продолжение 2)
  4. а также, я полагаю, что-то выпустить

Использование руководством «модульных тестов» может быть решено только путем их запроса, но вот мой 2c о том, что может быть хорошей идеей в отношении всех 4 вопросов, перечисленных выше.

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

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

Вы не хотите тратить целый день на рефакторинг или переписывание своей кодовой базы, поэтому вам нужно быть осторожным в том, как вы справляетесь с зависимостями. В приведенном вами примере внутри Foo лучше всего повысить Bar до internal property и настроить проект так, чтобы внутренние компоненты были видны вашему тестовому проекту (используя атрибут InternalsVisibleTo в AssemblyInfo.cs). Конструктор по умолчанию Bar может установить для свойства new Bar(). Ваш тест может установить для него некоторый подкласс Bar, используемый для тестирования. Или заглушка. Я думаю, что это сократит количество изменений, которые вы должны будете внести, чтобы сделать эту вещь тестируемой в будущем.

И, конечно, вам не нужно выполнять рефакторинг класса, пока вы не внесете какие-то другие изменения в этот класс.

0 голосов
/ 10 ноября 2010

Когда у вас возникают проблемы с глубоко вложенной иерархией, это просто означает, что вы вводите недостаточно зависимостей.

Проблема в том, что у нас есть Baz, и похоже, что вам нужно передать Baz Foo, который передает его Bar, который наконец вызывает метод. Это похоже на большую работу и вроде бесполезно ...

Что вы должны сделать, это передать Baz в качестве параметра конструктора объекта Bar. Затем Bar должен быть передан в конструктор объекта Foo. Фу никогда не должен касаться или даже знать о существовании Баз. Только Бар заботится о Баз. При тестировании Foo вы должны использовать другую реализацию интерфейса Bar. Эта реализация, вероятно, делает только запись того факта, что Бла был вызван. Не нужно учитывать существование Баз.

Вы, вероятно, думаете что-то вроде этого:

class Foo
{
    Foo(Baz baz)
    {
         bar = new Bar(baz);
    }

    Frob()
    {
         bar.Blah()
    }
}

class Bar
{
     Bar(Baz baz);

     void blah()
     {
          baz.biz();
     }
}

Вы должны сделать что-то вроде этого:

class Foo
{
    Foo(Bar bar);

    Frob()
    {
         bar.Blah()
    }
}

class Bar
{
     Bar(Baz baz);

     void blah()
     {
          baz.biz();
     }
}

Если вы делаете это правильно, каждый объект должен иметь дело только с объектами, с которыми он непосредственно взаимодействует.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...