Pull to refresh
6
0
Дмитрий Ануфриев @dmitryanufriev

Пользователь

Send message
Да что там набросить… увидел Buildman и всплакнул. Ностальгия :).
А если рассмотреть такой пример. Вот, допустим, мы в коде используем классическое наследование. И для класса Rectangle вводим класс-наследник, расширяющий поведение базовового класса за счет добавления метода Draw. Один класс, один метод Draw, ответственность определена — отрисовка. Назовем его, предположим, DrawableRectangle. Этот дизайн соответствует SRP?
Наверное, если опираться на то, что говорит yegor256, то в статье вообще неудачный пример, на мой взгляд. Фактически, получается, что будет модель данных (DTO), которая содержит ширину и высоту и классы-процедуры, которые с этой моделью будут работать. О чем, собственно, автор и говорит в одном из комментариев выше
Лучше конечно ввести модель прямоугольника безо всяких там методов, а методы собственно реализовать в других классах — рисовальщике и вычисляльщике.
.
И в этом контексте да, зачем классы? Это хороший процедурный, структурированный код, который можно реализовать хоть на C, хоть на Pascal. Кстати, SRP это про ООП или вообще про организацию кода?
Добыча знаний напоминает Закон Деметры.
В моем понимании это штука для быстрого поиска «всего и вся» (условно). Как фильтр из множества доступных вариантов (например, при поиске файла, функции Elisp, буфера и т.д.). Раньше называлась anything. Здесь кратко описывается как “über-extensible spotlight for emacs” или “QuickSilver for emacs”. Имеет интеграцию со сторонними плагинами — silversearcher, projectile и еще много с чем. На MELPA можно посмотреть в результатах поиска по строке «helm».
Мне кажется, что в рассуждениях IDE vs текстовый редактор есть доля лукавства. Я практически не встречал людей, которые в профессиональной разработке пользуются только текстовым редактором. Все случаи использования Vim и Emacs в повседневной работе заключаются в том, чтобы взять эти инструменты и превратить их в IDE.

Например, мне для более менее комфортной работы (Emacs) требуется 23 плагина:

(depends-on «nlinum»)
(depends-on «which-key»)
(depends-on «powerline»)
(depends-on «projectile»)
(depends-on «auto-complete»)
(depends-on «highlight-symbol»)
(depends-on «magit»)
(depends-on «yasnippet»)
(depends-on «helm»)
(depends-on «helm-projectile»)
(depends-on «helm-ag»)
(depends-on «virtualenvwrapper»)
(depends-on «anaconda-mode»)
(depends-on «ac-anaconda»)
(depends-on «flycheck»)
(depends-on «web-mode»)
(depends-on «emmet-mode»)
(depends-on «json-mode»)
(depends-on «js2-mode»)
(depends-on «ac-js2»)
(depends-on «coffee-mode»)
(depends-on «tern»)
(depends-on «tern-auto-complete»)

Так что, наверное, можно признать, что подавляющему большинству программистов нужна именно IDE (с теми или иными возможностями). Разница заключается в том, что из Vim или Emacs IDE нужно делать самому. А здесь основная мотивация — это очень интересно! И как бонус, в случае ошибок почти всегда есть возможность посмотреть, что происходит внутри и связаться с авторами того или иного плагина (например, автор Tern отвечает достаточно оперативно).

В общем, если вы в детстве ломали игрушки, чтобы посмотреть, что у них внутри, то, скорее всего, вы уже используете Vim или Emacs :)

Кстати, мое мнение — Emacs ну никак не текстовый редактор, это скорее виртуальная машина с языком Emacs Lisp на борту.
Не совсем. Суть подхода скорее в том, чтобы чаще заглядывать немного вперёд. Если получается дробить на осязаемые кусочки по 25 минут, то это очень хорошо.
Бывает. И иногда здорово помогает понять, что двигаешься не в том направлении. Тут самое сложное усилием воли заставить себя взять таймаут. А иногда просто понимаешь, что направление в целом верное, но вот здесь можно сделать проще, а этот класс вообще не нужен.
Я использую пару приёмов, чтобы хоть как-то повысить качество своего кода. Это утренний code review и помидорка. Первое достаточно просто — рабочий день начинается с обзора того, что сам написал за вчерашний день. Очень часто помогает самому увидеть недостатки своего кода. Ну, а второе — задаёт удобный (для меня конечно) режим работы. 25 минут кодируешь — 5 минут перерыв, опять 25 минут кодируешь — 15 минут перерыв и по новой. Эти перерывы помогают обдумать, что буду кодировать следующий «спринт». Как-то так.
Ну, создание базы каждый раз с нуля можно больно ударить по производительности.

