【问题标题】:Auto Generate Identity Column in C# Net Core 3.1 Unit Test在 C# Net Core 3.1 单元测试中自动生成标识列
【发布时间】:2020-01-30 17:36:37
【问题描述】:

我的基础课

    public class Book {
        public int Id { get; set;}
        public string Title { get; set;} 
    }
    public class Author {
        public Author () {
            BookList = new HashSet<Book> ();
            for (int i = 0; i< 10; i++) {
                var book = new Book() {
                    Title = "Funny book" + i;
                }
                BookList.Add(book);
            }
        }
        public int Id { get; set;}
        public string Name { get; set;}
        public ICollection<Book> BookList { get; set; }
    }

我的存储库类

        public interface IBookRepository
    {       
        Task<Book> CreateBook(Book book);
    }
    public class BookRepository : IBookRepository
    {
        public BookRepository(BookStoreContext context)
             : base(context)
        {
        }
        public async Task<Book> CreateBook(Book book)
        {
            if (book == null)
                throw new ArgumentNullException("book cannot be null");
            }
            Context.Book.Add(book);
            await Context.SaveChangesAsync().ConfigureAwait(false);

            return book;
        }           
    }

我正在使用以下实用程序类设置我的单元测试

    public static class AuthorData
    {
        public static List<Author> GetData()
        {
            List<Author> authors = new List<Author>();
            Author author1 = new Author()
            {
                Id = 100,
                Name="author1"
            };
            authors.Add(author1);
            Author author2 = new Author()
            {
                Id = 200,
                Name="author2"
            };
            authors.Add(author2);
        }
    }
    public static class BookData
    {
        public static List<Book> GetData()
        {
            List<Book> books = new List<Book>();
            Book book1 = new Book()
            {
                Id = 1000,
                Name="Wonderful book 1"
            };
            books.Add(book1);
            Book book2 = new Book()
            {
                Id = 1001,
                Name="Wonderful book 2"
            };
            books.Add(book2);
        }
    }
      public class AuthorContextMock
      {
         public static BookStoreContext ConfigureSensorContext(IServiceCollection services)
          {
            services.AddDbContext<BookStoreContext>(c =>
         c.UseInMemoryDatabase(Guid.NewGuid().ToString())
        .UseQueryTrackingBehavior(QueryTrackingBehavior.TrackAll)
        .EnableDetailedErrors()
        .EnableSensitiveDataLogging());
         var serviceProvider = services
        .AddEntityFrameworkInMemoryDatabase()
        .BuildServiceProvider();
         var Context = serviceProvider.GetRequiredService<BookStoreContext>();
         Context.Database.EnsureCreated();

         Context.Authors.AddRange(AuthorData.GetData());
         Context.Books.AddRange(BookData.GetData());
         Context.SaveChanges();
         return Context;
        }
    }

我的单元测试

[Fact]
    public async Task CreateBookTest()
    {       
        var context = BookContextMock.ConfigureBookContext(new ServiceCollection());
        BookRepository repo = new BookRepository(context);
        Book book = new BookSensor()
        {
            Title = "New Book 1",
        };
        var newBook = await repo.CreateBook(book);
    }

在单元测试方法中,如果我没有为 book 对象指定 Id,则会抛出一个错误,指出 Id = 1000 的对象已被另一个实体跟踪。

这是我的推理:

(1) 当我创建作者测试数据时,它为每个作者创建了许多书籍。由于字段是 SQL 表的 Identity 列,因此在创建书籍时未指定 Id。

(2) 然后,我创建了指定 ID 的图书测试数据。测试数据创建成功。

(3) 在单元测试期间,系统似乎对选择下一个可用 ID 感到困惑。

有没有办法在不指定 ID 的情况下解决问题?

单元测试适用于 netcore 2.1。单元测试转成net core 3.1后出现问题。

