Как модульно протестировать объект с помощью запросов к базе данных

StackOverflow https://stackoverflow.com/questions/30710

  •  09-06-2019
  •  | 
  •  

Вопрос

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

Я использую PHP и Python, но я думаю, что это вопрос, который применим к большинству / всем языкам, использующим доступ к базе данных.

Это было полезно?

Решение

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

Чтобы настроить ваши объекты для издевательства, вам, вероятно, нужно использовать какую-то инверсию шаблона внедрения управления / зависимости, как в следующем псевдокоде:

class Bar
{
    private FooDataProvider _dataProvider;

    public instantiate(FooDataProvider dataProvider) {
        _dataProvider = dataProvider;
    }

    public getAllFoos() {
        // instead of calling Foo.GetAll() here, we are introducing an extra layer of abstraction
        return _dataProvider.GetAllFoos();
    }
}

class FooDataProvider
{
    public Foo[] GetAllFoos() {
        return Foo.GetAll();
    }
}

Теперь в вашем модульном тестировании вы создаете макет FooDataProvider, который позволяет вам вызывать метод GetAllFoos без фактического обращения к базе данных.

class BarTests
{
    public TestGetAllFoos() {
        // here we set up our mock FooDataProvider
        mockRepository = MockingFramework.new()
        mockFooDataProvider = mockRepository.CreateMockOfType(FooDataProvider);

        // create a new array of Foo objects
        testFooArray = new Foo[] {Foo.new(), Foo.new(), Foo.new()}

        // the next statement will cause testFooArray to be returned every time we call FooDAtaProvider.GetAllFoos,
        // instead of calling to the database and returning whatever is in there
        // ExpectCallTo and Returns are methods provided by our imaginary mocking framework
        ExpectCallTo(mockFooDataProvider.GetAllFoos).Returns(testFooArray)

        // now begins our actual unit test
        testBar = new Bar(mockFooDataProvider)
        baz = testBar.GetAllFoos()

        // baz should now equal the testFooArray object we created earlier
        Assert.AreEqual(3, baz.length)
    }
}

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

Другие советы

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

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

В моих проектах на c # я использую NHibernate с полностью отдельным уровнем данных.Мои объекты находятся в базовой модели предметной области, и доступ к ним осуществляется с моего прикладного уровня.Прикладной уровень взаимодействует как с уровнем данных, так и с уровнем модели предметной области.

Прикладной уровень также иногда называют "Бизнес-уровнем".

Если вы используете PHP, создайте определенный набор классов для Только доступ к данным.Убедитесь, что ваши объекты понятия не имеют, как они сохраняются, и соедините их в ваших классах приложений.

Другим вариантом было бы использовать mocking / stubs.

Самый простой способ модульного тестирования объекта с доступом к базе данных - это использование областей транзакций.

Например:

    [Test]
    [ExpectedException(typeof(NotFoundException))]
    public void DeleteAttendee() {

        using(TransactionScope scope = new TransactionScope()) {
            Attendee anAttendee = Attendee.Get(3);
            anAttendee.Delete();
            anAttendee.Save();

            //Try reloading. Instance should have been deleted.
            Attendee deletedAttendee = Attendee.Get(3);
        }
    }

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

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

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

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

Сначала мы создали уровень абстракции, который позволял нам "вставлять" любое разумное соединение с базой данных (в нашем случае мы просто поддерживали одно соединение типа ODBC).

Как только это было внедрено, мы смогли сделать что-то подобное в нашем коде (мы работаем на C ++, но я уверен, что вы поняли идею).:

getDatabase().ExecuteSQL( "ВСТАВИТЬ В foo (бла-бла-бла )" )

При обычном выполнении getDatabase() вернет объект, который передает все наши sql (включая запросы) через ODBC непосредственно в базу данных.

Затем мы начали изучать базы данных в памяти - лучшей, по-видимому, является SQLite.(http://www.sqlite.org/index.html).Он удивительно прост в настройке и использовании и позволил нам создать подкласс и переопределить функцию getDatabase() для пересылки sql в базу данных в памяти, которая создавалась и уничтожалась для каждого выполненного теста.

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

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

Очевидно, что наш опыт был сосредоточен вокруг среды разработки на C ++, однако я уверен, что вы могли бы получить нечто подобное, работающее под управлением PHP / Python.

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

Книга Тестовые шаблоны xUnit описывает некоторые способы обработки кода модульного тестирования, который попадает в базу данных.Я согласен с другими людьми, которые говорят, что вы не хотите этого делать, потому что это медленно, но вы должны это когда-нибудь сделать, ИМО.Имитация подключения к базе данных для тестирования материалов более высокого уровня - хорошая идея, но ознакомьтесь с этой книгой, чтобы узнать, что вы можете сделать для взаимодействия с реальной базой данных.

Варианты, которые у вас есть:

  • Напишите скрипт, который удалит базу данных перед запуском модульных тестов, затем заполните базу данных предопределенным набором данных и запустите тесты.Вы также можете делать это перед каждым тестированием – это будет медленно, но менее подвержено ошибкам.
  • Внедрите базу данных.(Пример на псевдо-Java, но применим ко всем OO-языкам)

    class Database {
     public Result query(String query) {... real db here ...}
    }

    класс MockDatabase расширяет базу данных { запрос общедоступного результата (строковый запрос) { возвращает "макет результата";} }

    класс ObjectThatUsesDB { общедоступный ObjectThatUsesDB(база данных db) { this.database = db;} }

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

  • Вообще не используйте DB на протяжении большей части кода (в любом случае, это плохая практика).Создайте объект "базы данных", который вместо возврата с результатами будет возвращать обычные объекты (т. е.вернется User вместо кортежа {name: "marcin", password: "blah"}) напишите все ваши тесты с помощью специальных построенных реальный объекты и напишите один большой тест, который зависит от базы данных, которая гарантирует, что это преобразование работает нормально.

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

Обычно я стараюсь разбить свои тесты между тестированием объектов (и ORM, если таковые имеются) и тестированием базы данных.Я тестирую объектную сторону вещей, имитируя вызовы доступа к данным, в то время как я тестирую сторону БД, тестируя взаимодействие объекта с БД, которое, по моему опыту, обычно довольно ограничено.

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

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

Простая форма IoC может быть выполнена просто путем кодирования интерфейсов.Для этого требуется какая-то объектная ориентация в вашем коде, поэтому это может не относиться к тому, что вы делаете (я говорю это, поскольку все, что мне нужно продолжить, - это ваше упоминание PHP и Python)

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

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

Вы могли бы использовать насмешливые фреймворки чтобы абстрагироваться от ядра базы данных.Я не знаю, есть ли что-то в PHP / Python, но для типизированных языков (C #, Java и т.д.) Есть много вариантов

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

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

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

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

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

Извините, у меня нет каких-либо конкретных примеров кода для PHP / Python, но если вы хотите увидеть пример .NET, у меня есть Публикация это описывает технику, которую я использовал для проведения того же самого тестирования.

Настройка тестовых данных для модульных тестов может оказаться непростой задачей.

Когда дело доходит до Java, если вы используете Spring API для модульного тестирования, вы можете управлять транзакциями на уровне модуля.Другими словами, вы можете выполнять модульные тесты, которые включают обновления / вставки / удаления базы данных и откат изменений.В конце выполнения вы оставляете все в базе данных таким, каким оно было до начала выполнения.Для меня это настолько хорошо, насколько это вообще возможно.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top