Generic Repository Design Pattern- Test Data Preparation

Generic Repository Design Pattern- Test Data Preparation

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.

Related Articles

Design Patterns

Observer Design Pattern Classic Implementation in Automated Testing

The Observer Design Pattern defines a one-to-many dependency between objects so that when one object changes state, all of its dependents are notified and updat

Observer Design Pattern Classic Implementation in Automated Testing

Design Patterns

Advanced Page Object Pattern in Automated Testing

While ago we were working on the first version of the BELLATRIX test automation framework, I did this research so that we can find the most convenient way for c

Advanced Page Object Pattern in Automated Testing

Design Patterns

Specification Design Pattern in Automated Testing

If you follow my series about Design Patterns in Automated Testing, I explain how you can utilize the power of various design patterns in your tests. In the cur

Specification Design Pattern in Automated Testing

Design Patterns

Page Objects That Make Code More Maintainable

If you have read some of my previous posts, most probably you have checked some of my articles about Design Patterns in Automated Testing. One of the most promi

Page Objects That Make Code More Maintainable

Design Architecture, Design Patterns

Highlight Elements on Action- Test Automation Framework Extensibility through Observer Design Pattern

As you know, in past articles from the Design and Architecture Series I wrote about the 5th generation test automation frameworks or as I like to call them Full

Highlight Elements on Action- Test Automation Framework Extensibility through Observer Design Pattern

Design Patterns

Custom Test Automation Framework: Modularity Planning and Design

In the new Build Custom Automation Framework Series, we will look into detailed explanations on creating custom test automation frameworks. Many people starting

Custom Test Automation Framework: Modularity Planning and Design
Anton Angelov

About the author

Anton Angelov is Managing Director, Co-Founder, and Chief Test Automation Architect at Automate The Planet — a boutique consulting firm specializing in AI-augmented test automation strategy, implementation, and enablement. He is the creator of BELLATRIX, a cross-platform framework for web, mobile, desktop, and API testing, and the author of 8 bestselling books on test automation. A speaker at 60+ international conferences and researcher in AI-driven testing and LLM-based automation, he has been recognized as QA of the Decade and Webit Changemaker 2025.