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

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

Итак, вот проблема: класс доступа к данным сделан статическим, это не позволяет имитировать его и, как следствие, создавать настоящие модульные тесты. Чтобы исправить это, мне нужно создать интерфейс и реализовать его в классе доступа к данным. Также мне нужно будет добавить конструктор в класс бизнес-логики, который будет принимать параметр этого типа интерфейса. Это означает, что в конечном итоге я создам класс доступа к данным в методе приложения Main (), и что-то мне подсказывает, что это не лучший подход (действительно ли нормально, что точка входа должна знать о некоторых вещах, связанных с доступом к данным? Что, если цепочка намного длиннее или цепочек должно быть несколько?). Я знаю, что могу использовать какой-нибудь контейнер IoC, но я думаю, что это слишком простое приложение для использования контейнеров.

Спасибо!


person Dev    schedule 21.09.2009    source источник


Ответы (3)


Мне нужно создать интерфейс и реализовать его в классе доступа к данным. Также мне нужно будет добавить конструктор в класс бизнес-логики, который будет принимать параметр этого типа интерфейса. Это означает, что в конечном итоге я создам класс доступа к данным в методе приложения Main (), и что-то мне подсказывает, что это не лучший подход (действительно ли нормально, что точка входа должна знать о некоторых вещах, связанных с доступом к данным? Что, если цепочка намного длиннее или цепочек должно быть несколько?)

Напротив! Это лучший подход, по крайней мере, с точки зрения тестируемости.

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

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

Если цепочка намного длиннее или цепочек несколько, в этом нет ничего страшного (хотя вы можете подумать о свертывании некоторых уровней приложения, если это выйдет из-под контроля). Рассмотрим этот потенциальный код в View приложения Model-View-Presenter, где Presenter имеет зависимость от CustomerService, который имеет зависимость от Repository и зависимость от AccountingService (который также зависит от Repository):

public CustomerView() {
    IRespository       repository        = new ConcreteRepository();
    IAccountingService accountingService = new ConcreteAccountingService(repository);
    ICustomerService   customerService   = new ConcreteCustomerService(accountingService, repository)
    this._Presenter = new CustomerPresenter(customerService);
}

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

person Jeff Sternal    schedule 21.09.2009
comment
+1: Хороший ответ! Мне особенно нравится линия, где доллар останавливается. Хорошие отзывы о контейнерах DI! - person TrueWill; 21.09.2009
comment
@ Джефф - спасибо, я попробую. Хотя мое пессимистическое существо говорит мне, что здесь должны быть какие-то скрытые подводные камни :) - person Dev; 21.09.2009
comment
@Dev: Скептицизм рядом с благочестием! ;) - person Jeff Sternal; 21.09.2009
comment
Хорошо, я добавил интерфейс для доступа к данным и передачи их с верхнего уровня приложения. Работает отлично. Но как быть с доступом к файловой системе? Например, у меня есть функция, которая загружает данные из базы данных в набор данных, форматирует загруженные данные (форматы хранятся в каком-то внешнем XML-файле) и, наконец, сериализует набор данных в файл. - person Dev; 22.09.2009
comment
Итак, опять же, для поддержки тестируемости мне нужно переместить все функции, которые обращаются к файловой системе, в какой-то интерфейс. Но сначала - мне очень удобно просто вызвать dataset.WriteXml (файл), но для проверки мне нужно создать интерфейс и переместить dataset.WriteXml () в его реализацию, которая мне кажется ненужным слоем и делает код менее очевидным. . Во-вторых, если я перенесу все методы доступа к файловой системе в единый интерфейс, это нарушит принцип SRP, потому что сериализация \ десериализация наборов данных и чтение форматов данных из файла, кажется, являются разными обязанностями, верно? - person Dev; 22.09.2009
comment
Там есть над чем подумать, и я думаю, что стоит опубликовать дополнительный вопрос (я попытался опубликовать ответ в комментариях, но потребовалось три, чтобы плохо изложить свою точку зрения, поэтому я удалил их). Сказав это, если вам не хочется этого делать, есть несколько неприятных случаев, когда действительно сложно написать тестируемый код, не вводя в противном случае лишние интерфейсы (недавно у меня была эта проблема с событием SqlConnection InfoMessage). С другой стороны, иногда они кажутся излишними - ›Здесь есть еще кое-что, чтобы сказать о принципале единоличной ответственности! - person Jeff Sternal; 22.09.2009
comment
@Jeff - Спасибо, я разместил еще один вопрос - stackoverflow.com/questions/1461016/ Будем рады вашим комментариям) - person Dev; 22.09.2009

