Often we can run the tests against an empty DB. However, we still need initial data. We can generate it ourselves. To do so, we need to add a code for accessing and modifying it. Then we will create a factory for producing the specific data. We can call it as a simple console application which can be run before each test run, or execute it based on a schedule. We can decouple the test data generation and the test run because we don’t want our test runs to fail if, for some reason, the test data generation fails. This is why I usually prefer to move it to a separate application that is run on a schedule and make sure the data is reset and generated. The test runs are faster this way.
For more detailed overview and usage of many more design patterns and best practices in automated testing, check my book “Design Patterns for High-Quality Automated Tests, C# Edition, High-Quality Tests Attributes, and Best Practices”. You can read part of three of the chapters:
Defining High-Quality Test Attributes for Automated Tests
Benchmarking for Assessing Automated Test Components Performance
Creating DB Access Layer
For the access layer we will use an ORM framework called EntityFrameworkCore.
Note
ORM is a standard for object-relational mapping. ORM frameworks are a middleware between DB and the code. You can work with standard programming language constructs such as classes and methods. After that the ORM framework is responsible for translating the call to the specific DB query syntax. For example, EntityFrameworkCore supports multiple types of DB technologies such as SQLite or MS SQL Server.
For the sake of the example, we will have only a single table in our DB called Users. For DB engine I will use SQLite since the whole DB is stored in a single file. The representation of the table Users in C# will be a class named User.
public class User
{
public int Id { get; set; }
public string Email { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Password { get; set; }
}
I will add all of the needed classes in a new project called DataAccess.Core. To work with EntityFrameworkCore, you need to add two NuGet packages. The first one is about the ORM itself called Microsoft.EntityFrameworkCore. The next one is the DB specific one. In our case, it will be the one for SQLite support- Microsoft.EntityFrameworkCore.Sqlite.
Next, we need to create the so-called DB context class, which is the main access point to the DB.
public sealed class UsersDBContext : DbContext
{
public UsersDBContext(DbContextOptions<UsersDBContext> options)
: base(options)
{
Database.EnsureCreated();
}
public DbSet<User> Users { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
optionsBuilder.UseSqlite("Data Source=users.db");
}
}
}
If the users.db file doesn’t exist the first time you access the context- the framework will generate it. We can make queries against the Users table using the Users DbSet property. You can use the class directly to make queries against the DB using SQL language, however, usually most teams use the repository design pattern to create one more abstraction level and hide the implementation details. How about reviewing how we can create a generic repository?
Generic Repository Design Pattern
The idea of the repository design pattern is to hide the data access logic from the business code. Hiding the details makes the code easier to read, maintain, and unit test. Also, in some rare cases, you can even change the data access technology easily. With this layer of abstraction, your code doesn’t know that you use EntityFrameworkCore, for example for ORM. Usually, the repository exposes CRUD operation against a particular DB table. Since the core access code will be located in a single place, the duplication will be minimal, and any further refactoring efforts will be easier- like changing the caching mechanism.
Note
CRUD stands for create, read, update, delete. These are the four basic functions each persistent storage should support.
Generic Repository Design Pattern in C#
How about examining a sample generic C# repository?
public abstract class DbRepository<TContext> : IDisposable
where TContext : DbContext
{
private TContext _context;
public void Dispose()
{
_context?.Dispose();
GC.SuppressFinalize(this);
}
public IQueryable<TEntity> GetAllQuery<TEntity>()
where TEntity : class
{
return Context.Set<TEntity>();
}
public IQueryable<TEntity> GetAllQueryWithInclude<TEntity>(params Expression<Func<TEntity, object>>[] actions)
where TEntity : class
{
DbSet<TEntity> dbSet = Context.Set<TEntity>();
IQueryable<TEntity> result = dbSet;
foreach (var action in actions)
{
result = result.Include(action);
}
return result;
}
public IQueryable<TEntity> GetQueryType<TEntity>()
where TEntity : class
{
return Context.Query<TEntity>();
}
public void Delete<TEntity>(TEntity entityToBeRemoved)
where TEntity : class
{
Context.Set<TEntity>().Remove(entityToBeRemoved);
Save<TEntity>(Context);
}
public void DeleteRange<TEntity>(IEnumerable<TEntity> entitiesToBeDeleted)
where TEntity : class
{
Context.RemoveRange(entitiesToBeDeleted);
Save<TEntity>(Context);
}
public TEntity Insert<TEntity>(TEntity entityToBeInserted)
where TEntity : class
{
Context.Set<TEntity>().Add(entityToBeInserted);
Save<TEntity>(Context);
return entityToBeInserted;
}
public void InsertRange<TEntity>(IEnumerable<TEntity> entitiesToBeInserted)
where TEntity : class
{
Context.Set<TEntity>().AddRange(entitiesToBeInserted);
Save<TEntity>(Context);
}
public TEntity Update<TEntity>(TEntity entityToBeUpdated)
where TEntity : class
{
Context.Set<TEntity>().Update(entityToBeUpdated);
Save<TEntity>(Context);
return entityToBeUpdated;
}
public IEnumerable<TEntity> UpdateRange<TEntity>(IEnumerable<TEntity> entitiesToBeUpdated)
where TEntity : class
{
Context.UpdateRange(entitiesToBeUpdated);
Save<TEntity>(Context);
return entitiesToBeUpdated;
}
protected abstract TContext CreateDbContextObject();
protected TContext Context
{
get
{
if (_context == null)
{
_context = CreateDbContextObject();
}
return _context;
}
}
private void Save<TEntity>(TContext context)
where TEntity : class
{
context.SaveChanges();
DetachEntities<TEntity>(context);
}
private void DetachEntities<TEntity>(TContext context)
where TEntity : class
{
context.Set<TEntity>().Local.ToList().ForEach(c =>
{
context.Entry(c).State = EntityState.Detached;
});
}
}
I won’t discuss the low-level details here since there are specifics of how the EntityFrameworkCore works which is far from the idea of the book. The main points here are that since the repo is generic, it can work for any DB context and any table in it. Also, it exposes all four CRUD operations.
To use the generic repo we need to derive from it. In our case we will have a new UsersRepository class.
public class UsersRepository : DbRepository<UsersDBContext>
{
protected override UsersDBContext CreateDbContextObject()
{
return new UsersDBContext(new DbContextOptions<UsersDBContext>());
}
}
You will see in a minute how it is used in practice. The CRUD operations will help us create the batch of test users into our empty DB.
Creating Users Factory
Before we develop the users’ factory, we will need two additional utility classes for creating unique data. The first one is called TimestampBuilder, which can be used in many cases to generate beautified data with dates included, which helps later during the maintenance of the tests.
public static class TimestampBuilder
{
public static string GenerateUniqueText(string text)
{
var newTimestamp = GenerateUniqueText();
var result = string.Concat(text, newTimestamp);
return result;
}
public static string GenerateUniqueText()
{
var newTimestamp = DateTime.Now.ToString("MM-dd-yyyy-hh-mm-ss-ffff");
return newTimestamp;
}
public static string GenerateUniqueTextMonthNameOneWord()
{
var newTimestamp = DateTime.Now.ToString("MMMMddyyyyhhmmss");
return newTimestamp;
}
}
The second class we will use to generate unique emails for the test users.
public static class UniqueEmailGenerator
{
public static string EmailPrefix { get; set; } = "atp";
public static string EmailSuffix { get; set; } = "bellatrix.solutions";
public static string GenerateUniqueEmail(string prefix, string sufix)
{
var result = string.Concat(prefix, "_", TimestampBuilder.GenerateUniqueText(), "@", sufix);
return result;
}
public static string GenerateUniqueEmailTimestamp()
{
var result = $"{EmailPrefix}-{TimestampBuilder.GenerateUniqueText()}@{EmailSuffix}";
return result;
}
public static string GenerateUniqueEmailGuid()
{
var result = $"{EmailPrefix}-{Guid.NewGuid()}@{EmailSuffix}";
return result;
}
public static string GenerateUniqueEmail(string prefix)
{
var result = $"{prefix}{TimestampBuilder.GenerateUniqueText()}@{EmailSuffix}";
return result;
}
public static string GenrateUniqueEmail(char specialSymbol)
{
var result = $"{EmailPrefix}-{TimestampBuilder.GenerateUniqueText()}{specialSymbol}@{EmailSuffix}";
return result;
}
}
Now it is time to develop the repository itself after we have all necessary utility classes. The whole idea will be that we want to have a method which can generate X number of users. We want to maintain, for example, a pool of 5000 test users in our DB. All users will be created with specific email, which can help us to determine the count of available test users. Also, the factory will have one more method for getting a test user from the pool. Once the user is retrieved from the pool, it will be marked as used. In our case, we will add a suffix to the last name.
public class UsersFactory
{
private readonly UsersRepository _usersRepository;
public UsersFactory(UsersRepository usersRepository)
{
_usersRepository = usersRepository;
}
public void GenerateUsers(int usersCount)
{
var activeUsers = _usersRepository.GetAllQuery<User>()
.Where(x => !x.LastName.EndsWith("used") && x.Email.StartsWith("atp"));
if (activeUsers.Count() < usersCount)
{
int numberOfUsersToBeGenerated = usersCount - activeUsers.Count();
for (int i = 0; i < numberOfUsersToBeGenerated; i++)
{
var fixture = new Fixture();
var newUser = new User()
{
Email = UniqueEmailGenerator.GenerateUniqueEmailTimestamp(),
FirstName = fixture.Create<string>(),
LastName = fixture.Create<string>(),
Password = fixture.Create<Guid>().ToString(),
};
_usersRepository.Insert(newUser);
}
}
}
public User GetUser()
{
var user = _usersRepository.GetAllQuery<User>().First(x => x.Email.StartsWith("atp"));
user.LastName += "used";
_usersRepository.Update(user);
return user;
}
}
As you can see, the UsersRepository is a dependency of the class since we use it to retrieve the existing users and create/update new ones. We use the GetAllQuery generic method to get the number of all available users. We use the C# LINQ extension method Where to filter the users. After that, we see how many users we need to create additionally and generate them in the cycle. Also, we use the AutoFixture library to generate the names and passwords (explained later). Lastly, we insert the new user in the DB through the Insert method of the generic repository.
As explained, we use the GetUser method to retrieve a test user from the users’ pool. We use another C# LINQ method called First, which will return the first object that meets the specified condition. After we get it, we add the suffix “used” to the user’s last name so that we can mark it as unavailable. We use the repo’s Update method to update the object.
Note
C# language contains a set of technologies named LINQ (language integrated query). Before them, all queries were made through strings without type checking at compile time or IntelliSense support. Also, various DB technologies require different queries’ syntax. LINQ hides these details and translates the C# code to the compatible low-level query languages.
Using AutoFixture for Generating Data
Sometimes we need to change a bit of it. Other times, we need to set random info as long as it is not empty. For cases where you don’t care about specifics, you can use a library called AutoFixture for handling the data generation. It will assign random values to the properties. It can handle complex setups such as whole object initialization and even doing recursive data generation.
public void PurchaseSaturnVWithRandomNoteFacade()
{
var purchaseInfo = new PurchaseInfo();
var fixture = new Fixture();
purchaseInfo.Note = fixture.Create<string>();
_purchaseFirstVersionFacade.PurchaseItem("Saturn V", "happybirthday", 3, "355.00€", purchaseInfo);
}
Here we use the Fixture class part of the AutoFixture library to generate a random order note.
Note
To use the Fixture logic you need to install a NuGet package called AutoFixture.
UsersFactory Usage
After we have the UsersFactory, we can use it in various ways. You can call it once per test run, or even before each test. For example, in the AssemblyInitialize method, it will be executed once before all tests. My preferred approach is to wrap it in a simple console application and call it on a regular basis from a CI job that has nothing to do with the tests. This way if the users’ creation fails or throws an exception, it won’t interrupt my test execution.
public class Program
{
static void Main(string[] args)
{
Console.WriteLine("Start Generating Users....");
var usersFactory = new UsersFactory(new UsersRepository());
usersFactory.GenerateUsers(5000);
Console.WriteLine("DONE");
}
}
Summary
We spoke of the Generic Repository design pattern and how it can help us to access DB data. We developed a sample application for a pool of available test users.
