TDD с зависимостями файловой системы - PullRequest
6 голосов
/ 09 сентября 2010

У меня есть интеграционный тест LoadFile_DataLoaded_Successfully () . И я хочу реорганизовать его в модульный тест для разрыва зависимости с filesytem.

P.S. Я новичок в TDD:

Вот мой производственный класс:

public class LocalizationData
{
    private bool IsValidFileName(string fileName)
    {
        if (fileName.ToLower().EndsWith("xml"))
        {
            return true;
        }
        return false;
    }

    public XmlDataProvider LoadFile(string fileName)
    {
        if (IsValidFileName(fileName))
        {
            XmlDataProvider provider = 
                            new XmlDataProvider
                                 {
                                      IsAsynchronous = false,
                                      Source = new Uri(fileName, UriKind.Absolute)
                                 };

            return provider;
        }
        return null;
    }
}

и мой тестовый класс (Nunit)

[TestFixture]
class LocalizationDataTest
{
    [Test]
    public void LoadFile_DataLoaded_Successfully()
    {
        var data = new LocalizationData();
        string fileName = "d:/azeri.xml";
        XmlDataProvider result = data.LoadFile(fileName);
        Assert.IsNotNull(result);
        Assert.That(result.Document, Is.Not.Null);
    }
}

Любая идея, как его реорганизовать, чтобы нарушить зависимость файловой системы

Ответы [ 11 ]

8 голосов
/ 09 сентября 2010

Здесь вам не хватает инверсии контроля.Например, вы можете ввести принцип внедрения зависимостей в ваш код:

public interface IXmlDataProviderFactory
{
    XmlDataProvider Create(string fileName);
}
public class LocalizationData
{
    private IXmlDataProviderFactory factory;
    public LocalizationData(IXmlDataProviderFactory factory)
    {
        this.factory = factory;
    }

    private bool IsValidFileName(string fileName)
    {
        return fileName.ToLower().EndsWith("xml");
    }

    public XmlDataProvider LoadFile(string fileName)
    {
        if (IsValidFileName(fileName))
        {
            XmlDataProvider provider = this.factory.Create(fileName);
            provider.IsAsynchronous = false;
            return provider;
        }
        return null;
    }
}

В приведенном выше коде создание XmlDataProvider абстрагируется с использованием интерфейса IXmlDataProviderFactory.Реализация этого интерфейса может быть предоставлена ​​в конструкторе LocalizationData.Теперь вы можете написать свой тестовый модуль следующим образом:

[Test]
public void LoadFile_DataLoaded_Succefully()
{
    // Arrange
    var expectedProvider = new XmlDataProvider();
    string validFileName = CreateValidFileName();
    var data = CreateNewLocalizationData(expectedProvider);

    // Act
    var actualProvider = data.LoadFile(validFileName);

    // Assert
    Assert.AreEqual(expectedProvider, actualProvider);
}

private static LocalizationData CreateNewLocalizationData(
    XmlDataProvider expectedProvider)
{
    return new LocalizationData(FakeXmlDataProviderFactory()
    {
        ProviderToReturn = expectedProvider
    });
}

private static string CreateValidFileName()
{
    return "d:/azeri.xml";
}

FakeXmlDataProviderFactory выглядит следующим образом:

class FakeXmlDataProviderFactory : IXmlDataProviderFactory
{
    public XmlDataProvider ProviderToReturn { get; set; }

    public XmlDataProvider Create(string fileName)
    {
        return this.ProviderToReturn;
    }
}

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

Однако в вашей производственной среде это может стать очень громоздким очень скоро, когда вы вручнуюсоздать класс.Особенно, когда он содержит много зависимостей.Вот где светятся рамки IoC / DI.Они могут помочь вам с этим.Например, если вы хотите использовать LocalizationData в своем производственном коде, вы можете написать такой код:

var localizer = ServiceLocator.Current.GetInstance<LocalizationData>();

var data = data.LoadFile(fileName);

Обратите внимание, что я использую Common Service Locator в качествепример здесь.

