Популярні бібліотеки

Генерація тестових даних з Bogus в ASP.NET Core

Bogus (Faker.NET): генерація реалістичних тестових даних — імена, адреси, телефони, дати. Seed-data для БД, локалізація, складні графи об

Генерація тестових даних з Bogus в ASP.NET Core

Написання тестів завжди стикається з однією проблемою: звідки взяти дані? var user = new User { Name = "Test", Email = "test@test.com" } — це не реалістично. Реальні помилки ховаються в крайніх випадках: довгі імена з пробілами та апострофами, email з субдоменами, телефони у різних форматах. Bogus генерує реалістичні дані, що виявляють справжні проблеми.

1. Навіщо Bogus?

Проблема тестових «заглушок»

Ручне написання тестових даних має конкретні недоліки:

Проблема 1: Нереалістичність. "test@test.com" ніколи не виявить проблему з валідацією довгих субдоменів. "John Doe" не виявить проблему з апострофом у "O'Brien".

Проблема 2: Одноманітність. Коли всі тести використовують ті самі дані, деякі класи помилок залишаються невидимими.

Проблема 3: Трудоємність. Написання 50 унікальних записів для seed-data вручну — марна трата часу.

Bogus вирішує це через генерацію реалістичних даних, що відповідають реальним закономірностям: імена у правильному регістрі, валідні формати email, телефони у стандартних форматах, адреси з реальними назвами вулиць.


2. Встановлення та основи

Встановлення
dotnet add package Bogus

Базове використання Faker<T>

Центральний клас — Faker<T>. Він описує, як генерувати екземпляри типу T:

Generators/UserFaker.cs
using Bogus;

// Локаль "uk" — українські дані
var faker = new Faker<User>("uk")
    // RuleFor: задаємо правило для кожного поля
    .RuleFor(u => u.Id,        f => f.IndexFaker + 1)  // Послідовний ID
    .RuleFor(u => u.FirstName, f => f.Name.FirstName())
    .RuleFor(u => u.LastName,  f => f.Name.LastName())
    .RuleFor(u => u.Email,     (f, u) =>               // Другий аргумент — сам об'єкт
        // Email на основі вже згенерованого імені
        f.Internet.Email(u.FirstName, u.LastName))
    .RuleFor(u => u.Phone,     f => f.Phone.PhoneNumber("+380-##-###-##-##"))
    .RuleFor(u => u.BirthDate, f => f.Date.Past(45, refDate: DateTime.Now.AddYears(-18)))
    .RuleFor(u => u.Role,      f => f.PickRandom<UserRole>())
    .RuleFor(u => u.IsActive,  f => f.Random.Bool(weight: 0.85f)) // 85% активних
    .RuleFor(u => u.CreatedAt, f => f.Date.Past(2));   // Протягом останніх 2 років

// Генеруємо один об'єкт
var user = faker.Generate();

// Генеруємо 100 об'єктів
var users = faker.Generate(100);

Console.WriteLine($"{user.FirstName} {user.LastName} <{user.Email}>");
// → Олена Петренко <olena.petrenko@gmail.com>

3. Категорії даних

Bogus організує генератори у тематичні категорії:


4. Складні графи об'єктів

Реальні додатки мають вкладені структури. Bogus дозволяє будувати повні графи:

Generators/OrderFaker.cs — повний граф
public static class SeedFakers
{
    // Важливо: Seed забезпечує ОДНАКОВІ дані при кожному запуску
    private static readonly int Seed = 12345;

    public static Faker<Category> Category = new Faker<Category>("uk")
        .UseSeed(Seed)
        .RuleFor(c => c.Id,   f => f.IndexFaker + 1)
        .RuleFor(c => c.Name, f => f.Commerce.Department())
        .RuleFor(c => c.Slug, (f, c) => c.Name.ToLower()
            .Replace(" ", "-"));

    public static Faker<Product> Product = new Faker<Product>("uk")
        .UseSeed(Seed)
        .RuleFor(p => p.Id,          f => f.IndexFaker + 1)
        .RuleFor(p => p.Name,        f => f.Commerce.ProductName())
        .RuleFor(p => p.Description, f => f.Lorem.Paragraph(2))
        .RuleFor(p => p.Price,       f => f.Finance.Amount(10, 5000))
        .RuleFor(p => p.Stock,       f => f.Random.Int(0, 500))
        .RuleFor(p => p.Sku,         f => f.Commerce.Ean13())
        .RuleFor(p => p.CategoryId,  f => f.Random.Int(1, 10))
        .RuleFor(p => p.CreatedAt,   f => f.Date.Past(3))
        .RuleFor(p => p.IsActive,    f => f.Random.Bool(0.9f));

    public static Faker<Order> Order = new Faker<Order>("uk")
        .UseSeed(Seed)
        .RuleFor(o => o.Id,         f => f.IndexFaker + 1)
        .RuleFor(o => o.CustomerId, f => f.Random.Int(1, 100))
        .RuleFor(o => o.Status,     f => f.PickRandom<OrderStatus>())
        .RuleFor(o => o.CreatedAt,  f => f.Date.Past(1))
        .RuleFor(o => o.Items,      f => new Faker<OrderItem>()
            .RuleFor(i => i.ProductId, ff => ff.Random.Int(1, 50))
            .RuleFor(i => i.Quantity,  ff => ff.Random.Int(1, 10))
            .RuleFor(i => i.Price,     ff => ff.Finance.Amount(10, 999))
            .Generate(f.Random.Int(1, 5)))  // 1-5 позицій у замовленні
        .FinishWith((f, o) =>
        {
            // AfterGenerate: обчислюємо Total після генерації Items
            o.Total = o.Items.Sum(i => i.Price * i.Quantity);
        });
}

