How to Set Autoincrement Ids in Unit Test

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:

  1. If your primary key is foreign key
  2. 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



Leave a reply



Submit