• Bogus 实战:使用 Bogus 和 EFCore 生成模拟数据和种子数据【完整教程】


    引言

    上一章我们介绍了在xUnit单元测试中用xUnit.DependencyInject来使用依赖注入,上一章我们的Sample.Repository仓储层有一个批量注入的接口没有做单元测试,今天用这个示例来演示一下如何用Bogus创建模拟数据 ,和 EFCore 的种子数据生成

    Bogus 的优势

    1. 丰富的数据生成支持:Bogus 提供了广泛的 API 支持,涵盖了各种数据类型和用例,使得生成虚假数据变得非常灵活和方便。

    2. 重复性和可控性:通过设置种子值,可以确保生成的虚假数据是可重复的,这对于需要一致的测试数据或示例数据非常有用。

    3. 易于使用:Bogus 使用流畅的语法和简单的方法调用,使得生成虚假数据变得简单直观,即使是对库不熟悉的用户也可以快速上手。

    4. 内置规则和语义:内置了许多常见数据类别的规则和语义,例如公司名称、产品名称、地址等,可以快速生成符合实际场景的数据。

    5. 灵活性:除了内置规则外,还可以通过自定义规则来生成特定的数据,满足不同场景下的需求。

    6. 社区支持:Bogus 是一个受欢迎的开源库,拥有活跃的社区支持和维护,可以获得持续的更新和改进。

    Bogus 实战

    简介

    Bogus 是一个简单的.NET 语言(如 C#F#VB.NET)的假数据生成器。Bogus 本质上是 faker.jsC#移植版本,并受到 FluentValidation 的语法糖的启发。

    使用

    创建新的xUnit测试项目dotNetParadise.Bogus

    Nuget包安装Bogus

    Install-Package Bogus

    
    创建一个`Bogus`帮助类
    ```c#
    using Bogus;
    using Sample.Repository.Models;
    
    namespace dotNetParadise.Bogus
    {
        public class BogusHelper
    ```package manager
    PM> NuGet\Install-Package Bogus -Version 35.5.0
    

    和上一篇的配置一样,测试项目需要添加仓储层的项目引用,并通过Nuget安装xUnit.DependencyInject,配置Startup

    先看一下我们的Staff实体

    public class Staff
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Email { get; set; }
        public int? Age { get; set; }
        public List<string>? Addresses { get; set; }
        public DateTimeOffset? Created { get; set; }
    }
    

    接下来对我们批量新增的接口进行单元测试,测试数据通过Bogus生成,先看使用在讲解用法。

    生成500条测试数据保存到 DB

    [Fact]
    public async Task BatchAddStaffAsync_WhenCalled_ShouldAddStaffToDatabase()
    {
        // Arrange
        var staffs = new Faker()
            .RuleFor(u => u.Name, f => f.Person.FullName)
            .RuleFor(u => u.Email, f => f.Person.Email)
            .RuleFor(u => u.Age, f => f.Random.Number(18, 60))
            .RuleFor(u => u.Addresses, f => f.MakeLazy(f.Random.Number(1, 3), () => f.Address.FullAddress()))
            .RuleFor(u => u.Created, f => f.Date.PastOffset())
            .Generate(500);
    
        // Act
        await _staffRepository.BatchAddStaffAsync(staffs, CancellationToken.None);
    
        // Assert
        var retrievedStaffs = await _staffRepository.GetAllStaffAsync(CancellationToken.None);
        Assert.NotNull(retrievedStaffs); // 确保 Staff 已成功添加到数据库
        Assert.Equal(500, retrievedStaffs.Count); // 确保正确数量的 Staff 已添加到数据库
        Assert.True(staffs.All(x => retrievedStaffs.Any(_ => x.Id == _.Id)));
    }
    

    看代码配置跟FluentValidation都是一样都是通过RuleFor来配置实体的属性

    看一下生成的测试数据

    image

    Run Tests

    image

    单元测试成功,有了Bogus之后我们创建一些测试数据就方便多了

    Bogus 的用法

    locales 国际化

    Bogus 支持许多不同的地区设置(locales),这些地区设置可用于生成特定语言或地区的虚假数据。您可以通过设置不同的 locale 参数来使用不同的地区设置。

    Bogus 支持以下地区设置(locales

    Locale Code Language Locale Code Language
    af_ZA Afrikaans fr_CH French (Switzerland)
    ar Arabic ge Georgian
    az Azerbaijani hr Hrvatski
    cz Czech id_ID Indonesia
    de German it Italian
    de_AT German (Austria) ja Japanese
    de_CH German (Switzerland) ko Korean
    el Greek lv Latvian
    en English nb_NO Norwegian
    en_AU English (Australia) ne Nepalese
    en_AU_ocker English (Australia Ocker) nl Dutch
    en_BORK English (Bork) nl_BE Dutch (Belgium)
    en_CA English (Canada) pl Polish
    en_GB English (Great Britain) pt_BR Portuguese (Brazil)
    en_IE English (Ireland) pt_PT Portuguese (Portugal)
    en_IND English (India) ro Romanian
    en_NG Nigeria (English) ru Russian
    en_US English (United States) sk Slovakian
    en_ZA English (South Africa) sv Swedish
    es Spanish tr Turkish
    es_MX Spanish (Mexico) uk Ukrainian
    fa Farsi vi Vietnamese
    fi Finnish zh_CN Chinese
    fr French zh_TW Chinese (Taiwan)
    fr_CA French (Canada) zu_ZA Zulu (South Africa)

    有些地区设置可能没有完整的数据集,比如说,有些语言可能缺少某些数据集,例如中文(zh_CN)可能没有 lorem 数据集,但韩语(ko)有。在这种情况下,Bogus 会默认使用英文(en)的数据集。换句话说,如果找不到特定语言的数据集,就会退而使用英文的数据集。如果您有兴趣帮助贡献新的地区设置或更新现有的设置,请查看我们的创建地区设置页面获取更多信息。

    来验证一下

        [Theory]
        [InlineData(null)]
        [InlineData("zh_CN")]
        public void Locales_ConfigTest(string? locale)
        {
            //default
            var faker = locale is null ? new Faker() : new Faker(locale);
    
            faker.RuleFor(u => u.Name, f => f.Person.FullName)
                .RuleFor(u => u.Email, f => f.Person.Email)
                .RuleFor(u => u.Age, f => f.Random.Number(18, 60))
                .RuleFor(u => u.Addresses, f => f.MakeLazy(f.Random.Number(1, 3), () => f.Address.FullAddress()).ToList())
                .RuleFor(u => u.Created, f => f.Date.PastOffset());
            var staff = faker.Generate();
            var consoleType = locale is null ? "default" : locale;
            testOutputHelperAccessor.Output?.WriteLine($"{consoleType}:{JsonConvert.SerializeObject(staff)}");
        }
    

    OutPut

      default:{"Id":0,"Name":"Clyde Price","Email":"Clyde17@yahoo.com","Age":39,"Addresses":["46277 Abraham Parkways, South Spencerland, Guadeloupe","6470 Porter Island, Lesliehaven, Chad","10804 Halvorson Brook, Ninaton, Iran"],"Created":"2023-04-30T11:31:35.5106219+08:00"}
    
        zh_CN:{"Id":0,"Name":"昊焱 尹","Email":"_82@yahoo.com","Age":58,"Addresses":["孙桥5号, 珠林市, Costa Rica"],"Created":"2024-02-11T08:16:49.1807504+08:00"}
    
    

    可以看出默认是en 英文,通过设置locale可以实现国际化的输出。

    生成相同数据集

    // 如果您希望生成可重复的数据集,请设置随机数种子。
    Randomizer.Seed = new Random(8675309);
    

    这段代码用于设置随机数生成器的种子,以便生成可重复的数据集。通过指定一个固定的种子值,可以确保每次运行生成的随机数据都是相同的,从而实现数据集的重复性。

    这个比较有意思,我们来做个 demo,要求随机生成五个对象 要求下一次运行生成的还是同一批对象。
    BogusSeed 就很容易实现。

       [Fact]
       public void Bogus_Compare_SeedTest()
       {
           // Arrange
           var faker = new Faker()
               .RuleFor(u => u.Name, f => f.Person.FullName)
               .RuleFor(u => u.Email, f => f.Person.Email)
               .RuleFor(u => u.Age, f => f.Random.Number(18, 60))
               .RuleFor(u => u.Addresses, f => f.MakeLazy(f.Random.Number(1, 3), () => f.Address.FullAddress()).ToList())
               .RuleFor(u => u.Created, f => f.Date.PastOffset());
    
           // Act
           var staffs1 = Enumerable.Range(1, 5)
               .Select(_ => faker.UseSeed(_).Generate())
               .ToList();
    
           OutputStaffInformation(staffs1, "第一次");
    
           var staffs2 = Enumerable.Range(1, 5)
               .Select(_ => faker.UseSeed(_).Generate())
               .ToList();
    
           OutputStaffInformation(staffs2, "第二次");
    
           // Assert
           Assert.True(staffs1.All(staff1 => staffs2.Any(staff2 => staff1.Name == staff2.Name && staff1.Email == staff2.Email)));
       }
    
       private void OutputStaffInformation(List staffs, string iteration)
       {
           foreach (Staff staff in staffs)
           {
               testOutputHelperAccessor.Output?.WriteLine($"{iteration}: name: {staff.Name}, email: {staff.Email}");
           }
       }
    
    • Arrange 部分初始化了一个 Faker 实例,并定义了一系列规则来生成 Staff 对象。
    • Act 部分通过使用不同的种子值,生成了两组包含 5 个 Staff 对象的列表,并输出了每个 Staff 对象的姓名和邮箱信息。
    • Assert 部分使用断言验证了两组生成的 Staff 列表中是否存在具有相同姓名和邮箱的对象,即通过 All 和 Any 方法进行比较。

    通过使用不同的种子值来生成多组数据,然后断言这些数据中是否存在相同的姓名和邮箱信息。

    image

    Bogus Api 支持

    Bogus之所以提供这么方便的假数据生成,得益于封装了开箱即用的获取各类数据的方法,如:

    Address

    • ZipCode - 获取邮政编码。
    • City - 获取城市名称。
    • StreetAddress - 获取街道地址。
    • CityPrefix - 获取城市前缀。
    • CitySuffix - 获取城市后缀。
    • StreetName - 获取街道名称。
    • BuildingNumber - 获取建筑编号。
    • StreetSuffix - 获取街道后缀。
    • SecondaryAddress - 获取次要地址,如 '公寓 2' 或 '321 号套房'。
    • County - 获取县名。
    • Country - 获取国家。
    • FullAddress - 获取完整地址,包括街道、城市、国家。
    • CountryCode - 获取随机的 ISO 3166-1 国家代码。
    • State - 获取随机州名。
    • StateAbbr - 获取州名缩写。
    • Latitude - 获取纬度。
    • Longitude - 获取经度。
    • Direction - 生成基数或序数方向,例如:西北、南、西南、东。
    • CardinalDirection - 生成基数方向,例如:北、南、东、西。
    • OrdinalDirection - 生成序数方向,例如:西北、东南、西南、东北。

    Commerce

    • Department - 获取随机商务部门。
    • Price - 获取随机产品价格。
    • Categories - 获取随机产品类别。
    • ProductName - 获取随机产品名称。
    • Color - 获取随机颜色。
    • Product - 获取随机产品。
    • ProductAdjective - 随机产品形容词。
    • ProductMaterial - 随机产品材料。
    • Ean8 - 获取随机的 EAN-8 条形码号码。
    • Ean13 - 获取随机的 EAN-13 条形码号码。

    后面的可以查看官网 Api 官网地址在文末...

    Bogus 库提供了丰富的 API 支持,涵盖了各种数据类型和用例,包括地址、商务、日期、金融、图片、互联网、Lorem 文本、姓名、电话等方面的虚假数据生成方法。

    image

    EFCore 利用 Bogus 生成种子数据

    在我们的Sample.Repository中设置种子数据

    • 使用 Bogus 库生成虚假数据,填充到 Staffs 列表
    
    public class FakeData
    {
        public static List Staffs = [];
    
        public static void Init(int count)
        {
            var id = 1;
            var faker = new Faker()
                .RuleFor(_ => _.Id, f => id++)
           .RuleFor(u => u.Name, f => f.Person.FullName)
           .RuleFor(u => u.Email, f => f.Person.Email)
           .RuleFor(u => u.Age, f => f.Random.Number(18, 60))
           .RuleFor(u => u.Addresses, f => f.MakeLazy(f.Random.Number(1, 3), () => f.Address.FullAddress()).ToList())
           .RuleFor(u => u.Created, f => f.Date.PastOffset());
            var staffs = faker.Generate(count);
            FakeData.Staffs.AddRange(staffs);
        }
    }
    
    • Program 写入 1000 条种子数据
    
    using (var context = app.Services.CreateScope().ServiceProvider.GetRequiredService())
    {
        context.Database.EnsureCreated();
        FakeData.Init(1000);
        await context.Staffs.AddRangeAsync(FakeData.Staffs);
        await context.SaveChangesAsync();
    }
    

    我这地方用的是Microsoft.EntityFrameworkCore.InMemory内存数据库,正常如果使用像Sqlserver,MySQLCodeFirst模式可以在 DbContext 的OnModelCreating配置种子数据。

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);
            //FakeData.Init(1000);
            //builder.Entity().HasData(FakeData.Staffs);
        }
    

    来测试一下

    通过我们Sample.Api提供的GetAll的方法测试一下种子数据

    image

    正好一千条测试数据,大功告成。

    最后

    在软件开发中,使用 Bogus 可以极大地简化测试数据的创建过程,同时结合 EFCore 的种子数据功能,可以快速生成并初始化数据库中的虚假数据。这种方法不仅提高了开发效率,还能确保测试数据的质量和一致性。通过本文的示例和说明,希望您能更加熟悉如何利用 BogusEFCore 来生成模拟数据和种子数据,从而为软件开发过程提供更好的支持和帮助,我们有大量数据的测试需求时,也不用再为创造数据而烦恼。

    😄欢迎关注笔者公众号一起学习交流,获取更多有用的知识~
    image


    __EOF__

  • 本文作者: 董瑞鹏
  • 本文链接: https://www.cnblogs.com/ruipeng/p/18138134
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    【Vue基本功】权限路由(1)
    手写Promise(一)
    任正非说:段到段而不是端到端的变革,一定会局部优秀了,而全局灾难了。
    C8051F020 SMBus一直处于busy状态解决办法
    自然语言处理(五):子词嵌入(fastText模型)
    数据中台实战(11)-数据中台的数据安全解决方案
    把握市场潮流,溯源一流品质:在抖in新风潮 国货品牌驶过万重山
    LeetCode简单题之统计共同度过的日子数
    【校招VIP】java语言考点之反射
    【MySQL】日期格式化 yyyy-mm-dd 详解 DATE_FORMAT() 函数
  • 原文地址:https://www.cnblogs.com/ruipeng/p/18138134