Фреймворк позаботится о создании этого экземпляра для вас.Однако, используя такую ​​инфраструктуру внедрения зависимостей, вы должны будете сообщить платформе, какие «сервисы» нужны вашему приложению.Например, когда я использую библиотеку Simple Service Locator в качестве примера (бесстыдный плагин), ваша конфигурация может выглядеть следующим образом:

var container = new SimpleServiceLocator();

container.RegisterSingle<IXmlDataProviderFactory>(
    new ProductionXmlDataProviderFactory());

ServiceLocator.SetLocatorProvider(() => container);

Этот код обычно идет впуть запуска вашего приложения.Конечно, единственная недостающая часть головоломки - это класс ProductionXmlDataProviderFactory.Вот оно:

class ProductionXmlDataProviderFactory : IXmlDataProviderFactory
{
    public XmlDataProvider Create(string fileName)
    {
        return new XmlDataProvider
        {
            Source = new Uri(fileName, UriKind.Absolute)
        };
    }
}

Пожалуйста, не имейте в виду, что вы, вероятно, не захотите обновлять свой LocalizationData в своем производственном коде самостоятельно, потому что этот класс, вероятно, используется другими классами, которые зависят от этого.тип.Что вы обычно делаете, это просите инфраструктуру создать самый верхний класс для вас (например, команду, которая реализует полный вариант использования) и выполните ее.

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

7 голосов
/ 31 мая 2012

Проблема в том, что вы не делаете TDD. Сначала вы написали рабочий код, а теперь хотите его протестировать.

Сотрите весь этот код и начните снова. Сначала напишите тест, а затем напишите код, который проходит этот тест. Затем напишите следующий тест и т. Д.

Какова ваша цель? Учитывая строку, которая заканчивается на «xml» (почему не «.xml»?), Вам нужен поставщик данных XML, основанный на файле, именем которого является эта строка. Это твоя цель?

Первые тесты были бы вырожденным случаем. Учитывая строку типа "name_with_wrong_ending", ваша функция должна завершиться ошибкой Как это должно потерпеть неудачу? Должен ли он вернуть ноль? Или это должно вызвать исключение? Вы можете подумать об этом и принять решение в своем тесте. Затем вы делаете тестовый проход.

Теперь, как насчет такой строки: "test_file.xml", но в случае, когда такого файла не существует? Что вы хотите, чтобы функция делала в этом случае? Должен ли он вернуть ноль? Должно ли это бросить исключение?

Простейший способ проверить это, конечно, состоит в том, чтобы фактически запустить код в каталоге, в котором нет этого файла. Однако, если вы предпочитаете написать тест, чтобы он не использовал файловую систему (мудрый выбор), вам нужно задать вопрос «Существует ли этот файл», и тогда ваш тест должен заставить ответ быть "ложным".

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

Конечно, теперь вам нужно проверить, что нормальная реализация isFilePresent работает правильно. И для этого вам придется использовать настоящую файловую систему. Однако вы можете не допустить тестов файловой системы в свои тесты LocalizationData, создав новый класс с именем FileSystem и переместив метод isFilePresent в этот новый класс. Затем ваш тест LocalizationData может создать производную от этого нового класса FileSystem и переопределить isFilePresent для возврата false.

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

ОК, какой следующий тест? Что делает ваша функция loadFile, когда файл существует , но не содержит действительный xml? Должно ли это сделать что-нибудь? Или это проблема для клиента? Вам решать. Но если вы решите проверить это, вы можете использовать ту же стратегию, что и раньше. Создайте функцию с именем isValidXML и сделайте так, чтобы тест переопределил ее для возврата false.

Наконец, нам нужно написать тест, который фактически возвращает XMLDataProvider. Итак, последняя функция, которую loadData должен вызывать, после всех этих других функций, createXmlDataProvider. И вы можете переопределить это, чтобы вернуть пустой или фиктивный XmlDataProvider.

