Как я могу создать и удалить базу данных в начале и в конце тестирования Fact в XUnit?

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

Как я могу это сделать ?

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
0
123
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

Существуют разные решения этой проблемы. Но все они сводятся к удалению общего ресурса.

  1. Удалите распараллеливание для xUnit: вы можете сделать это, добавив xunit.runner.json и добавив parallelizeTestCollections в этот файл, как описано в документации , и вы можете использовать Respawn вместе с этим, чтобы восстановить базу данных до контрольной точки после каждый тест. Если у вас много тестов, то это решение может быть медленным, но может быть быстрее, чем каждый раз запускать БД. (это не рекомендуется, см. ответ @RubenBartelink ниже)

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

  3. Если тест не посвящен интеграции с БД, вы можете использовать базу данных в памяти.

  4. И, наконец, вы можете использовать Docker-образ базы данных, возможно, изменяя один из параметров подключения, чтобы сделать каждый тест нацеленным на отдельную базу данных или схему и т. д.

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

Milad_Sh 24.12.2022 14:17

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

Ruben Bartelink 24.12.2022 14:20

@RubenBartelink спасибо за дополнение. Добавлю в редакцию.

Abdelkrim Bournane 24.12.2022 14:56

Используйте Collection Fixture. Это отвечает вашим потребностям:

  1. позволить только одному тесту, которому нужен ресурс, использовать его одновременно
  2. позволяя вам сделать одно вращение вверх / вниз за общий тестовый прогон

Фикстура коллекции сама по себе синхронизирует доступ к фикстуре несколькими тестами в одной коллекции?

Abdelkrim Bournane 24.12.2022 15:02

Я обнаружил, что это должно быть использование фикстуры Collection вместе с пользовательской тестовой коллекцией для тестов, которые должны запускаться последовательно. Спасибо Рубен.

Abdelkrim Bournane 24.12.2022 15:05

Да. Параллелизм никогда не применяется в пределах одного тестового класса (или между экземплярами теории и т. д.). Думайте о фикстуре Collection как об объединении тестовых классов с точки зрения управления параллелизмом.

Ruben Bartelink 25.12.2022 02:19

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

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

using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.IO;
using System.Text;

namespace UnitTestHelpers
{
    /// <summary>
    /// Class supports creation of temporary schemas in an existing database to be
    /// able to use them in unit tests.<br/>
    /// The schemas are deleted on disposal of the object.
    /// </summary>
    public class DataBaseUnitTestHelper : IDisposable
    {
        private bool schemaCreated = false;
        private bool disposedValue;

        /// <summary>
        /// Public constructor requires naming the data source and database.
        /// </summary>
        /// <param name = "dataSource_">The data source, i.e. server name of the database server.</param>
        /// <param name = "catalog_">The database name where the temporary schemas will be created.</param>
        public DataBaseUnitTestHelper(string dataSource_, string catalog_)
        {
            if (string.IsNullOrEmpty(dataSource_))
            {
                throw new ArgumentException($"{nameof(dataSource)} is null or empty.", nameof(dataSource_));
            }
            this.dataSource = dataSource_;
            if (string.IsNullOrEmpty(catalog_))
            {
                throw new ArgumentException($"{nameof(catalog_)} is null or empty.", nameof(catalog_));
            }
            this.catalog = catalog_;
        }

        public string dataSource { get; private set; }
        public string catalog { get; private set; }
        public string schema { get; private set; }

        /// <summary>
        /// Builds a connect string that can be used to connect to the database,
        /// for example in <see cref = "SqlConnection.SqlConnection(string)"/>.
        /// </summary>
        public string connectString
        {
            get
            {
                if (disposedValue) throw new ObjectDisposedException(this.ToString());
                if (connectString_ == null)
                {
                    var csb = new SqlConnectionStringBuilder();
                    csb.DataSource = dataSource;
                    csb.IntegratedSecurity = true;
                    csb.InitialCatalog = catalog;
                    connectString_ = csb.ConnectionString;
                }
                return connectString_;
            }
            private set
            {
                if (disposedValue) throw new ObjectDisposedException(this.ToString());
                connectString_ = value;
            }
        }

        private string connectString_ = null;

        /// <summary>
        /// Returns a (normally unopened) connection to the database.
        /// </summary>
        /// <returns></returns>
        public SqlConnection getConnection()
        {
            if (disposedValue) throw new ObjectDisposedException(this.ToString());
            return new SqlConnection(connectString);
        }

