Как эффективно протестировать анализатор плоских файлов фиксированной длины с помощью MSpec?

У меня есть подпись этого метода: List<ITMData> Parse(string[] lines)

ITMData имеет 35 свойств.

Как бы вы эффективно протестировали такой синтаксический анализатор?

Вопросы:

  • Должен ли я загрузить весь файл (могу ли я использовать System.IO)?
  • Должен ли я поместить строку из файла в строковую константу?
  • Должен ли я проверить одну или несколько строк
  • Должен ли я тестировать каждое свойство ITMData или должен тестировать весь объект?
  • Как насчет названия моего теста?

ИЗМЕНИТЬ

Я изменил сигнатуру метода на ITMData Parse(string line).

Тестовый код:

[Subject(typeof(ITMFileParser))]
public class When_parsing_from_index_59_to_79
{
    private const string Line = ".........";
    private static ITMFileParser _parser;
    private static ITMData _data;

    private Establish context = () => { _parser = new ITMFileParser(); };

    private Because of = () => { _data = _parser.Parse(Line); };

    private It should_get_fldName = () => _data.FldName.ShouldBeEqualIgnoringCase("HUMMELDUMM");
}

ИЗМЕНИТЬ 2

Я все еще не уверен, следует ли мне тестировать только одно свойство для каждого класса. На мой взгляд, это позволяет мне предоставить дополнительную информацию для спецификации, а именно то, что когда я анализирую одну строку от индекса 59 до индекса 79, я получаю fldName. Если я проверю все свойства в одном классе, я потеряю эту информацию. Я преувеличиваю свои тесты?

Мои тесты теперь выглядят так:

[Subject(typeof(ITMFileParser))]
public class When_parsing_single_line_from_ITM_file
{
    const string Line = ""

    static ITMFileParser _parser;
    static ITMData _data;

    Establish context = () => { _parser = new ITMFileParser(); };

    private Because of = () => { _data = _parser.Parse(Line); };

    It should_get_fld??? = () => _data.Fld???.ShouldEqual(???);
    It should_get_fld??? = () => _data.Fld???.ShouldEqual(???);
    It should_get_fld??? = () => _data.Fld???.ShouldEqual(???);
    It should_get_fld??? = () => _data.Fld???.ShouldEqual(???);
    It should_get_fld??? = () => _data.Fld???.ShouldEqual(???);
    It should_get_fld??? = () => _data.Fld???.ShouldEqual(???);
    It should_get_fld??? = () => _data.Fld???.ShouldEqual(???);
    ...

}

person Community    schedule 16.08.2011    source источник
comment
Что именно вы подразумеваете под эффективно? Вы хотите минимизировать время разработки модульных тестов?   -  person Daniel B    schedule 16.08.2011
comment
ну т.е. тестирование с одной строкой или со многими строками? тестирование со строкой, которая содержит только те значения, которые я хочу подтвердить, или взять всю строку.   -  person Rookian    schedule 16.08.2011
comment
Я все еще не уверен на 100%, в чем именно заключается вопрос, но я бы создал набор различных модульных тестов. Один тестовый метод может передать конкретную единственную строку и подтвердить, что она была правильно проанализирована в соответствующие свойства результирующего объекта IMTData. Возможно, еще один тест, который проверяет 20 строк, создает список из 20 элементов. У меня могут быть другие методы для конкретных сложных случаев. Это в значительной степени то, что описывал @Kenny, базовые вещи модульного тестирования. Возьмите требования к этому методу Parse и перепроектируйте его в несколько тестовых случаев, которые докажут правильность.   -  person Daniel B    schedule 16.08.2011
comment
Я не знаю, как вы хотите это проверить, но сигнатура метода — это ошибка. Не имеет смысла принимать массив строк для разбора; это побуждает людей читать весь файл в память и помещать его в массив, что почти наверняка глупо. Вероятно, было бы более уместно принять либо TextReader, либо IEnumerable<string>. (Возврат списка тоже подозрительный, если вы можете обойтись ленивым синтаксическим анализом построчно.)   -  person mqp    schedule 19.08.2011
comment
Чтение всего файла в память не глупо. Это зависит от размера файла. Мне приходится работать с файлами размером около 20 МБ. Огромные файлы не должны загружаться в память.   -  person Rookian    schedule 20.08.2011
comment
@Daniel B, можно ли проверить спецификацию, согласно которой при анализе индексов с 59 по 79 должно быть получено свойство fldName, или это своего рода чрезмерная спецификация? Я не уверен, должен ли я тестировать весь объект один раз или проверять каждое свойство в отдельном тестовом классе.   -  person Rookian    schedule 23.08.2011
comment
Я разместил ответ ниже, надеюсь, он ответит на ваш вопрос.   -  person Daniel B    schedule 24.08.2011


