How to set autoincrement Ids in unit test
Should I use public setter instead ?
Probably not, no.
There are two alternatives to consider.
One is that, if the identifier is really private data, then the test shouldn't need to interact with it directly. We limit the scope of the test to the observable side effects in the model, so that we have the freedom to later change the private implementation details.
Another is that the test is trying to draw your attention to the fact that there are different strategies that you might use for generating the identifier, and that a different strategy might be appropriate in different contexts -- one strategy in use in production code, another for use in the test harness.
The basic pattern isolates the strategy and provides the affordances you need to control which strategy is applied. In the test, we implement the strategy contract using a test double, which allows the test to maintain deterministic control over what would otherwise be an arbitrary side effect.
Testing problem, test order and auto increment
Here, I'm not sure but think I've found a solution.
I think these two ways can be used:
One with the help of annotation @DirtiesContext(methodMode = DirtiesContext.MethodMode.BEFORE_METHOD)
that reloads the application context and restarts everything but slows down the execution of tests.
And the other way is with sql annotation @Sql(statements = "ALTER TABLE role AUTO_INCREMENT = 2")
we call before the test method and restart the auto increment with the sql statement.
I would like you to comment on whether this solution is good or not. Of course any advice is welcome.
Thanks.
Auto increment primary key in mocked db context
I finally could solve this. As you see in the following, EntityId is primary key in my Product class and I used [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] attribute in order to have Auto-increment identity column for EntityId
public class Product
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long EntityId { get; set; }
// rest of the code
}
In this case items are added to my mocked dbContext with EntityIds starting from 1.
Suppose you set up your dbContext with one item that has EntityId 1. Then
you call the Add function to add a product to Products table you will get the same error since it tries to add a product with EntityId 1 which was already in your mocked dbContext. So Items with the same keys cannot be in the db.
You may NOT use this attribute for your primary key in 2 cases:
- If your primary key is foreign key
- when you have ValueGeneratedNever() method on your primary key
AutoIncrement with HSQL, Hibernate and Spring and Tests
This is expected. auto-increment IDs are handled by the database, so, unless you drop the tables and recreate them before each test (which you shouldn't do), the IDs will keep incrementing.
Just don't rely in absolute values for the IDs in your tests. Get the ID values from the entities that you create when setting up your test case.
Deleting test rows with an auto increment key in a unit test
The common practice is to use a special blank database just for tests. Then in setUp()
you fill your tables with test data, and in tearDown()
you clean everything.
To be more precise: declare some cleanDatabase()
method and invoke it in 2 places, in setup and in teardown.
c# mocking EF6 context and dealing with auto increment primary keys
I had to develop my own solution for this as follows:
/// <summary>
/// A helper class for managing custom behaviors of Mockable database contexts
/// </summary>
public static partial class EFSaveChangesBehaviors
{
/// <summary>
/// Enable auto-incrementing of primary key values upon SaveChanges/SaveChangesAsync
/// </summary>
/// <typeparam name="T">The type of context to enable auto-incrementing on</typeparam>
/// <param name="context">The context to enable this feature</param>
public static void EnableAutoIncrementOnSave<T>(this Mock<T> context) where T : DbContext
{
context.Setup(m => m.SaveChangesAsync())
.Callback(() =>
{
EFSaveChangesBehaviors.SaveChangesIncrementKey(context.Object);
})
.Returns(() => Task.Run(() => { return 1; }))
.Verifiable();
context.Setup(m => m.SaveChanges())
.Callback(() =>
{
EFSaveChangesBehaviors.SaveChangesIncrementKey(context.Object);
})
.Returns(() => { return 1; })
.Verifiable();
}
/// <summary>
/// Implements key incrementing of data records that are pending to be added to the context
/// </summary>
/// <param name="context"></param>
public static void SaveChangesIncrementKey(DbContext context)
{
var tablesWithNewData = GetUnsavedRows<DbContext>(context);
for (int i = 0; i < tablesWithNewData.Count; i++)
{
long nextPrimaryKeyValue = 0;
var tableWithDataProperty = tablesWithNewData[i];
var tableWithDataObject = tableWithDataProperty.GetValue(context);
if (tableWithDataObject != null)
{
var tableWithDataQueryable = tableWithDataObject as IQueryable<object>;
// 1) get the highest value in the DbSet<> (table) to continue auto-increment from
nextPrimaryKeyValue = IterateAndPerformAction(context, tableWithDataQueryable, tableWithDataProperty, nextPrimaryKeyValue, (primaryExistingKeyValue, primaryKeyRowObject, primaryKeyProperty) =>
{
if (primaryExistingKeyValue > nextPrimaryKeyValue)
nextPrimaryKeyValue = Convert.ToInt64(primaryExistingKeyValue);
return nextPrimaryKeyValue;
});
// 2) increase the value of the record's primary key on each iteration
IterateAndPerformAction(context, tableWithDataQueryable, tableWithDataProperty, nextPrimaryKeyValue, (primaryKeyExistingValue, primaryKeyRowObject, primaryKeyProperty) =>
{
if (primaryKeyExistingValue == 0)
{
nextPrimaryKeyValue++;
Type propertyType = primaryKeyProperty.PropertyType;
if (propertyType == typeof(Int64))
primaryKeyProperty.SetValue(primaryKeyRowObject, nextPrimaryKeyValue);
else if (propertyType == typeof(Int32))
primaryKeyProperty.SetValue(primaryKeyRowObject, Convert.ToInt32(nextPrimaryKeyValue));
else if (propertyType == typeof(Int16))
primaryKeyProperty.SetValue(primaryKeyRowObject, Convert.ToInt16(nextPrimaryKeyValue));
else if (propertyType == typeof(byte))
primaryKeyProperty.SetValue(primaryKeyRowObject, Convert.ToByte(nextPrimaryKeyValue));
else
throw new System.NotImplementedException($"Cannot manage primary keys of type: {propertyType.FullName}");
}
return nextPrimaryKeyValue;
});
}
}
}
/// <summary>
/// Get a list of properties for a data table that are indicated as a primary key
/// </summary>
/// <param name="t"></param>
/// <param name="context"></param>
/// <returns></returns>
/// <remarks>Reflection must be used, as the ObjectContext is not mockable</remarks>
public static PropertyInfo[] GetPrimaryKeyNamesUsingReflection(Type t, DbContext context)
{
var properties = t.GetProperties();
var keyNames = properties
.Where(prop => Attribute.IsDefined(prop, typeof(System.ComponentModel.DataAnnotations.KeyAttribute)))
.ToArray();
return keyNames;
}
/// <summary>
/// Iterates a table's data and allows an action to be performed on each row
/// </summary>
/// <param name="context">The database context</param>
/// <param name="tableWithDataQueryable"></param>
/// <param name="tableWithDataProperty"></param>
/// <param name="nextPrimaryKeyValue"></param>
/// <param name="action"></param>
/// <returns></returns>
private static long IterateAndPerformAction(DbContext context, IQueryable<object> tableWithDataQueryable, PropertyInfo tableWithDataProperty, long nextPrimaryKeyValue, Func<long, object, PropertyInfo, long> action)
{
foreach (var primaryKeyRowObject in tableWithDataQueryable)
{
// create a primary key for the object
if (tableWithDataProperty.PropertyType.GenericTypeArguments.Length > 0)
{
var dbSetType = tableWithDataProperty.PropertyType.GenericTypeArguments[0];
// find the primary key property
var primaryKeyProperty = GetPrimaryKeyNamesUsingReflection(dbSetType, context).FirstOrDefault();
if (primaryKeyProperty != null)
{
var primaryKeyValue = primaryKeyProperty.GetValue(primaryKeyRowObject) ?? 0L;
nextPrimaryKeyValue = action(Convert.ToInt64(primaryKeyValue), primaryKeyRowObject, primaryKeyProperty);
}
}
}
return nextPrimaryKeyValue;
}
/// <summary>
/// Get a list of objects which are pending to be added to the context
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="context"></param>
/// <returns></returns>
private static IList<PropertyInfo> GetUnsavedRows<T>(T context)
{
// get list of properties of type DbSet<>
var dbSetProperties = new List<PropertyInfo>();
var properties = context.GetType().GetProperties();
foreach (var property in properties)
{
var setType = property.PropertyType;
var isDbSet = setType.IsGenericType && (typeof(IDbSet<>).IsAssignableFrom(setType.GetGenericTypeDefinition()) || setType.GetInterface(typeof(IDbSet<>).FullName) != null);
if (isDbSet)
{
dbSetProperties.Add(property);
}
}
return dbSetProperties;
}
}
Usage:
// enable auto-increment in our in-memory database
MockTenantContext.EnableAutoIncrementOnSave();
Related Topics
Remove Hours:Seconds:Milliseconds in Datetime Object
How to Make a Soap/Wsdl Client in C#
Launch an Application and Send It to Second Monitor
How to Convert String Formula to a Mathematical Formula in C#
How to Get the Xml Soap Request of an Wcf Web Service Request
Why Does Integer Division in C# Return an Integer and Not a Float
The Find Element Returns Empty String..Using Xpath Contains,Text()
Using X-Alt-Desc/Applying HTML to Calendar Invites in Outlook
Accessing a Shared File (Unc) from a Remote, Non-Trusted Domain With Credentials
How to Connect to a Database in ASP.NET Core Without Entity Framework
How to Pass Parameter from @Url.Action to Controller Function
How to Trigger Event When a Variable'S Value Is Changed
How to Use Telegram API in C# to Send a Message
Automatically Update Values in Database from Datagridview
Null Value on Xml Deserialization Using [Xmlattribute]