Обратите внимание, что в своих тестах вы никогда не переходили на настоящую файловую систему и действительно создавали XMLDataProvider на основе файла. Но то, что вы сделали , это проверка каждого оператора if в вашей функции loadData. Вы проверили функцию loadData.

Теперь вам нужно написать еще один тест. Тест, который использует настоящую файловую систему и настоящий действительный файл XML.

3 голосов
/ 31 мая 2012

Когда я смотрю на следующий код:

public class LocalizationData
{
    private static bool IsXML(string fileName)
    {
        return (fileName != null && fileName.ToLower().EndsWith("xml"));
    }

    public XmlDataProvider LoadFile(string fileName)
    {
        if (!IsXML(fileName)) return null*;
        return new XmlDataProvider{
                                     IsAsynchronous = false,
                                     Source = new Uri(fileName, UriKind.Absolute)
                                  };
    }
}
  • (* Я не в восторге от возврата null. Тьфу! Это пахнет.)

В любом случае, я бы задал себе следующие вопросы:

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

Функция IsXML чрезвычайно тривиальна. Вероятно, даже не принадлежит к этому классу.

Функция LoadFile создает синхронный XmlDataProvide, если получает действительное имя файла XML.

Сначала я бы поискал, кто использует LoadFile и откуда передается fileName. Если он является внешним по отношению к нашей программе, то нам нужна некоторая проверка. Если это внутреннее и где-то еще мы уже проводим валидацию, то нам хорошо идти. Как предложил Мартин, я бы порекомендовал рефакторинг, чтобы принять Uri в качестве параметра вместо строки.

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

Теперь, стоит ли что-нибудь тестировать? XMLDataProvider - это не класс, который мы создали, мы ожидаем, что он будет работать нормально, когда мы предоставим действительный Uri.

Честно говоря, я бы не стал тратить свое время на написание теста для этого. В будущем, если мы увидим больше логики, мы могли бы вернуться к этому снова.

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

Это не имеет ничего общего с вашим тестированием (x), но рассмотрите возможность использования Uri вместо String в качестве типа параметра для вашего API.

http://msdn.microsoft.com/en-us/library/system.uri(v=VS.100).aspx

x: Я думаю, что Стивен очень хорошо освещал эту тему.

2 голосов
/ 09 сентября 2010

В одном из моих (Python) проектов я предполагаю, что все модульные тесты выполняются в специальном каталоге, который содержит папки «data» (входные файлы) и «output» (выходные файлы). Я использую тестовый сценарий, который сначала проверяет, существуют ли эти папки (то есть, если текущий рабочий каталог указан правильно), а затем запускает тесты. Мои модульные тесты могут затем использовать относительные имена файлов, такие как «data / test-input.txt».

Я не знаю, как это сделать в C #, но, возможно, вы можете проверить наличие файла "data / azeri.xml" в методе test SetUp.

1 голос
/ 09 сентября 2010

Почему вы используете XmlDataProvider?Я не думаю, что это ценный модульный тест, как сейчас.Вместо этого, почему бы вам не протестировать то, что вы бы сделали с этим поставщиком данных?

Например, если вы используете данные XML для загрузки списка Foo объектов, создайте интерфейс:

public interface IFooLoader
{
    IEnumerable<Foo> LoadFromFile(string fileName);
}

Затем вы можете протестировать реализацию этого класса, используя тестовый файл, сгенерированный во время модульного теста.Таким образом, вы можете сломать свою зависимость от файловой системы.Удалите файл при выходе из теста (в блоке finally).

А что касается соавторов, использующих этот тип, вы можете передать имитирующую версию.Вы можете либо вручную кодировать макет, либо использовать фреймворк, такой как Moq, Rhino, TypeMock или NMock.Насмешка - это замечательно, но если вы новичок в TDD, то хорошо, когда вы пишете код для ваших издевательств, пока вы изучаете, для чего они полезны.Если у вас есть это, то вы в хорошем положении, чтобы понять хорошие, плохие и уродливые насмешливые рамки.С ними может быть немного грубовато работать, когда вы запускаете TDD.Ваш пробег может варьироваться.