Предполагая, что вы используете LINQ to SQL, возможно, вы могли бы использовать шаблон репозитория, чтобы обернуть DataContext в интерфейс, который позже можно смоделировать, что сделает возможным модульное тестирование.

В Интернете есть несколько статей на эту тему, вот одна: http://andrewtokeley.net/archive/2008/07/06/mocking-linq-to-sql-datacontext.aspx

person Konamiman    schedule 21.09.2009
comment
Прямо сейчас я использую типизированный набор данных с настраиваемыми методами для чтения из файла и записи в файл. Я изначально не использовал entity framework или linq2sql, потому что в то время, когда мне приходилось писать это приложение, у меня не было много времени для экспериментов с технологиями, в которых я не имел опыта. Но это хорошая идея, может быть, я переделаю свой ДАЛ. - person Dev; 21.09.2009

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

  public void insert (...) {
      DataAccess.insert (...);
  }

Теперь вы можете игнорировать эти вызовы. Предлагаю разбить тесты так:

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

  2. В тестах макета просто соберите параметры, отправленные в insert(). Ни в коем случае не звоните DataAccess.

Тесты в №1 гарантируют, что запись данных в БД будет работать, а тесты в №2 будут гарантировать, что вы вызываете DataAccess с правильными значениями. Последние тесты будут выполняться очень быстро, что упростит тестирование особых случаев и т. Д.

Вам также не нужно постоянно запускать тесты с №1. Только когда вы что-то измените в DataAccess или перед выпуском. Это сделает тестирование эффективным и приятным.

person Aaron Digulla    schedule 21.09.2009
comment
добавил комментарий в качестве ответа, не было много места - person Dev; 21.09.2009
comment
@Dev - ответ может очень быстро запутать. В конце концов, ответы будут переупорядочены, Аарон может изменить свое имя или даже удалить свой ответ. Я бы рекомендовал отвечать в комментариях - если вам нужно больше места, чем выделенных символов, вы можете распределить ответ по двум комментариям. В качестве альтернативы вы можете отредактировать исходный вопрос, чтобы он содержал некоторую информацию, если считаете, что это поможет людям ответить вам. - person Jeff Sternal; 21.09.2009
comment
Спасибо, Джефф, перенесу этот ответ в комментарии. - person Dev; 21.09.2009
comment
@Aaron, Спасибо за быстрый ответ. Если я вас правильно понимаю, предложение состоит в том, чтобы обернуть класс доступа к данным некоторой оболочкой с виртуальными методами, чтобы я мог переопределить их для издевательства. Интересная идея, но кажется, что мне нужно будет продублировать все методы доступа к данным только с целью тестирования, и каждый раз, когда мне нужно добавить новый метод в класс доступа к данным, мне придется создавать соответствующую оболочку. Это нормально, если я создам один виртуальный метод в классе BL, который будет создавать объект доступа к данным? - person Dev; 21.09.2009
comment
Это нормально, если я создам один виртуальный метод в классе BL, который будет создавать объект доступа к данным? Примерно так: защищенный виртуальный IDataAccess CreateDataAccess () {return new DataAccess (); } Тогда я смогу переопределить единственный метод для имитации объекта доступа к данным. В чем недостатки такого подхода? - person Dev; 21.09.2009
comment
В моем случае мне просто пришлось переопределить метод executeUpdate(sql, param...), так что этого было достаточно. Если у вас много методов, используйте интерфейс. - person Aaron Digulla; 22.09.2009