        /// <summary>
        /// Creates a new  uniquely named schema in the given database and returns its name.
        /// </summary>
        /// <returns>The name of the schema that was created.</returns>
        public string createNewTestSchema()
        {
            if (disposedValue) throw new ObjectDisposedException(this.ToString());

            if (schemaCreated)
            {
                throw new InvalidOperationException("Object can only be used to create one test schema.");
            }
            using (SqlConnection connection = getConnection())
            {
                connection.Open();
                string localSchema = "Test" + Guid.NewGuid().ToString("N").Substring(0, 16);
                string sql = $"CREATE SCHEMA {localSchema};";
                using (SqlCommand command = new SqlCommand(sql, connection))
                {
                    int res = command.ExecuteNonQuery();
                    schema = localSchema;
                    schemaCreated = true;
                }
                return schema;
            }
        }

        /// <summary>
        /// Deletes the temporary database schema created by this object,  first clearing all its elements 
        /// </summary>
        private void deleteSchema()
        {
            if (disposedValue) throw new ObjectDisposedException(this.ToString());

            if (!schemaCreated) return;

            // Determine all the objects in the schema
            List<Tuple<string, string>> list = new List<Tuple<string, string>>();
            using (SqlConnection connection = getConnection())
            {
                connection.Open();
                using (SqlCommand selectTablesCmd = connection.CreateCommand())
                {
                    selectTablesCmd.CommandText = "SELECT * FROM [INFORMATION_SCHEMA].[TABLES] WHERE [TABLE_CATALOG] = @tableCatalog AND [TABLE_SCHEMA] = @tableSchema";
                    selectTablesCmd.Parameters.AddWithValue("tableCatalog", catalog);
                    selectTablesCmd.Parameters.AddWithValue("tableSchema", schema);
                    using (SqlDataReader reader = selectTablesCmd.ExecuteReader())
                    {
                        while (reader.Read())
                        {
                            string tableName = reader["TABLE_NAME"].ToString();
                            string tableType = reader["TABLE_TYPE"].ToString();
                            list.Add(new Tuple<string, string>(tableName, tableType));
                        }
                    }
                }

                // Delete all the objects in the Schema
                if (list.Count > 0)
                {
                    using (SqlCommand deleteTableCmd = connection.CreateCommand())
                    using (SqlCommand deleteViewCmd = connection.CreateCommand())
                    {
                        foreach (Tuple<string, string> item in list)
                        {
                            switch (item.Item2)
                            {
                                case "BASE TABLE":
                                    deleteTableCmd.CommandText = $"DROP TABLE [{catalog}].[{schema}].[{item.Item1}]";
                                    deleteTableCmd.ExecuteNonQuery();
                                    break;
                                case "VIEW":
                                    deleteViewCmd.CommandText = $"DROP VIEW [{catalog}].[{schema}].[{item.Item1}]";
                                    deleteViewCmd.ExecuteNonQuery();
                                    break;
                                default:
                                    throw new InvalidDataException($"Found table type '{item.Item2}' in [INFORMATION_SCHEMA].[TABLES] for" +
                                        $" [{catalog}].[{schema}].[{item.Item1}], expected 'BASE TABLE' or 'VIEW'.");
                            }
                        }
                    }
                }

                // Delete the schema itself
                using (SqlCommand dropSchemaCmd = connection.CreateCommand())
                {
                    dropSchemaCmd.CommandText = $"DROP SCHEMA {schema}";
                    dropSchemaCmd.ExecuteNonQuery();
                }
                schema = null;
                schemaCreated = false;
                return;
            }
        }


        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing)
                {
                    // TODO: dispose managed state (managed objects)
                }

                // Free unmanaged resources (unmanaged objects) and override finalizer
                deleteSchema();

                disposedValue = true;
            }
        }

        ~DataBaseUnitTestHelper()
        {
            // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
            Dispose(disposing: false);
        }

        public void Dispose()
        {
            // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
            Dispose(disposing: true);
            GC.SuppressFinalize(this);
        }
    }
}

Вы можете использовать его следующим образом:

        [Fact]
        public void testSchemaIsReallyCreated()
        {
            string schema;
            string connectString;

            using (DatabaseUnitTestHelper dbhelper = new DatabaseUnitTestHelper(defaultDataSource, defaultInitialCatalog))
            {
                connectString = dbhelper.connectString;
                schema = dbhelper.createNewTestSchema();

                bool schemaExists;
                using (SqlConnection connection = new SqlConnection(connectString))
                {
                    connection.Open();
                    SqlCommand cmd = connection.CreateCommand();
                    cmd.CommandText = $"SELECT COUNT(*) from SYS.SCHEMAS WHERE name = @schema";
                    cmd.Parameters.AddWithValue("schema", schema);
                    schemaExists = (int)cmd.ExecuteScalar() > 0;
                }

                Assert.True(schemaExists, $"Schema {schema} doesn't exist although method {nameof(dbhelper.createNewTestSchema)} was executed and this schema name was returned.");
            }
        }

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