Удачи.

0 голосов
/ 01 июня 2012
  • Вместо того, чтобы возвращать XmlDataProvider, который связывает вас с конкретной технологией, скройте эту деталь реализации. Похоже, вам нужен репозиторий Роль для

    LocalizationData GetLocalizationData (params)

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

  • Остальная часть вашего кода может макетировать GetLocalizationData ()
0 голосов
/ 31 мая 2012

На этот раз не пытайтесь нарушить свою зависимость от файловой системы. Это поведение явно зависит от файловой системы и, по-видимому, находится в точке интеграции с файловой системой, поэтому протестируйте ее с файловой системой.

Теперь, я второй совет Боба: выбросьте этот код и попробуйте его протестировать. Это делает для большой практики, и именно так я научился делать это. Удачи.

0 голосов
/ 31 мая 2012

Итак, прежде всего, давайте поймем, что нам нужно проверить. Нам нужно проверить, что при наличии правильного имени файла ваш метод LoadFile (fn) возвращает XmlDataProvider, в противном случае он возвращает ноль.

Почему метод LoadFile () сложно протестировать? Поскольку создает XmlDataProvider с URI, созданным из имени файла. Я не много работал с C #, но я предполагаю, что если файл на самом деле не существует в системе, мы получим исключение. Настоящая проблема в том, что ваш производственный метод LoadFile () создает что-то, что трудно подделать . Невозможность подделать это проблема, потому что мы не можем обеспечить существование определенного файла во всех тестовых средах, без принудительного применения неявных рекомендаций.

Таким образом, решение состоит в том, что мы должны подделать коллабораторов (XmlDataProvider) метода loadFile. Однако, если метод создает своих соавторов, он не может имитировать их, поэтому метод никогда не должен создавать своих соавторов.

Если метод не создает своих соавторов, как он их получает? - Одним из этих двух способов:

  1. Они должны быть введены в метод
  2. Они должны быть получены с какого-то завода

В этом случае не имеет смысла вставлять XmlDataProvider в метод, поскольку это именно то, что он возвращает. Таким образом, мы должны получить его от глобальной фабрики - XmlDataProviderFactory.

А вот и интересная часть. Когда ваш код работает в рабочей среде, фабрика должна вернуть XmlDataProvider, а когда ваш код работает в тестовой среде, фабрика должна вернуть поддельный объект.

Теперь единственная часть головоломки состоит в том, как гарантировать, что фабрика ведет себя по-разному в разных условиях? Одним из способов является использование некоторых свойств, которые имеют разные значения в обеих средах, а другой способ - настроить фабрику для того, что она должна возвращать. Я лично предпочитаю первый способ.

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

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

Мне нравится @ ответ Стивена, за исключением того, что я думаю, что он не зашел достаточно далеко:

public interface DataProvider
{
    bool IsValidProvider();
    void DisableAsynchronousOperation();
}

public class XmlDataProvider : DataProvider
{
    private string fName;
    private bool asynchronousOperation = true;

    public XmlDataProvider(string fileName)
    {
        fName = fileName;
    }

    public bool IsValidProvider()
    {
        return fName.ToLower().EndsWith("xml");
    }

    public void DisableAsynchronousOperation()
    {
        asynchronousOperation = false;
    }
}


public class LocalizationData
{
    private DataProvider dataProvider;

    public LocalizationData(DataProvider provider)
    {
        dataProvider = provider;
    }

    public DataProvider Load()
    {
        if (provider.IsValidProvider())
        {
            provider.DisableAsynchronousOperation();
            return provider;
        }
        return null;
    }
}

Не заходя достаточно далеко, я имею в виду, что он не следовал за Last Possible Responsible Moment. Протолкните как можно больше в реализованный класс DataProvider.

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

Другое дело, что я попытался удалить зависимости от того, что LocalizationData знает, что провайдер использует файл. Что если это был веб-сервис или база данных?

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