На сегодняшний день сборка проекта плюс запуск всех модульных тестов составляет чуть больше 4 минут. Это не то чтобы больно, но да, хочется быстрее. Про создание базы данных я неверно выразился. Под созданием базы данных подразумевается копирование файлов БД на RAM диск и подключение этих файлов к экземпляру SQL сервера. Опять же, это только один из возможных подходов к тестирования слоя взаимодействия с БД.

Тем более, что при более сложных организациях объектов (когда они друг на друга ссылаются), ваше требование держать тест коротким сводится на нет, потому что стадия arrange растягивается на 10-20 строчек, если не больше.

Подобная ситуация может также возникнуть при неправильном использовании паттерна «репозиторий». То есть, когда начинает появляться масса методов, которые логически друг с другом не связаны. Как правило, методы подобных классов связаны между собой только общим контектом подключения к БД. Это довольно легко проверить. Если при добавлении в сигнатуру метода контекста БД метод может стать статическим, то это оно и есть. Обычно подобные классы имеют тенденцию разрастаться до состояния «невлезания» в экран. Тесты на подобные классы очень сложны для реализации. Вернее, начинается всё вроде бы неплохо, но со временем код теста превращается в одну большую нечитаемую массу. В этой ситуации можно попробовать провести декомпозицию такого «репозитория». Превратить методы в классы. Для которых будут свои тесты.

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

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

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

Для проверки постраничной выборки совсем необязательно вставлять в базу данных 1000 записей. Цель ведь заключается в том, чтобы проверить сам механизм постраничной выборки, а не тот факт, что из 1000 записей вернется только 10. Ведь можно 10 записей разбить на страницы? Нужно только указать размер страницы. Для теста он может быть равен 2 записям. Если со стороны заказчика есть требование, что на странице должно отображаться только 10 записей, то это требование может быть проверено при функциональном тестировании.
Желание использовать модульные тесты для классов, подобных ArticlesRepository вызвано тем, что хочется как можно раньше видеть ошибки реализации. Соответственно, как можно быстрее их исправлять.

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

Допустим, у нас есть следующий класс, который так или иначе взаимодействует с базой данных. Класс реализует определённый интерфейс.

public class ArticlesRepository : IArticlesRepository
{
    // Здесь используется ORM. Неважно какая.
    private ORM _orm = new ORM();

    // Пример вымышленный. Вместо массива может быть всё, что угодно.
    public Article[] GetPublishedArticles()
    {
        return _orm.Articles.Where(a => a.Published).ToArray();
    }
}


И есть класс, который использует наш выдуманный репозиторий. Например, такой:

public class ArticlesClient
{
    private IArticlesRepository _articlesRepository;

    public ArticlesClient(IArticlesRepository articlesRepository)
    {
        _articlesRepository = articlesRepository;
    }

    public int CalculatePublishedArticles()
    {
        return _articlesRepository.GetPublishedArticles().Length;
    }
}


Этот ArticlesClient тоже может реализовывать какой-нибудь интерфейс. И использоваться в любом необходимом месте. В контроллере или ещё где-нибудь.

Модульные тесты на ArticlesClient не будут использовать базу данных. В качестве реализации IArticlesRepository будет использоваться mock.

Модульные тесты на ArticlesRepository будут использовать базу данных, разворачиваемую в памяти. В статье описан способ с использованием MS SQL.

ORM же — внешний код по отношению к нашему. Возможно, это просто библиотека или набор библиотек, которые пришли к нам из NuGet. Мы не тестируем код этой библиотеки. Мы тестируем то, что с помощью него делаем. Нашу логику.

Как-то так.
Как уже сказано, это не юнит тесты. Это можно назвать интеграционными или behavior тестами. И они пишутся, как правило после реализации. Юнит тесты в рамках TDD пишутся до реализации

Вопрос терминологии опять же. Если под «этим» подразумеваются тесты на сущности, работающие с БД, то в описываемом подходе вполне возможно писать тесты до реализации. Мы, собственно, на проекте так и делаем.

Тест не должен звучать как «объект должен достать из базы, потом посчитать что-то», он должен звучать как «объект должен вызвать метод, получающий что-то и посчитать».

Тесты на объект который должен вызвать метод и выполняющий подсчёт выглядят именно так. Другое дело, что на метод, который получает что-то мы тоже пишем тесты. Разворачивая БД в памяти.

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

Я бы сказал «не только для того». Это одно из преимуществ TDD. Дополнительно к тем, которые вы описали.
его разработчик — по идеи написал тесты и описал как правильно работать с ним в документации

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

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

Да вроде все разумные люди :)

Ну так это уже совсем другая история, которая зовется «НАГРУЗОНОЕ ТЕСТИРОВАНИЕ», при которых выявляется как раз тоги, слабые места приложения.

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