Ответы (4)


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

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

  1. Я бы не стал включать загрузку файлов в парсер. Как предложил @mquander, я предпочел бы использовать TextReader или IEnumerable в качестве входного параметра. Это приведет к более быстрому тестированию, поскольку вы можете указать ввод парсера в памяти и не должны касаться файловой системы.
  2. Я не большой поклонник ручных тестовых данных, поэтому в большинстве случаев я использую встроенные ресурсы и ResourceManager для загрузки тестовых данных непосредственно из сборки спецификации с помощью Assembly.GetManifestResource(). Обычно в моем решении есть несколько методов расширения для упрощения чтения ресурсов (что-то вроде TextReader TextResource.Load("NAME_OF_SOME_RESOURCE")).
  3. Что касается MSpec: я использую один класс для каждого файла для анализа. Для каждого свойства, которое проверяется в проанализированном результате, у меня есть отдельное утверждение (It). Обычно это однострочные, поэтому дополнительный объем кода не так уж велик. С точки зрения документации и диагностики imho это огромный плюс, поскольку, когда свойство анализируется неправильно, вы можете напрямую увидеть, какое утверждение не удалось, без необходимости заглядывать в источник или искать номера строк. Он также отображается в файле результатов MSpec. Кроме того, вы не скрываете другие ошибочные утверждения (ситуация, когда вы исправляете одно утверждение только для того, чтобы увидеть, что спецификация не работает в следующей строке со следующим утверждением). Это, конечно, заставляет вас больше думать о формулировках, которые вы используете в своих спецификациях, но для меня это также огромный плюс, поскольку я сторонник идеи о том, что язык формирует мышление. Другими словами, если вы понятия не имеете, как, черт возьми, назвать ваше утверждение, вероятно, что-то подозрительное либо в вашей спецификации, либо в вашей реализации.
  4. Что касается сигнатуры вашего метода для синтаксического анализатора: я бы не стал возвращать конкретный тип, такой как List‹T› или массив, и я бы также предложил не возвращать изменяемый тип List‹T›. В основном вы говорите здесь: «Эй, вы можете возиться с результатом синтаксического анализа после того, как я закончу», что в большинстве случаев, вероятно, вам не нужно. Я бы предложил вместо этого вернуть IEnumerable‹T› (или ICollection‹T›, если вам ДЕЙСТВИТЕЛЬНО нужно изменить его впоследствии)
person Community    schedule 31.08.2011
comment
пока спасибо :) 1. Я анализирую одну строку, потому что каждая строка анализируется одинаково 2. Я буду беспокоиться о размещении моих тестовых данных в ресурсах. 3. Я не могу использовать один класс для каждого типа файлов, потому что есть какая-то особая логика, которая зависит от конфигурации. Поэтому некоторые свойства должны быть проверены отдельно. 4. Я решил разобрать только одну строку. - person Rookian; 01.09.2011

Должен ли я загрузить весь файл (могу ли я использовать System.IO)?

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

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

Должен ли я поместить строку из файла в строковую константу?

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

Должен ли я протестировать одну или несколько строк?

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

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

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

person Joe White    schedule 18.08.2011

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

person kakridge    schedule 16.08.2011

Что касается ваших новых вопросов:

Должен ли я тестировать каждое свойство ITMData или должен тестировать весь объект?

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

Как насчет названия моего теста?

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

public void Check_All_Properties_Parsed_Correctly(){.....}

public void Exception_Thrown_If_Lines_Is_Null(){.....}

public void Exception_Thrown_If_Lines_Is_Wrong_Length(){.....}

То есть, другими словами, тестирование именно того поведения, которое вы считаете «правильным» для парсера. Как только это будет сделано, вы почувствуете себя намного спокойнее при внесении изменений в код синтаксического анализатора, потому что у вас будет комплексный набор тестов для проверки того, что вы ничего не сломали. Не забывайте часто тестировать и обновлять тесты при внесении изменений! На MSDN.

В общем, я думаю, вы можете найти ответы на большинство своих вопросов, немного погуглив. Есть также несколько отличных книг по разработке через тестирование, в которых объясняется не только как TDD, но и почему. Если вы относительно независимы от языка программирования, я бы порекомендовал тест Кента Бека Test Разработка на основе примеров, иначе что-то вроде Test- Управляемая разработка в Microsoft .NET. Они должны привести вас на правильный путь очень быстро.

РЕДАКТИРОВАТЬ:

Я преувеличиваю свои тесты?

На мой взгляд, да. В частности, я не согласен с вашей следующей строкой:

Если я проверяю все свойства в одном классе, я теряю эту информацию.

Каким именно образом вы теряете информацию? Допустим, есть 2 способа выполнить этот тест, кроме создания нового класса для каждого теста:

  1. Используйте разные методы для каждого свойства. Ваши методы тестирования могут называться CheckPropertyX, CheckPropertyY и т. д. Когда вы запускаете тесты, вы точно увидите, какие поля прошли успешно, а какие нет. Это явно удовлетворяет вашим требованиям, хотя я бы сказал, что это все еще излишество. Я бы пошел по варианту 2:
  2. Используйте несколько разных методов, каждый из которых проверяет один конкретный аспект. Это то, что я изначально рекомендовал, и я думаю, что вы имеете в виду. Если один из тестов не пройден, вы получите информацию только о том, что не удалось сделать первым, для каждого метода, но если вы правильно написали свой Assert, вы будете точно знать, какое свойство неверно. Рассмотрим следующий код:

Assert.AreEqual("test1", myObject.PropertyX, "Property X was incorrectly parsed"); Assert.AreEqual("test2", myObject.PropertyY, "Property Y was incorrectly parsed");

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

person Daniel B    schedule 24.08.2011
comment
ОК, я ответил на ваше редактирование, я бы порекомендовал создать максимум новый метод для каждого свойства, чтобы проверить это свойство. - person Daniel B; 30.08.2011
comment
Каким именно образом вы теряете информацию? Machine.Specification позволяет создавать отчеты в формате HTML. Как видите, мои тесты начинаются с When_... Итак, имя класса является частью спецификации. Если бы я написал класс для каждого свойства, у меня была бы конкретная спецификация, которая содержит, какой индекс используется для получения определенного свойства. - person Rookian; 30.08.2011