Тестирование с базой данных в .NET


    Обычным подходом в .NET к тестированию приложений работающих с базой данных является внедрение зависимостей (Dependency Injection). Предлагается отделить код работающий с базой, от основной логики путем создания абстракции, которую в дальнейшем можно подменить в тестах. Это очень мощный и гибкий подход, который тем не менее имеет некоторые недостатки — увеличение сложности, разделение логики, взрывной рост количества типов. Подробнее в предыдущей статье Что-то не то с тестированием в .NET (Java и т.д.) или в Wiki/Dependency Injection.


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



    Пример


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

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

    Для этого введем следующий метод (его и нужно будет протестировать):

    public class ReminesService 
    {
        RemineItem[] GetReminesFor(Storage storage, DateTime time) { ... }
    }
    

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

    Тестовая база данных


    Нам понадобится база данных для тестирования. Для простых проектов можно использовать SQLite, это неплохой компромисс между скоростью тестов и их надежностью. Для более сложных случаев лучше использовать такую же БД, что и при разработке. В большинстве случаев это не проблема — MySql и PostgreSql легковесные, для SQLServer есть режим LocalDb.

    Если вы работаете с SQLServer, удобно воспользоваться LocalDb режимом для тестовой базы — он намного легче и быстрее полной базы, при этом полностью функционален. Для этого нужно сконфигурировать App.config в тестовом проекте:

    Конфигурация для SQLServer LocalDb
    <?xml version="1.0" encoding="utf-8"?>
    <configuration>
        <configSections>
          <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
        </configSections>
        <entityFramework>
          <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework">
            <parameters>
              <parameter value="MSSQLLocalDB" />
            </parameters>
            </defaultConnectionFactory>
          <providers>
            <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
          </providers>
        </entityFramework>
    </configuration>
    


    Фреймворк


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

    Начальные данные


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

    Чтобы упростить дальнейшее создание тестовых сценариев, необходимо создать минимальный набор общих для системы данных.

    Чтобы было немного веселее, давайте в качестве товаров возьмем бутылки с виски. Начнем с модели, у которой нет зависимостей — страна производителя (Country):

    public class Countries : IModelFixture<Country>
    {
        public string TableName => "Countries";
    
        public static Country Scotland => new Country
        {
            Id = 1,
            Name = "Scotland",
            IsDeleted = false
        };
    
        public static Country USA => new Country
        {
            Id = 2,
            Name = "USA",
            IsDeleted = false
        };
    }
    

    Чтобы фреймворк понял, что это описание начальных данных, класс должен реализовывать интерфейс IModelFixture<T>. Экземпляры моделей объявляются статическими, чтобы обеспечить к ним доступ из других фикстур и тестов. Вы должны явно указывать первичные ключи (Id) и следить за их уникальностью в рамках одной модели.

    Теперь можно создавать производителей:

    class Manufacturers : IModelFixture<Manufacturer>
    {
        public string TableName => "Manufacturers";
    
        public static Manufacturer BrownForman => new Manufacturer
        {
            Id = 1,
            Name = "Brown-Forman",
            CountryId = Countries.USA.Id,
            IsDeleted = false
        };
    
        public static Manufacturer TheEdringtonGroup => new Manufacturer
        {
            Id = 2,
            Name = "The Edrington Group",
            CountryId = Countries.Scotland.Id,
            IsDeleted = false
        };
    }
    

    И товары:

    public class Goods : IModelFixture<Good>
    {
        public string TableName => "Goods";
    
        public static Good JackDaniels => new Good
        {
            Id = 1,
            Name = "Jack Daniels, 0.5l",
            ManufacturerId = Manufacturers.BrownForman.Id,
            IsDeleted = false
        };
    
        public static Good FamousGrouseFinest => new Good
        {
            Id = 2,
            Name = "The Famous Grouse Finest, 0.5l",
            ManufacturerId = Manufacturers.TheEdringtonGroup.Id,
            IsDeleted = false
        };
    }
    

    Обратите внимание на внешние ключи — они не указываются явно, а ссылаются на другую фикстуру.

    Такой подход имеет множество преимуществ перед sql-файлами или json файлами фикстур:

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

    Важно! У этого подхода есть недостаток — при каждом обращении к статическому свойству создается экземпляр модели и всех зависимых от него моделей (и их зависимостей тоже). Если возникают проблемы с производительностью или циклическими ссылками, то можно исправить это с помощью ленивой инициализации Lazy<T>.

    private static Good _famousGrouseFinest = new Lazy<Good>(() => new Good
    {
        Id = 2,
        Name = "The Famous Grouse Finest, 0.5l",
        ManufacturerId = Manufacturers.TheEdringtonGroup.Id,
        IsDeleted = false
    };
    public static Good FamousGrouseFinest => _famousGrouseFinest.Value;
    

    Подготовка окружения


    Тестовое окружение в первую очередь это база данных, также это могут быть синглтоны и статические переменные (например, в asp.net можно установить HttpContext). Лучше собрать все эти операции в одном месте и запускать перед каждым тестом. Мы назвали у себя такое место — World. Чтобы подготовить базу данных — нужно вызвать метод ResetWithFixtures и передать туда список начальных фикстур.

    static class World
    {
        public static void InitDatabase()
        {
            using (var context = new MyContext())
            {
                var dbTest = new EFTestDatabase<MyContext>(context);
    
                dbTest.ResetWithFixtures(
                    new Countries(),
                    new Manufacturers(),
                    new Goods()
                );
            }
        }
    
        public static void InitContextWithUser()
        {
            HttpContext.Current = new HttpContext(
                new HttpRequest("", "http://your-domain.com", ""),
                new HttpResponse(new StringWriter())
            );
            HttpContext.Current.User = new GenericPrincipal(
                new GenericIdentity("root"),
                new string[0]
                );
        }
    }
    

    Возможность задать статические переменные и синглтоны особенно важна при тестировании legacy кода, где не так-то просто поменять архитектуру — но есть острая необходимость в тестировании. Разделение настройку окружения на несколько методов позволяет подготавливать окружение индивидуального для каждого теста. Например, в unit тестах не используется база и нет смысла очищать для них базу. Или у вас может быть необходимость подготовить различное окружение для разных состояний системы (авторизованный и неавторизованный пользователь).

    Создание тестового сценария


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

    public class ModelBuilder
    {
        public MoveDocument CreateDocument(string time, Storage source, Storage dest)
        {
            var document = new MoveDocument
            {
                Number = "#",
    
                SourceStorageId = source.Id,
                DestStorageId = dest.Id,
    
                Time = ParseTime(time),
                IsDeleted = false
            };
    
            using (var db = new MyContext())
            {
                db.MoveDocuments.Add(document);
                db.SaveChanges();
            }
    
            return document;
        }
    
        public MoveDocumentItem AddGood(MoveDocument document, Good good, decimal count)
        {
            var item = new MoveDocumentItem
            {
                MoveDocumentId = document.Id,
                GoodId = good.Id,
                Count = count
            };
    
            using (var db = new MyContext())
            {
                db.MoveDocumentItems.Add(item);
                db.SaveChanges();
            }
    
            return item;
        }
    }
    

    Тестируем


    Пришло время собрать все вместе и посмотреть что получилось:

    [SetUp]
    public void SetUp()
    {
        World.InitDatabase(); // подготавливаем базу к каждому тесту
    }
    
    [Test]
    public void CalculateRemainsForMoveDocuments()
    {
        /// ARRANGE - создаем тестовую ситуацию
        var builder = new ModelBuilder();           
    
        // Приход товаров на удаленный склад
        var doc1 = builder.CreateDocument("15.01.2016 10:00:00", Storages.MainStorage, Storages.RemoteStorage);
        builder.AddGood(doc1, Goods.JackDaniels, 10);
        builder.AddGood(doc1, Goods.FamousGrouseFinest, 15);
               
        // Расход товаров с удаленного склада
        var doc2 = builder.CreateDocument("16.01.2016 20:00:00", Storages.RemoteStorage, Storages.MainStorage);
        builder.AddGood(doc2, Goods.FamousGrouseFinest, 7);
    
        /// ACT - вызываем тестируемую функцию
        var remains = RemainsService.GetRemainFor(Storages.RemoteStorage, new DateTime(2016, 02, 01));
    
        /// ASSERT - проверяем результат
        Assert.AreEqual(2,  remains.Count);
        Assert.AreEqual(10, remains.Single(x => x.GoodId == Goods.JackDaniels.Id).Count);
        Assert.AreEqual(8,  remains.Single(x => x.GoodId == Goods.FamousGrouseFinest.Id).Count);
    }
    

    Обратите внимание на использование начальных фикстур в коде теста
    Storages.MainStorage, Goods.JackDaniels, Goods.FamousGrouseFinest и т.д.

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

    Резюме


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

    По сравнению с DI, тестирование с настоящей базой имеет следующие преимущества:

    • Меньшее влияние тестов на архитектуру
    • Меньше слоев абстракции — меньше сложность и упрощается чтение кода
    • Больше доверия к тестам, которые на самом деле читают и вставляют данные в базу
    • Быстрее в написании и проще в поддержке

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

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

    Полезные ссылки


    DbTest (репозиторий с тестовым фреймворком и примерами из статьи)
    Smocks (мок для статических системных методов)
    Поделиться публикацией

    Комментарии 92

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

      Разделение логики — это достоинство, а не недостаток. А "взрывной рост" наблюдается только там, где при проектировании допущена ошибка.


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

      Проще, серьезно? "Проще" это только тогда, когда вы для каждого теста создаете нужную ему (и только ему) БД. Но вы представляете себе, насколько это медленно? Поэтому начинают переиспользовать БД между несколькими тестами — а это уже анти-паттерн shared fixture, ну и понеслась...


      А еще представьте себе, как просто это делать на билд-агентах при каждом билде.


      По крайней мере серверное время намного дешевле времени разработчика.

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


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

      Интеграционные тесты — это не альтернатива DI. Интеграционные тесты — это "альтернатива" юнит-тестам; хотя на самом деле, интеграционные тесты — это другой способ тестирования, не способный заменить юнит-тестирование (в обратную сторону тоже верно).

        0
        Разделение логики — это достоинство, а не недостаток.

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


        Но вы представляете себе, насколько это медленно? Поэтому начинают переиспользовать БД между несколькими тестами — а это уже анти-паттерн shared fixture, ну и понеслась...

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


        Интеграционные тесты — это не альтернатива DI.

        Альтернатива — то как можно тестировать. А интеграционные тесты это дополнение к unit-тестам.

          +2
          Только если это разделение по ответственности или еще каким-то логическим критериям, а не искусственное — чтобы отделить обращение к базе.

          Обращение к БД — это и есть другая ответственность. Поэтому разделение работы с БД и разделение бизнес-логики — это разделение по ответственности.


          Не настолько медленно как принято представлять

          Понимаете ли, я опираюсь не на "принято представлять", а на свою ежедневную деятельность, в которой много интеграционных тестов. И они — медленные. На несколько порядков медленее, чем юнит-тесты.


          , и есть куда думать, чтобы ускорить.

          Например? Потому что в моем опыте "куда ускорить" неизбежно приводит к shared fixture, потому что все рано или поздно упирается во время развертывания чистой БД.


          Альтернатива — то как можно тестировать. А интеграционные тесты это дополнение к unit-тестам.

          Если дополнение — значит, от DI вы отказаться не сможете. Поэтому и не альтернатива.

            +1
            Обращение к БД — это и есть другая ответственность. Поэтому разделение работы с БД и разделение бизнес-логики — это разделение по ответственности.

            Тут можно поспорить — так принято в .NET, что работа с БД это отдельная ответственность. И я считаю, что во многом из-за того, что по другому не протестировать.


            значит, от DI вы отказаться не сможете

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

              0
              Тут можно поспорить — так принято в .NET, что работа с БД это отдельная ответственность.

              Далеко не только в .net. Вы Фаулера читали?


              И я считаю, что во многом из-за того, что по другому не протестировать.

              Нет, потому что так сложность меньше.

                0

                Читал, и наверное его читали создатели Ruby on Rails, Django, Yii2 и тем не менее выбрали эту схему. Я могу привести мнение DHH (создателя RoR), но может быть лучше не авторитетами давить, а аргументированно критиковать?


                И еще раз — я не против DI как такового… я про то, что это часто избыточно.

                  0
                  Читал, и наверное его читали создатели Ruby on Rails, Django, Yii2 и тем не менее выбрали эту схему

                  Какую "эту"? Слияния логики работы с БД с бизнес-логикой? Или все-таки интеграционного тестирования?


                  И еще раз — я не против DI как такового… я про то, что это часто избыточно.

                  А я и не про DI, я про разделение ответственностей. DI — лишь один из способов решения этой задачи.

                    +1
                    Слияния логики работы с БД с бизнес-логикой

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


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

                      +1
                      Я слышу про отделение базы только в контексте двух сценариев: тестирование и гипотетическая смена базы в будущем.

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

                        0

                        Увеличение уровней абстракции не факт, что ведет к уменьшению сложности. А очень даже наоборот — вам теперь нужно помнить как работают две сущности и их взаимодействие вместо одной.
                        Попробуйте пописать на python — довольно неплохо прочищает мозги. Мне C# милее в сотню раз, но свой отпечаток питон оставил.

                          +1
                          Увеличение уровней абстракции не факт, что ведет к уменьшению сложности

                          А никто не говорит, заметим, про увеличение уровней абстракции — можно просто заменить одну абстракцию другой.


                          А очень даже наоборот — вам теперь нужно помнить как работают две сущности и их взаимодействие вместо одной.

                          Мне не нужно помнить, как работает DAL, мне нужно знать, какой контракт он выполняет. И это ничем не отличается от того, чтобы помнить, какой контракт поддерживает Entity Framework или ADO.NET.

                        +1
                        второе похоже на раннюю оптимизацию
                        В качестве мимокрокодила отмечу, что когда-то несколько лет назад тоже на это забил, а теперь сильно жалею об этом, так как для перехода с mysql на postgresql оказывается нужным по сути переписать ВСЁ, так и не перехожу до сих пор
                          0
                          В чём проблема? Вы писали SQL запросы вручную?
                            0
                            В те времена да. Теперь не пишу)
                              +1
                              Если у вас SQL запросы локализованы в слое DBAL, то переписать их это не означает переписать «ВСЁ»
                          0
                          Я вижу разделение базы чуть ли не каждый день. И замену одной базы на другую — например, в тестах вместо PROD базы MS SQL используется in-process база H2, а зачастую и in-memory база тоже. Для тестов, да. Это очень, очень широко распространенная практика в мире Java.
                      +2

                      Меня бы полностью удовлетворила такая формулировка: есть подход А и Б, вот их плюсы и минусы, решайте, что вам дороже обойдется. К сожалению часто звучит "есть только А, остальное ересь" и это напоминает картинку про PNG и JPEG.

                        0

                        Ну вот мы эти плюсы и минусы сейчас обсуждаем.

                +3
                «Проще» это только тогда, когда вы для каждого теста создаете нужную ему (и только ему) БД. Но вы представляете себе, насколько это медленно?

                Я не знаю как с этим дела в C#, но в своих проектах на Python и Ruby я только так тесты и писал, всё тестирование с постоянным пересозданием этих баз занимало от нескольких секунд до 5-10 минут в зависимости от размера и оптимизированности проекта, имхо вполне приемлемо
                  0

                  О… как я ждал этого комментария! Ирония в том, что так делают очень многие, но в мире .NET про это мало кто знает и порицается хуже чем goto ))

                    0

                    При каком количестве тестов? БД создается на каждый тест?

                      +1

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

                        +1
                        быстро

                        Насколько быстро?

                          +1

                          Около 600 мс на тест

                            +2
                            ALTER DATABASE ... SET RECOVERY SIMPLE
                            ALTER DATABASE ... SET DELAYED_DURABILITY = FORCED
                            

                            А если так, то сколько будет? :)
                              0

                              А что здесь происходит? Можно попробовать замерить

                                +3
                                При создании новой базы настройки наследуются от базы model (если не учитывать некоторые нюансы). По дефолту в model стоит FULL. Если база создалась и для нее сделался бекап, то это приведет к разрастанию лога, если нет, то в Вашей базе будет неявно использоваться SIMPLE модель.

                                Для базы с тестами мы также включаем модель восстановления SIMPLE и отложенную запись в лог DELAYED_DURABILITY = FORCED. В теории это самый простой путь без лишних телодвижений снизить время на подготовку данных для теста.
                              0

                              … а у меня на (юнит-)тест уходит меньше 10 мс. Вот вам и порядок.

                                0

                                Зато при программировании и поддержке цифры меняются местами… там конечно, не будет отличия на порядок, но и время там подороже стоит.

                                  0
                                  Зато при программировании и поддержке цифры меняются местами…

                                  Почему вдруг?


                                  там конечно, не будет отличия на порядок, но и время там подороже стоит.

                                  Понимаете ли, время, потраченное на выполнение интеграционного теста — это тоже мое время.

                                    –1
                                    Почему вдруг?

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

                                      0
                                      Потому что меньше кода, он лучше локализован, тесты максимально приближены к реальной системе (никаких моков) — создал за 10 минут сценарий и погнали.

                                      Меньше кода где?


                                      он лучше локализован

                                      Локализация тут вообще ни при чем.


                                      тесты максимально приближены к реальной системе (никаких моков) — создал за 10 минут сценарий и погнали

                                      10 минут — это круто, да. Хотел бы я вам поверить, но не выходит.


                                      А главное, быстрое выполнение тестов — это больше тестов и более частое их выполнение — более раннее обнаружение багов — меньше затрат на саппорт. И упрощение рефакторинга.

                          +2
                          До пары тысяч бывало. Сейчас пилю Python-проект, полтысячи тестов выполняются за 20 секунд (с «пересозданием» БД на каждый тест, ага)
                            0
                            полтысячи тестов выполняются за 20 секунд

                            У вас БД со всем наполнением создается за 40 мс?


                            (ну и да, я вот тут рядом попинал юнит-тесты, на тест уходит меньше 10 мс — и их еще и можно параллелить)

                              +1
                              Наполнение у меня почти отсутствует, так что почему бы и нет)

                              Сейчас попробовал принудительно создавать по тысяче записей перед каждым тестом (честной неоптимизированной тысячей insert-запросов :) — время выполнения увеличилось до 40 секунд, но я всё ещё считаю это приемлемым

                              Но всё равно так «в лоб» обычно редко делают, есть куча оптимизаций «пересоздания», в разной степени применимых в каждом конкретном случае)
                                0
                                Сейчас попробовал принудительно создавать по тысяче записей перед каждым тестом (честной неоптимизированной тысячей insert-запросов :) — время выполнения увеличилось до 40 секунд, но я всё ещё считаю это приемлемым

                                Понимаете ли, в чем дело, у меня тут под боком система, где ~1000 коротких интеграционных тестов идет где-то 40 минут. А начинали с секунд, да.

                                  0
                                  И я как-то сильно сомневаюсь, что в этих интеграционных тестах узким местом является или будет являться именно пересоздание БД)
                                    0

                                    Там узкое место — это операции с БД. Включая ее инициализацию в корректное (нужное для каждого отдельного теста) состояние.

                                      +1
                                      Ну от операций с БД мы в любом случае никуда не убежим, а топик вроде как лишь про её пересоздание)

                                      (Правда, я ничего не могу сказать про ту конкретную реализацию, что описана в топике, так как C# не юзаю)
                                        +2
                                        Ну от операций с БД мы в любом случае никуда не убежим

                                        Если использовать юнит-тесты вместо интеграционных — еще как убежим.

                        –7
                        Обожаю читать разносы .NET разработчиков от liar
                        +4
                        Самая большая ложка дегтя с интеграционными тестами — это время выполнения, они намного медленнее, но это решаемая проблема.

                        У Вас БД для каждого теста пересоздается? Если да, то может помочь Instant File Initialization. Либо лучше базу вообще один раз создать, а потом использовать database snapshot для каждого теста. Начиная с 2016 SP1 эта функциональность и в Express редакции доступна.

                        Как сделать быстрее тут когда-то публиковал про Delayed Durability. Для OLTP нагрузки как раз поможет снизить выполнение Ваших тестов.
                          0

                          База не пересоздается — в ней отключаются constraints и она чистится, получается очень быстро.

                            0
                            Констрейнты включаются после того как в таблицах появились свежие порции данных для нового теста?
                          0

                          Спасибо! Я попробую эти варианты!

                            +1
                            ИМХО самый лучший вариант: создается база, создается snapshot, накатываются данные, тест проверяется, snapshot откатывается и все по-новому. Тут Вам и минимальная нагрузка на диск + не надо чистить каждый раз базу. В идеале конечно включить Delayed Durability, чтобы снизить WRITELOG ожидания коих при OLTP нагрузке будет достаточно.
                          +3

                          ИМХО, как-то у Вас все хардкорно. Маленькая библиотека — прям фрэймворк, который указывает на необходимый дизайн приложения и вносит зависимости (интерфейсы). При это все, что требуется для тестового набора — получить базу.


                          Так как данный подход очень мало распространен в .NET — почти нет никаких готовых библиотек для его реализации.

                          Подход распространен, нет смысла создавать библиотеку вокруг System.Data.SqlLocalDb. Пример, https://github.com/damianh/SqlStreamStore/blob/master/src/SqlStreamStore.MsSql.Tests/MsSqlStreamStoreFixture.cs
                          А для простых вставок данных (если абстрагироваться от логики самого приложения), достаточно и dapper-dot-net как легковесного решения (как пример подхода с минимумом абстракций).


                          У нас обычный тест выглядит так:


                          • получить базу (connection string)
                          • накатить схему
                          • добавить тестовые данные
                          • выполнить сам тест
                          • очистить ресурсы

                          Для ускорения, пустая база со схемой создается во время компиляции (post-build). А после, в каждом тесте/наборе, файл копируется и присоединяется (простой скрипт на master: CREATE DATABASE [...] ON (filename = ...)[, (filename = ...)]).


                          За наполнение данными отвечает сам тестируемый модуль, т.к. не всегда БД это только лишь CRUD, иногда есть поток сообщений/событий, из которого создаются проекции. Заодно и последнее тестируется.


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

                            –1

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

                            +1
                            Обычным подходом в .NET к тестированию приложений работающих с базой данных является внедрение зависимостей (Dependency Injection). Предлагается отделить код работающий с базой, от основной логики путем создания абстракции, которую в дальнейшем можно подменить в тестах.

                            То, о чем вы говорите — это не внедрение зависимостей, а инверсия зависимостей. И подменяется, если уж на то пошло, не абстракция, а ее реализации.
                              +1

                              Все верно, посыпаю голову пеплом… Исправлю в статье.

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

                                Логика в данном случае никак не разделяется — она как лежала в условном классе репозитория, так и лежит. Взрывного роста количества типов тоже никакого нет — добавляется всего-лишь по одному интерфейсу/абстрактному классу на каждый репозиторий. Отсюда резонный вывод, что сложность если и растет, то крайне незначительно, и увеличение косвенности в данном случае отбивается многократно уменьшением связанности модулей.
                                  0

                                  Ну вот есть у вас сложный запрос — он уходит в репозиторий, его теперь не видно из кода бизнес-логики… а в нем почти вся суть метода. А его ведь еще и протестировать нужно. И для чего мне тогда разделять их?

                                    0
                                    Что значит «не видно»? А как его было видно до этого? Или вы имеете в виду, что по нажатию F12 в студии вас теперь кидает на файл с абстрактным классом/интерфейсом, а не на класс реализации?
                                      0

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

                                        0
                                        Чтобы он был тестируемым, очевидно (я имею в виду здесь модульное тестирование). Если у вас код доступа к БД находится в самой модели (сиречь объекте бизнес-логики), то вполне очевидно, что вы не сможете покрыть эту модель юнит-тестами. Этим вы усложните для себя рефакторинг, а для всего проекта — внесение изменений, т.к. дизайн будет сильно связанным. Если вы пишете утилитку из трех файлов, то конечно на это можно плюнуть в угоду скорости разработки, но на большом проекте в долгосрочной перспективе вы сами роете себе яму.
                                      +1
                                      Ну вот есть у вас сложный запрос — он уходит в репозиторий, его теперь не видно из кода бизнес-логики… а в нем почти вся суть метода.

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

                                        0

                                        И каким же?

                                          0

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


                                          (впрочем, в большей части случаев и так никто не указ, чего уж...)

                                            0

                                            Ну вот приехали… то есть бывают случаи когда проще применить интеграционный тест?

                                              0

                                              Бывают.

                                                0
                                                Вот Вам пример, живее всех живых: приложение предназначено для обмена данными. Определяются граничные условия, делается подготовленная выборка, которая потом загоняется в какой-нибудь самобытный формат. И таких «обменов» в приложении более пятидесяти. Подготовка выборки вкупе с первичной обработкой данных самым естественным образом делегируется серверу. Таким образом, одними из главных кандидатов на тестирование выступают SQL запросы, тем более, что большая часть доработки тоже падает на них (запросов в среднем три-четыре на «обмен», реже от одного до десятка).

                                                Вы умеете тестировать sql-запросы модульными тестами?
                                            +1
                                            Я бы сказал, что из репозитория ее теперь надо вынести обратно :)
                                        0
                                        Есть более простой подход, широко распространенный в мире динамических языков. Вместо создания абстракции, которую можно контролировать в тестах, этот подход предлагает контролировать саму базу.

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

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

                                            0
                                            Конечно, оно работает. Так же, как и ручное тестирование, например. Вы же не хотите предложить сообществу отказаться от юнит-тестов, потому что приложение можно потыкать руками? :)
                                              0

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

                                                0
                                                Я об том и говорю, что эффекты кардинально разные. Юнит-тестирование не предназначено для выявления дефектов. 99% всего интересного ловят функциональные и интеграционные тесты.
                                                0
                                                Рискуя получить обструкцию, таки скажу: Да, не так уж редко вполне оправдано отказаться от юнит-тестов потому что приложение можно потыкать руками.

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

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

                                                    –1
                                                    Ну вот лично я не считаю себя достаточно «грамотным» программистом, чтобы обойтись без юнит-тестов.

                                                    Я с вами соглашусь если из вашего утверждения убрать слово «юнит». Юнит-тесты — это просто один из способов тестировать код. Иногда удобный, а иногда не очень. Иногда оправданный, иногда не слишком. Меня беспокоит увлечение людей в индустрии именно этим типом тестов. Вера в исключительно полезные свойства именно юнит-тестов, ИМХО, иной раз достигает гипертрофированных масштабов, сродни чему-то религиозному.

                                                    А «обсуждение с товарищами» невозможно в силу размеров системы.

                                                    Под «товарищами», разумеется, понимаются те немногие, кто работал над конкретным модулем/куском кода. Найти оных и обсудить с ними вопрос обычно не представляет труда. Сколько у вас программистов окучивает один модуль или, скажем, фичер?
                                                      +1
                                                      Я с вами соглашусь если из вашего утверждения убрать слово «юнит».

                                                      Я не очень понимаю, с чем тут соглашаться или нет, учитывая, что я говорю про себя.


                                                      Вера в исключительно полезные свойства именно юнит-тестов,

                                                      У юнит-тестов есть одно исключительное достоинство: скорость и простота запуска.


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

                                                      Особенно когда они уже не работают в компании, да.


                                                      Сколько у вас программистов окучивает один модуль или, скажем, фичер?

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

                                                        0
                                                        У юнит-тестов есть одно исключительное достоинство: скорость и простота запуска.


                                                        Тесты нужны чтобы:
                                                        1. Проверить и отладить только что написанный код.
                                                        2. Убедиться что никто код не «испортил» ненамеренно.

                                                        Скорость и простота запуска важны только для 2 пункта. Во многих проектах этот пункт не так уж актуален.
                                                          +1
                                                          Скорость и простота запуска важны только для 2 пункта.

                                                          Почему это? Вот я написал новый код, я хочу проверить, что он правильно работает, почему мне не важна скорость этой проверки?


                                                          Во многих проектах этот пункт не так уж актуален.

                                                          В смысле, там никто не меняет старый код?

                                                      0
                                                      Вообще-то мы с вами эту тему уже подробно обсудили лет 5 назад.

                                                        +1

                                                        Вполне возможно. Более того, возможно, что за это время моя оценка меня, как программиста, поменялась.

                                            –1

                                            Я не знаю как в мире дот нета, но в пыхе мы покрываем юнит тестами отдельные классы. Юнит-тесты пишутся так, чтобы работал лишь код тестируемого метода, все остальные зависимости мокаются.
                                            А функциональными тестами покрываются "юзкейсы". Число этих функциональных тестов равно сумме произведений количества ендпоинтов с которыми взаимодействует клиент приложения на количество вариантов ответов. В целом их выходит не много, потому пересоздавать бд не слишком накладно. Кроме того их запросто можно паралелить, так как они полностью изолированы друг от друга.

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

                                                "Рабочие данные есть у вас всегда" — это громкое заявление, конечно.

                                                  +1
                                                  То есть у разработчика есть доступ к реальным данным, пусть и к копии базы? Рискну предположить, что не все рассматривают это как вариант.
                                                    0
                                                    • На этапе разработки этих данных может тупо не быть в природе
                                                    • Данные могут быть настолько объёмные, что не помещаются в дев.окружение.


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

                                                      А если у вас нету возможности сделать прикладной тест, то вы всегда будете производить кота в мешке. Все юнит тесты пройдут хоть тысячу раз, а реальный юз-кейс типа “ввёл стопку чеков, нажал кнопку, и получил годовой отчёт” всё равно может не пройти. А если ваша рабочая задача сделать экспорт из своего приложения в другое, и процесс экспорта подразумевает обработку данных? А если на выходе этих данных должно быть несколько гигабайт? С рандомно намоканной абракадаброй (а я такое видел) это не протестируешь, а внедрять всё это в определение какого-нибудь мега-монстро-теста кто угодно сойдёт с ума.

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

                                                        Так вот, фиктивные данные — они фиктивные, они не реальные. Это тоже мок, просто другого уровня.

                                                          0
                                                          Причем печаль этих тестовых БД в том, что автор этих данных «знает» какие правильные данные должны быть со всеми их связями.
                                                          Кейсы для случаев когда в БД оказались некорректные данные зачастую не тестируются вовсе.

                                                          Возможно их и стоило бы опускать при 100% покрытии. Вот только его в сложных случаях не бывает.
                                                          А именно там (в тех самых сложных случаях) тесты приносят наибольшую пользу.
                                                    0

                                                    Мой юз-кейз: Создание отчета по набору фильтров, заданному пользователем.
                                                    Существенная часть логики — конструирование сложного SQL-запроса по этому набору фильтров.


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


                                                    Вопросы к знатокам:


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

                                                        Можно использовать для тестирования In-Memory DB. Юнит-тестами это будет если выполнение запроса, пост-обработка, слияние с другими источниками и генерация самого отчета будут проверяться раздельно.

                                                          0

                                                          Спасибо.


                                                          In-Memory DB

                                                          На другую БД заменить непросто, используются специфичные вещи типа JSON-функций в MySQL.


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

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

                                                            0

                                                            Тогда это что-то среднее. Модульный тест с элементами интеграционного.

                                                              0
                                                              Если вы сравниваете сгенерированный текст SQL запроса с ожидаемым текстом, нигде не выполняя сам запрос — то это будет юнит-тест для генератора запросов. Как только посылаете запрос на выполнение в какой-то сторонний сервис (даже in-memory DB), и проверяете данные пришедшие из сервиса — это становится интеграционным тестом,
                                                                +1

                                                                Я думал про такой вариант (проверка sql-строки). Но он мне представляется непрактичным.


                                                                1) Sql не обязан быть детерминированным — допустим, порядок джойнов при подлитии второстепенных табличек меня не волнует. Пробелы и отступы опять же.


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

                                                                  0
                                                                  Дык я не спорю, что лучше или практичнее — я лишь о терминологии, о том какие тесты как называть.

                                                                  моя конечная цель — не получение запроса, а выборка из базы.

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

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

                                                        Самое читаемое