Доступ к файлам, модульное тестирование, внедрение зависимостей - PullRequest
2 голосов
/ 22 сентября 2009

Я недавно задал вопрос о лучших способах отделения бизнес-логики от доступа к данным, чтобы сделать приложение тестируемым ( здесь ). Благодаря Джеффу Стерну я создал интерфейс для доступа к данным и передачи его конкретной реализации от верхнего уровня приложения к BL. Но сейчас я пытаюсь понять, как я могу отделить методы доступа к файлам от бизнес-логики.

Допустим, у меня есть функция, которая загружает данные из базы данных в набор данных, форматирует загруженные данные (форматы хранятся в каком-то внешнем XML-файле) и, наконец, сериализует набор данных в файл. Итак, для поддержки тестируемости мне нужно переместить все функции, которые обращаются к файловой системе, на какой-то интерфейс. Но сначала - я нахожу весьма удобным просто вызывать dataset.WriteXml (file), но для тестируемости я должен создать интерфейс и переместить dataset.WriteXml () в его реализацию, которая выглядит для меня как ненужный слой и делает код менее очевидным , И второе - если я перенесу все методы, которые обращаются к файловой системе, к одному интерфейсу, это нарушит принцип SRP, потому что сериализация \ десериализация наборов данных и чтение форматов данных из файла, кажется, разные обязанности, верно?

Ответы [ 5 ]

2 голосов
/ 22 сентября 2009

Я думаю, вам нужно немного больше разделить ваш код ...

Вы говорите: Допустим, у меня есть функция, которая

  1. загружает данные из базы данных в набор данных,
  2. форматирует загруженные данные (форматы хранятся в каком-то внешнем XML-файле) и
  3. наконец сериализует набор данных в файл.

Звучит как минимум 3-4 работы ...

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

Если вы просто хотите использовать Dataset.WriteXML, тогда вам не нужно это проверять. Это функция Framework, которая работает довольно хорошо. Попробуй сделать несколько насмешек, чтобы подделать это. Как именно зависит от вашего решения ...

Ответ на комментарий:

Создание всех этих небольших классов с их собственными тестами облегчит тестирование, а также сделает ваши функции небольшими и компактными (-> легко тестировать). Вы бы проверили, является ли содержимое набора данных именно тем, что вам нужно, а не если набор данных правильно сериализован в XML-файл. Вы также проверите, правильно ли ваш форматер выполняет свою функцию, без каких-либо зависимостей от какой-либо другой логики. Вы также протестировали бы доступ к данным, но без доступа к БД (снова заглушки / пробки)

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

Сложная часть о юнит-тестировании - это получение значимых тестов. Они должны быть "быстрыми" и "простыми"

Чтобы сделать тесты быстрыми, вы не должны касаться вещей, которые вы не можете контролировать:

  • Webservices
  • файлсистемах
  • Базы данных
  • Com Objects

Чтобы упростить их, вы сохраняете свои предложения сосредоточенными на одной задаче, на которой начинается SRP, о которой вы уже упоминали. Взгляните на этот ответ ... Он также укажет на другие принципы разработки "SOLID"

https://stackoverflow.com/questions/1423597/solid-principles/1423627#1423627

1 голос
/ 22 сентября 2009

Перейти с дополнительными классами. При тестировании легче:

  • Для экспортера / автора набора данных - проверьте, что измененные данные передаются в устройство записи
  • Для форматера - проверьте логику форматирования

Это не значит, что вы не:

  • Используйте dataSet.WriteXml, вы просто делаете это в классе v. Simple ... в качестве однострочника, вам не нужно добавлять целенаправленный интеграционный тест для него ... но если вы в магазине скажете во внешней системе вы можете сделать это в другой реализации, не затрагивая другой код
  • Используйте отдельные классы для экспортера / записи набора данных, чем для простого средства записи файлов.

Пока вы используете v. Простое внедрение зависимостей, вы на самом деле просто держите все просто ... что даст вам лучшие результаты как в краткосрочной, так и в долгосрочной перспективе.

Ps. важно избегать попадания в файловую систему во время тестов, так как при увеличении количества тестов вы хотите, чтобы они выполнялись v. fast, поэтому не заблуждайтесь в отношении того, что сначала появляется при «быстрых» доступах для теста

1 голос
/ 22 сентября 2009

Для краткого изложения, если вы действительно хотите сделать этот тестируемый, я рекомендую:

  1. Извлечение форматирования данных код в новый класс (который реализует интерфейс, который вы можете использовать издеваться).
  2. Проходя этот класс, ваш DataSet.
  3. Заставить новый класс вернуть DataSet как IXmlSerializable что вы тоже можете издеваться.

Не видя текущий код, я должен сделать некоторые предположения (надеюсь, не слишком много!) - так что, возможно, текущий код выглядит примерно так:

public class EmployeeService {

    private IEmployeeRepository _Repository;

    public EmployeeService(IRepository repository) {
        this._Repository = repository;
    }

    public void ExportEmployeeData(int employeeId, string path) {

        DataSet dataSet = this._Repository.Get(employeeId);
        // ... Format data in the dataset here ...
       dataSet.WriteXml(path);
    }
}

Этот код прост и эффективен, но не тестируем (без побочных эффектов). Кроме того, наличие всей этой логики в одном месте является нарушением принципала единой ответственности.

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

Однако, если вы хотите немного больше разделить обязанности и сделать их тестируемыми, вы можете сделать это, переместив логику форматирования в свой собственный класс (который будет реализовывать IEmployeeDataSetFormatter), а затем внедрить IEmployeeDataSetFormatter в этот метод call (альтернативно, мы могли бы внедрить его в конструктор сервиса, как IEmployeeRepository). Метод, который форматирует данные, вернет IXmlSerializable, поэтому мы можем смоделировать его для безопасного изолированного тестирования:

public interface IEmployeeDataSetFormatter {
    IXmlSerializable FormatForExport(DataSet dataSet);
}

public class EmployeeDataSetFormatter: IEmployeeDataSetFormatter {
    public IXmlSerializable FormatForExport(DataSet dataSet) {
        // ... Format data in the dataset here ...
        return (IXmlSerializable) dataSet;
    }
}

public void ExportEmployeeData2(int employeeId, string path, IEmployeeDataSetFormatter formatter) {

    DataSet dataSet = this._Repository.Get(employeeId);

    IXmlSerializable xmlSerializable = formatter.FormatForExport(dataSet);

    // This is still an intermediary step - it's probably worth
    // moving this logic into its own class so you don't have to deal
    // with the concrete FileStream underlying the XmlWriter here
    using (XmlWriter writer = XmlWriter.Create(path)) {
        xmlSerializable.WriteXml(writer);
    }
}

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

0 голосов
/ 22 сентября 2009

Вы можете использовать версии WriteXml(), которые принимают Stream или TextWriter, и настроить свой код так, чтобы этот объект передавался в ваш код, а затем для тестирования проходить в фиктивном объекте.

0 голосов
/ 22 сентября 2009

Из вашего описания не совсем понятно, что делает ваш код. Что самое важное, что делает ваш код? Это fomatting? Я бы сказал, не пытайтесь проверить написание XML. Как бы вы это проверили?

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

...