5. Seed-data для EF Core

Найпрактичніше застосування — заповнення бази даних тестовими даними:

Data/DbSeeder.cs
public class DbSeeder
{
    private readonly AppDbContext _db;
    private readonly ILogger<DbSeeder> _logger;

    public DbSeeder(AppDbContext db, ILogger<DbSeeder> logger)
    {
        _db     = db;
        _logger = logger;
    }

    public async Task SeedAsync()
    {
        if (await _db.Users.AnyAsync())
        {
            _logger.LogInformation("БД вже містить дані — пропускаємо сідінг.");
            return;
        }

        _logger.LogInformation("Починаємо сідінг БД...");

        // Генеруємо категорії
        var categories = SeedFakers.Category.Generate(10);
        await _db.Categories.AddRangeAsync(categories);
        await _db.SaveChangesAsync();

        // Генеруємо продукти (пов'язуємо з реальними CategoryId)
        var categoryIds = categories.Select(c => c.Id).ToList();
        var products = new Faker<Product>("uk")
            .UseSeed(12345)
            .RuleFor(p => p.Id,         f => f.IndexFaker + 1)
            .RuleFor(p => p.Name,       f => f.Commerce.ProductName())
            .RuleFor(p => p.Price,      f => f.Finance.Amount(10, 5000))
            .RuleFor(p => p.CategoryId, f => f.PickRandom(categoryIds))
            .Generate(50);

        await _db.Products.AddRangeAsync(products);
        await _db.SaveChangesAsync();

        _logger.LogInformation(
            "Сідінг завершено: {Categories} категорій, {Products} продуктів.",
            categories.Count, products.Count);
    }
}
Program.cs — виклик Seeder
using (var scope = app.Services.CreateScope())
{
    if (app.Environment.IsDevelopment())
    {
        var seeder = scope.ServiceProvider.GetRequiredService<DbSeeder>();
        await seeder.SeedAsync();
    }
}

6. Bogus у Unit-тестах

З xUnit та AutoFixture

Tests/UserServiceTests.cs — Bogus в тестах
public class UserServiceTests
{
    private readonly Faker<User> _userFaker = new Faker<User>("uk")
        .RuleFor(u => u.Id,        f => f.Random.Int(1, 10000))
        .RuleFor(u => u.FirstName, f => f.Name.FirstName())
        .RuleFor(u => u.LastName,  f => f.Name.LastName())
        .RuleFor(u => u.Email,     f => f.Internet.Email());

    [Fact]
    public async Task CreateUser_WithValidData_ReturnsUser()
    {
        // Arrange: генеруємо реалістичний запит
        var request = new Faker<CreateUserRequest>()
            .RuleFor(r => r.FirstName, f => f.Name.FirstName())
            .RuleFor(r => r.LastName,  f => f.Name.LastName())
            .RuleFor(r => r.Email,     f => f.Internet.Email())
            .RuleFor(r => r.Password,  f => f.Internet.Password(12))
            .Generate();

        // Act
        var result = await _service.CreateAsync(request);

        // Assert
        Assert.True(result.IsError == false);
        Assert.Equal(request.Email, result.Value.Email);
    }

    [Fact]
    public async Task GetAll_ReturnsAllUsers()
    {
        // Генеруємо 25 реалістичних користувачів
        var users = _userFaker.Generate(25);
        _db.Users.AddRange(users);
        await _db.SaveChangesAsync();

        var result = await _service.GetAllAsync();

        Assert.Equal(25, result.Count);
    }
}

Property-Based Testing підхід

Tests/ValidationTests.cs — перевірка з множиною варіантів
[Theory]
[MemberData(nameof(GetInvalidEmails))]
public void Validator_ShouldFail_ForInvalidEmail(string email)
{
    var request = new RegisterRequest { Email = email };
    var result  = _validator.TestValidate(request);
    result.ShouldHaveValidationErrorFor(x => x.Email);
}

public static IEnumerable<object[]> GetInvalidEmails()
{
    // Комбінуємо Bogus із відомими крайніми випадками
    var faker = new Faker();

    yield return [faker.Lorem.Word()];        // Просто слово
    yield return [faker.Lorem.Sentence()];    // Речення
    yield return ["@domain.com"];             // Без local-part
    yield return ["user@"];                   // Без domain
    yield return ["user@.com"];               // Крапка на початку домену
    yield return [faker.Random.AlphaNumeric(256) + "@test.com"]; // Завдовгий
}

7. Локалізація

Bogus підтримує 60+ локалей:

Доступні локалі
new Faker("uk")    // Українська
new Faker("en")    // Англійська (за замовчуванням)
new Faker("pl")    // Польська
new Faker("de")    // Німецька
new Faker("fr")    // Французька
new Faker("ja")    // Японська
new Faker("zh_CN") // Китайська (спрощена)
new Faker("ar")    // Арабська
Мультилінгвальні дані
// Генерація з випадковою локаллю для реального тестування
var locales = new[] { "uk", "en", "pl", "de" };
var faker   = new Faker(new Random().Next(locales));
var user    = faker.Name.FullName();

Практичні завдання


Резюме

Реалістичність

Генеровані імена, email, адреси — реальні, не "test123". Виявляють справжні баги.

Seed

UseSeed(42) — однакові дані при кожному запуску. Детерміновані тести, стабільна seed-data.

Локалізація

60+ локалей включно з українською. Імена, адреси відповідають реальним даним регіону.

Граф об'єктів

FinishWith() та вкладені Faker<T> — генерація складних ієрархічних структур.

Посилання: