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





Существуют разные решения этой проблемы. Но все они сводятся к удалению общего ресурса.
Удалите распараллеливание для xUnit: вы можете сделать это, добавив xunit.runner.json и добавив parallelizeTestCollections в этот файл, как описано в документации , и вы можете использовать Respawn вместе с этим, чтобы восстановить базу данных до контрольной точки после каждый тест. Если у вас много тестов, то это решение может быть медленным, но может быть быстрее, чем каждый раз запускать БД. (это не рекомендуется, см. ответ @RubenBartelink ниже)
Если между двумя пользователями каждого теста нет связи, вы можете использовать разные идентификаторы для каждого пользователя и сделать тест независимым друг от друга.
Если тест не посвящен интеграции с БД, вы можете использовать базу данных в памяти.
И, наконец, вы можете использовать Docker-образ базы данных, возможно, изменяя один из параметров подключения, чтобы сделать каждый тест нацеленным на отдельную базу данных или схему и т. д.
Хотя некоторые из предложенных вами вещей полезны, есть и откровенно плохие советы. Полная нейтрализация параллелизма не является необходимой или хорошей идеей. Collection Fixtures — это способ ограничить параллельный доступ более целенаправленным образом.
@RubenBartelink спасибо за дополнение. Добавлю в редакцию.
Используйте Collection Fixture. Это отвечает вашим потребностям:
Фикстура коллекции сама по себе синхронизирует доступ к фикстуре несколькими тестами в одной коллекции?
Я обнаружил, что это должно быть использование фикстуры Collection вместе с пользовательской тестовой коллекцией для тестов, которые должны запускаться последовательно. Спасибо Рубен.
Да. Параллелизм никогда не применяется в пределах одного тестового класса (или между экземплярами теории и т. д.). Думайте о фикстуре Collection как об объединении тестовых классов с точки зрения управления параллелизмом.
Я оказался в похожей ситуации и создал вспомогательный класс для модульных тестов, требующих доступа к базе данных, который создает новую схему в базе данных для теста и удаляет ее после 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.");
}
}
на самом деле ответ - докер, но проблема в том, что наш проект не использует докер, поэтому я не могу его использовать. Но Respawn был практичным, и я думаю, что это то, что я ищу, спасибо