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


153

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

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

Ответы:


82

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

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

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

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


Я знаю, что это старый, но как насчет создания дубликата таблицы к той, которая уже находится в БД. Таким образом, вы можете подтвердить работу вызовов БД?
bretterer

1
Я использую PDO PHP как самый низкий уровень доступа к базе данных, через который я извлек интерфейс. Затем я построил слой базы данных с поддержкой приложений. Это слой, который содержит все необработанные запросы SQL и другую информацию. Остальная часть приложения взаимодействует с этой высокоуровневой базой данных. Я нашел, что это работает довольно хорошо для модульного тестирования; Я тестирую страницы своего приложения на предмет их взаимодействия с базой данных приложения. Я проверяю базу данных своего приложения на предмет ее взаимодействия с PDO. Я предполагаю, что PDO работает без ошибок. Исходный код: manx.codeplex.com
узаконить

1
@bretterer - Создание дублирующейся таблицы хорошо для интеграционного тестирования. Для модульного тестирования вы обычно используете фиктивный объект, который позволит вам протестировать блок кода независимо от базы данных.
BornToCode

2
Какова ценность в моделировании вызовов базы данных в ваших модульных тестах? Это не кажется полезным, потому что вы можете изменить реализацию, чтобы получить другой результат, но ваш модульный тест (неправильно) пройдет.
bmay2

2
@ bmay2 Ты не ошибаешься. Мой первоначальный ответ был написан давным-давно (9 лет!), Когда многие люди не писали свой код тестируемым образом, и когда инструментов для тестирования сильно не хватало. Я бы не рекомендовал этот подход больше. Сегодня я бы просто создал тестовую базу данных и наполнил ее данными, необходимыми для теста, и / или разработал свой код, чтобы я мог тестировать как можно больше логики без базы данных вообще.
Даг Р

25

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

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

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

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

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

Другой вариант будет использовать насмешки / заглушки.


Я всегда соглашался с этим, но на практике из-за крайних сроков и «хорошо, теперь добавьте еще одну функцию, к 14:00 сегодня», это одна из самых трудных вещей, которую нужно достичь. Подобные вещи являются главной целью рефакторинга, хотя, если мой босс решит, что он не подумал о 50 новых возникающих проблемах, которые требуют совершенно новой бизнес-логики и таблиц.
Даррен Рингер

3
Если ваши объекты тесно связаны с вашим слоем данных, сложно провести надлежащее модульное тестирование. первая часть юнит-теста, это «юнит». Все устройства должны быть в состоянии быть проверены в изоляции. хорошее объяснение
Амитабха

11

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

Например:

    [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, другая, которая впоследствии запускается для обработки модульных тестов, анализа кода, упаковки и т. Д.


1
+1 Экономит много времени при юнит-тестировании слоев доступа к данным. Просто отметьте, что для TS часто требуется MSDTC, который может быть нежелателен (в зависимости от того, нужен ли MSDTC вашему приложению)
StuartLC

Первоначальный вопрос был о PHP, этот пример выглядит как C #. Среды очень разные.
узаконить

2
Автор вопроса заявил, что это общий вопрос, относящийся ко всем языкам, имеющим отношение к БД.
Ведран

9
и это, дорогие друзья, называется интеграционными тестами
АА.

10

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

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

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

GetDatabase (). ExecuteSQL («ВСТАВИТЬ В foo (бла, бла)»)

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

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

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

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

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

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


9

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

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


6

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


4

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

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

    База данных класса {
     запрос общего результата (строковый запрос) {... real db here ...}
    }

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

    class ObjectThatUsesDB { public ObjectThatUsesDB (База данных, дБ) { this.database = db; } }

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

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

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


3

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

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

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

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

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


2

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

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


2

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

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

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


2

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


2

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

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


2

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

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.