【问题讨论】:

    标签: c# .net-core integration-testing xunit.net in-memory-database


    【解决方案1】:

    如果您将 Book 类的定义更改为:

    public class Book {
        public string Id { get; set;}
        public string Title { get; set;} 
    }
    

    您可以通过动态生成新的 guid 来添加其他书籍:

    Guid g = Guid.NewGuid();
    

    然后,当您创建新 Book 时,只需调用 g.ToString() 即可获取基础值并将其分配给 Id 属性。当然,您还必须更改后端数据库表的结构以支持此功能。

    【讨论】:

    • 在我的测试中创建书籍时,我目前正在使用实用程序函数来分配 ID。我正在尝试在不指定 ID 的情况下找到解决方案。
    • @user3097695 通过修改该函数以将 ID 指定为 Guid.NewGuid() 方法调用的结果,您无需手动指定 Id,因为系统将为每个生成一个新的 Guid调用该方法的时间。
    • 我们无法更改 Id 的定义,因为数据库已经投入生产了一段时间。实际的 API 调用没有问题,但它只影响单元测试。我需要找到这些行为背后的深层原因。
    • 好的,也许您可​​以使用辅助函数从数据库中选择 ID 的最大值,然后简单地将新 ID 加 1?
    • 我已经在做单元测试了。我认为 NetCore 3.1 中可能存在错误。我认为我不需要添加 ID。系统应该会自动添加下一个可用的 ID。
    【解决方案2】:

    想想它是如何工作的......你告诉你的系统创建一本 id 为 1000 的书。你运行你的方法,现在数据库中存在一本 id 为 1000 的书。然后您再次运行该方法,但它失败了,因为显然您的 id 为 1000 的书仍在数据库中。

    接下来,您模拟您的数据库层并创建一个 ID 为 1000 的实体,但您永远无法摆脱它。下次运行方法时,您可能会遇到实体跟踪问题。

    你称之为测试的东西实际上不是测试,因为没有断言,你没有检查任何东西,因此它不是测试。

    这一切都非常复杂,并且必然会产生拉扯头发的问题。你正在尝试做的事情告诉我你真的想要集成测试而不是单元测试。

    如果你想要单元测试,我会说把 EF 去掉,专注于真正的功能,比如业务规则等。

    进行集成测试并不是一件坏事,但如果您创建真实数据,在调用结束时,您还应该清理它。

    实际的集成测试如下所示:

    1. 在数据库中创建测试书
    2. 使用正确的 Assert 语句在预期数据和实际数据之间发出 get 并检查数据是否完全符合您的预期。

    所以:

    1. 用 1000 创建书
    2. 检查调用结果
    3. 从数据库中加载 id 为 1000 的书籍
    4. 使用断言检查你关心的事情
    5. 从数据库中删除 ID 为 1000 的图书和所有相关数据

    这是集成测试的正常流程。

    通常,当你创建数据时,你不应该告诉你的数据库使用什么 id,让它插入记录并告诉你唯一的 id 是什么。这可以解决许多问题,包括同时进行多个呼叫。不要在列上使用 max 函数来计算下一个存在并发问题的 id。

    根据您的系统,最好将 db 中的 id 字段标记为身份和自动增量,然后您的 ORM 可以看到这一点,并且您的层会改变,所以现在您在尝试创建时不要指定 id某物。

    但在某些情况下,您必须指定 ID,在这种情况下,您可以选择使用 uniqueidentifier 之类的东西,特别是 GUID。那个碰撞的几率很低。

    我会说,想想你想测试什么。是的,您可以模拟您的回购等,但如果您模拟它们,那么您在测试什么?如果您想测试您的项目是否正确创建,那么您需要一直访问数据库,因为没有模拟可以帮助您。您也可以使用内存数据库来加快处理速度,而不是实际触及真正的数据库。

    但事实仍然是,单元测试是关于功能、检查数据转换等的。模拟越少越好,它使您的测试可维护,尤其是在您重构代码时。

    另外,如果可以的话,尽量避免使用 ORM,以函数式的方式思考问题。 这是一些数据,我正在调用这个方法,我应该得到这个结果。这适用于单元测试。

    【讨论】:

    • 你有一个有效的点。我想我想问的是为什么 NetCore 2.1 和 NetCore 3.1 之间存在不同的行为。
    猜你喜欢
    • 2018-05-26
    • 1970-01-01
    • 2020-08-18
    • 2010-09-07
    • 2011-02-18
    • 2012-01-03
    • 1970-01-01
    • 2010-09-10
    • 1970-01-01
    相关资源
    最近更新 更多