Статьи

Написание тестов для кода доступа к данным — вопросы данных

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

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

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

Вот почему я решил написать этот пост в блоге.

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

Три смертных греха из наборов данных DbUnit

Самая распространенная причина, по которой такие библиотеки, как DbUnit, имеют такую ​​плохую репутацию, заключается в том, что разработчики используют их неправильно и жалуются на то, что застрелились в ногу.

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

Есть три распространенных (и дорогостоящих) ошибки, которые мы можем совершать, когда используем наборы данных DbUnit:

1. Инициализация базы данных с использованием одного набора данных

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

Скорее всего, наше приложение имеет много функций и большую базу данных с десятками (или сотнями) таблиц базы данных. Если мы используем этот подход в реальном программном проекте, наш набор данных будет ОГРОМНЫМ, потому что:

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

Размер нашего набора данных является большой проблемой, потому что:

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

пример

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

1
2
3
4
5
6
7
8
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
    <cities id="1" name="Helsinki"/>
     
    <customers id="1" city_id="1" name="Company A"/>
     
    <offices id="1" city_id="1" name="Office A"/>
</dataset>

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

01
02
03
04
05
06
07
08
09
10
11
12
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
    <cities id="1" name="Helsinki"/>
    <cities id="2" name="Tampere"/>
    <cities id="3" name="Turku"/>
     
    <customers id="1" city_id="1" name="Company A"/>
    <customers id="2" city_id="2" name="Company B"/>
     
    <offices id="1" city_id="1" name="Office A"/>
    <offices id="2" city_id="3" name="Office B"/>
</dataset>

Как мы можем видеть,

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

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

2. Создание одного набора данных для каждого теста или группы тестов

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

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

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

Поддержание наших наборов данных становится адом.

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

пример

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

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

1
2
3
4
5
6
7
8
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
    <cities id="1" name="Helsinki"/>
    <cities id="2" name="Tampere"/>
     
    <customers id="1" city_id="1" name="Company A"/>
    <customers id="2" city_id="2" name="Company B"/>
</dataset>

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

1
2
3
4
5
6
7
8
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
    <cities id="1" name="Helsinki"/>
    <cities id="3" name="Turku"/>
     
    <offices id="1" city_id="1" name="Office A"/>
    <offices id="2" city_id="3" name="Office B"/>
</dataset>

Что произойдет, если мы внесем изменения в структуру таблицы городов ?

Точно! Вот почему следовать этому подходу не очень хорошая идея.

3. Утверждая все

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

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

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

Однако у этого подхода есть три недостатка:

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

пример

Давайте предположим, что нам нужно написать тесты для функции, которая обновляет информацию о клиенте (идентификатор обновленного клиента равен 2).

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

1
2
3
4
5
6
7
8
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
    <cities id="1" name="Helsinki"/>
    <cities id="2" name="Tampere"/>
     
    <customers id="1" city_id="1" name="Company A"/>
    <customers id="2" city_id="2" name="Company B"/>
</dataset>

Набор данных, обеспечивающий сохранение правильной информации в базе данных, выглядит следующим образом:

1
2
3
4
5
6
7
8
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
    <cities id="1" name="Helsinki"/>
    <cities id="2" name="Tampere"/>
     
    <customers id="1" city_id="1" name="Company A"/>
    <customers id="2" city_id="1" name="Company B"/>
</dataset>

Давайте рассмотрим недостатки этого решения по одному:

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

Наборы данных сделаны правильно

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

Давайте начнем с более внимательного изучения требований хорошего набора тестов. Требования хорошего тестового набора:

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

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

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

Если мы хотим выполнить эти требования, мы должны следовать этим правилам:

1. Используйте небольшие наборы данных

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

пример

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

1
2
3
4
5
6
7
8
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
    <cities id="1" name="Helsinki"/>
    <cities id="2" name="Tampere"/>
     
    <customers id="1" city_id="1" name="Company A"/>
    <customers id="2" city_id="2" name="Company B"/>
</dataset>

С другой стороны, набор данных, который инициализирует нашу базу данных при выполнении тестов, которые тестируют связанные с офисом функции, выглядит следующим образом:

1
2
3
4
5
6
7
8
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
    <cities id="1" name="Helsinki"/>
    <cities id="3" name="Turku"/>
     
    <offices id="1" city_id="1" name="Office A"/>
    <offices id="2" city_id="3" name="Office B"/>
</dataset>

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

1
2
3
4
5
6
7
8
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
    <cities id="1" name="Helsinki"/>
    <cities id="2" name="Tampere"/>
     
    <offices id="1" city_id="1" name="Office A"/>
    <offices id="2" city_id="2" name="Office B"/>
</dataset>

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

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

2. Разделите большие наборы данных на меньшие наборы данных

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

Мы можем избавиться от этой проблемы, выполнив следующие действия:

  1. Определите данные, которые используются в более чем одном наборе данных.
  2. Переместите эти данные в отдельный набор данных (или в несколько наборов данных).

пример

У нас есть два набора данных, которые выглядят следующим образом (общие данные выделены):

1
2
3
4
5
6
7
8
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
    <cities id="1" name="Helsinki"/>
    <cities id="2" name="Tampere"/>
     
    <customers id="1" city_id="1" name="Company A"/>
    <customers id="2" city_id="2" name="Company B"/>
</dataset>
1
2
3
4
5
6
7
8
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
    <cities id="1" name="Helsinki"/>
    <cities id="2" name="Tampere"/>
     
    <offices id="1" city_id="1" name="Office A"/>
    <offices id="2" city_id="2" name="Office B"/>
</dataset>

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

1
2
3
4
5
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
    <cities id="1" name="Helsinki"/>
    <cities id="2" name="Tampere"/>
</dataset>
1
2
3
4
5
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
    <customers id="1" city_id="1" name="Company A"/>
    <customers id="2" city_id="2" name="Company B"/>
</dataset>
1
2
3
4
5
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
    <offices id="1" city_id="1" name="Office A"/>
    <offices id="2" city_id="2" name="Office B"/>
</dataset>

Что мы только что сделали?

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

3. Утверждайте только ту информацию, которая может быть изменена протестированным кодом

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

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

Мы можем решить все эти проблемы, следуя этому простому правилу:

Мы должны утверждать только ту информацию, которая может быть изменена проверенным кодом.

Давайте выясним, что означает это правило.

пример

Ранее мы создали (проблемный) набор данных, который гарантирует, что правильная информация будет сохранена в базе данных при обновлении информации о клиенте (идентификатор обновленного клиента равен 2). Этот набор данных выглядит следующим образом:

1
2
3
4
5
6
7
8
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
    <cities id="1" name="Helsinki"/>
    <cities id="2" name="Tampere"/>
     
    <customers id="1" city_id="1" name="Company A"/>
    <customers id="2" city_id="1" name="Company B"/>
</dataset>

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

После того как мы удалили ненужную информацию из нашего набора данных, она выглядит следующим образом:

1
2
3
4
5
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
    <customers id="1" city_id="1" name="Company A"/>
    <customers id="2" city_id="1" name="Company B"/>
</dataset>

Теперь мы исправили проблемы с производительностью и техническим обслуживанием, но осталась еще одна проблема:

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

После того, как мы это сделали, наш набор данных выглядит следующим образом:

1
2
3
4
5
6
7
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
    <customers id="1" city_id="1" name="Company A"/>
     
    <!-- The information of the updated customer -->
    <customers id="2" city_id="1" name="Company B"/>
</dataset>

Намного лучше. Правильно?

Резюме

Этот пост научил нас тому, что:

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