Допустим, есть метод некоего класса, который предназначен для выборки данных из БД — DataSelector. Выборки данных по определённым условиям. Понятно, что в модульных тестах на сущности, использующие этот класс, будет использоваться mock нашего DataSelector'а. Но ведь хочется протестировать и сам DataSelector. Не соврёт ли, возвращая данные из БД? Поэтому пишем модульные тесты на сам DataSelector. Потом, проведя нагрузочное тестирование, о котором вы говорите, мы понимаем, что вот этот DataSelector работает медленно. Ну вот использовали в качестве реализации метода какой-нибудь ORM для скорости разработки, а он строит неоптимальные запросы. Решили в целях оптимизации переписать метод без использования ORM. Чистый ADO.NET. Или Dapper какой-нибудь. И мы можем это сделать. Потому что у нас уже есть код, проверяющий как должна работать выборка этого DataSelector'а. То есть, я пытаюсь донести мысль, что нельзя просто сказать, что мы здесь используем ORM, поэтому тело метода, использующего ORM, считаем тривиальным и не тестируем.

Ну, а нагрузочное тестирование имеет смысл проводить после того, как стало понятно, что система прошла все проверки и работает корректно. Не смог найти кому принадлежит фраза: «Медленно, но правильно работающая программа лучше, чем работающая быстро, но неправильно». Может быть фразу переврал, но надеюсь, что суть передал верно.
Сам же ConsoleWriter не тестируется в силу его тривиальности.

Ну хорошо. А как определять границы тривиальности? В моём представлении, тривиальный код — это код, который просто делегирует вызов. Допустим, если нам нужен какой-нибудь статический метод. Например, пока не используем Microsoft Fakes, но тестировать вызов DateTime.Now уже нужно. В этом случае мы создаём «обёртку» с методом Now() которая просто делегирует вызов стандартному DateTime. И эту «обёртку» мы не тестируем.

Так же и с базой данных. Не нужно имитировать базу данных. Нужно ее изолировать. С O/RM это делается вполне себе красиво.

К сожалению, с базой данных не так. И ORM не со всеми запросами справляется так эффективно, как этого хотелось бы.
Возможно я вас неправильно понял, но я ни в коем случае не против разделения на слои и выделения абстракций для БД. В процитированной вами фразе я имел в виду, что класс может быть как репозиторием, так и калькулятором. И тесты на эти классы будут разными. В статье описан возможный подход к тестированию того слоя, который отвечает за взаимодействие с базой данных и http запросами. Один из подходов. Причём приближенный к тем условиям, в которых будет работать код. С оглядкой на скорость выполнения тестов. При этом я совершенно согласен, что слой доступа к данным должен быть настолько «тонким» насколько это возможно. Но этот слой тоже должен тестироваться. И очень хорошо, что это можно сделать посредством модульных тестов.

использовать ту же SQLite с хранилищем в памяти

К своему стыду, я не знаю, как работает SQLite и насколько специфика работы с ней отличается от специфики работы с MS SQL. Вполне возможно, что классы, отвечающие за взаимодействие с БД, будут одинаково работать и с SQLite и с MS SQL. Просто изменяя connection string. Если это не так, то получается ситуация в которой мы тестируем не то, что будет работать на самом деле. Наверное, это даже хуже, чем отсутствие тестов. Так как даёт иллюзию работоспособности кода.
Ну так ведь это другой вид, тестирования и в юнит тестировании их не должно быть

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

унит тесты должны тестировать только одну ответсвенность, в правильном коде в котором соблюдены правила SOLID, одной ответсвенности соответсвует один класс.

А если ответственность класса заключается в том, чтобы выбрать определённую сущность из хранилища? Некий ObjectFinder. В статье рассматривается пример, когда в качестве хранилища используется MS SQL. Поэтому и рассматривается вариант использования своеобразного mock'а БД, который максимально приближен к «оригиналу». То есть, если mock реального класса должен соответствовать интерфейсу оригинала, то mock реальной базы данных должен соответствовать структуре реальной базы данных. Изменения в структуре тестовой базы данных, необходимые для успешного прохождения модульного теста, в дальнейшем транслируются на реальную базу данных. То есть, здесь также может работать правило test first.

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

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

Чтобы избежать подобной ситуации используются маленькие коммиты. Ситуация, когда разработчик весь день работает над кодом и делает коммит только под конец рабочего дня крайне нежелательна. Потом и code review делать сложно и есть риск, что результаты работы за день не попадут в систему контроля версий.
и тогда лучше ему не юнит-тесты читать, а с проектом знакомиться

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

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

Я могу ошибаться, но мне такой подход напоминает readme driven development. Тоже очень интересный подход к разработке.
1

Information

Rating
Does not participate
Location
Екатеринбург, Свердловская обл., Россия
Date of birth
Registered
Activity