2010-09-09 5 views
6

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

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); 
    } 
} 

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

+0

я думаю, что некоторые поддельные объект должен быть использовать вместо XmlDataProvider – Polaris

+0

вы можете быть лучше тратить время на код, который является более сложным и вспомогательная скважина тестировать. Подобная ситуация может быть лучше охвачена испытаниями на уровне системы. –

+0

Не забудьте отметить свой любимый ответ ;-) – Steven

ответ

2

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

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

+1

Это интересный идеал, но я думаю, что этот подход плохо подходит для приложения C#. Я думаю, что должен быть создан некоторый объект-заглушка – Polaris

+1

Любой тест, который вызывает файловую систему, - это * не * единичный тест. –

+2

@Billy ONeal: все, что тестирует единицы (или модули), является модульным тестированием IMO, если входные параметры остаются неизменными. Если входные параметры слишком велики или модуль выполняет файловые операции (например, модуль для управления временными файлами), то модульный тест должен читать или записывать файлы. – AndiDog

0

В этом случае вы в основном находитесь на более низком уровне зависимости. Вы тестируете, что файл существует и что xmlprovider может быть создан с файлом в качестве источника.

Единственный способ, которым вы могли бы разбить зависимость, - это что-то сделать, чтобы создать XmlDataProvider. Вы могли бы затем издеваться над ним, чтобы вернуть созданный вами XmlDataProvider (в отличие от чтения). Как упрощенный пример может быть:

class XmlDataProviderFactory 
{ 
    public virtual XmlDataProvider NewXmlDataProvider(string fileName) 
    { 
     return new XmlDataProvider 
        { 
         IsAsynchronous = false, 
         Source = new Uri(fileName, UriKind.Absolute) 
        }; 
} 

class XmlDataProviderFactoryMock : XmlDataProviderFactory 
{ 
    public override XmlDataProvider NewXmlDataProvider(string fileName) 
    { 
     return new XmlDataProvider(); 
    } 
} 

public class LocalizationData 
{ 
... 
    public XmlDataProvider LoadFile(string fileName, XmlDataProviderFactory factory) 
     { 
      if (IsValidFileName(fileName)) 
      { 
       return factory.NewXmlDataProvider(fileName); 
      } 
      return null; 
     } 
} 

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

} 

Использование рамки инъекции может упростить вызов LoadFile путем введения завода в конструкторе класса или в других местах.

+0

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

+0

@ KeithS. Я согласен. Это был скорее пример того, как добиться развязки с помощью макетного объекта. – Rod

1

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

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

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

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

Что касается соавторов, которые используют этот тип, вы можете перейти в макетную версию. Вы можете либо передать код макета, либо использовать насмешливую структуру, такую ​​как Moq, Rhino, TypeMock или NMock. Mocking - это здорово, но если вы новичок в TDD, тогда вы можете скомпоновать свои макеты, когда узнаете, для чего они полезны. Как только у вас это получится, вы сможете найти хорошее, плохое и уродливое из насмешливых фреймворков. Они могут быть немного грубыми, чтобы работать, когда вы начинаете TDD. Ваш пробег может отличаться.

Удачи.

8

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

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 в вашем рабочем коде себя, потому что этот класс, вероятно, используется другими классами, которые зависят от этого типа. То, что вы обычно делаете, это попросить фреймворк создать для вас самый верхний класс (например, команду, которая реализует полный прецедент) и выполнить его.

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

+0

Вы можете представить все фабрики, которые хотите, но если вы обрабатываете параметр 'Create()' как имя файла, тогда вы зависите от файловой системы. Я тоже это делал, но я интерпретирую его как ложную абстракцию. Некоторые назвали бы это «негерметичной» абстракцией Структурно, вы не зависите от файловой системы, но код по-прежнему работает только с файловой системой, потому что 'fileName' ничего не значит. –

0

I Like @ ответ Стивена только я думаю, что он не пошел достаточно далеко:

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 знает, что поставщик использует файл. Что делать, если это веб-служба или база данных?

2

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

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

х: Я думаю, что Стивен покрыты эту тему довольно хорошо.

0

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

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

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

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

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

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

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

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

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

3

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

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) 
            }; 
    } 
} 
  • (.!. * Я не в восторге от возвращения нулевой Тьфу, что запахи)

Во всяком случае, я хотел бы спросить следующие вопросы для меня:

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

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

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

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

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

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

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

+0

Отличный ответ в целом, но я не уверен, что он подходит для новичок в TDD. Вы и я agre e на этом, но представьте себя 10 лет назад. Что бы вы сделали? –

+1

Я не уверен, что бы я сделал 10 лет назад. Но это то, что я буду делать сейчас, и я бы рекомендовал, чтобы другие делали это. –

6

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

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

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

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

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

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

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

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

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

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

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

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

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

0

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

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

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

    LocalizationData GetLocalizationData (Params)

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

  • Остальная часть кода может издеваться из GetLocalizationData()
Смежные вопросы