使用EF6(Entity Framework 6)编写单元测试

4

我有一个使用.NET Framework 4.6.1和EF6的ASP.NET Core项目。

现在我想编写一些单元测试,并已经花费了几个小时来配置一个内存中的SQLite数据库以使用EF6。但是它不起作用。

所以问题是如何在没有任何mock的情况下(而是使用内存中的数据库)测试我的项目?

我的当前代码:

public class DataAccessLayer : DbContext
{
  public DataAccessLayer(string connectionString)
     : base(connectionString) {
  }

  public DataAccessLayer(DbConnection connection)
     : base(connection, true) {
  }

  public DbSet<User> Users { get; set; }

  public DbSet<Setting> Settings { get; set; }

  public DbSet<UserRole> UserRoles { get; set; }

  public DbSet<MainKey> MainKeys { get; set; }
}

[Table("Users")]
public class User
{
  [Key]
  [Required]
  public int UserID { get; set; }

  [Required][StringLength(50)]
  public string UserName { get; set; }

  ...
}

public class Testbase
{
  protected DataAccessLayer Context { get; private set; }

  [TestInitialize]
  public virtual void SetUp()
  {
     var connection = this.CreateConnection();
     connection.Open();
     this.Context = new DataAccessLayer(connection);
     this.Context.Database.CreateIfNotExists();
  }

  private SQLiteConnection CreateConnection() {
     var connectionStringBuilder = new SQLiteConnectionStringBuilder { DataSource = ":memory:" };
     return new SQLiteConnection(connectionStringBuilder.ToString());
  }
}

如果我试图添加一个用户,就会出现以下错误:

System.Data.SQLite.SQLiteException: SQL逻辑错误或缺失数据库 没有这样的表:Users。

我认为我的表是通过调用this.Context.Database.CreateIfNotExists();生成的,或者我对此有误解吗?

我知道这并不回答你的问题,但是你应该考虑一下https://github.com/tamasflamich/effort。 - Jim
就我个人而言,无论是内存数据库与否,这并不是一个单元测试;它是一个集成测试。在这方面,你应该真正使用与你实际网站运行的相同的数据库平台,比如SQL Server,而不是SQLite。 - Chris Pratt
这些不是单元测试。对于单元测试,您会希望模拟依赖项,如数据库。 - FCin
1个回答

7
考虑使用Nuget包Effort 它是一个简单快速的内存数据库,用于单元测试。
您可以使用数据库种子填充空数据库或者从测试CSV文件中填充数值。
详见Tutorials Effort - Entity Framework Unit Testing Tool Blogs和Posts的简单示例。Blogs和Posts之间是一对多的关系。
public class Blog
{
    public int Id { get; set; }
    public string Name { get; set; }

    public virtual ICollection<Post> Posts { get; set; } 
}

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public int BlogId { get; set; }
    public virtual Blog Blog { get; set; } 
}

public class BloggingContext : DbContext
{
    public BloggingContext() : base() { } // constructor using config file

    public BloggingContext(string nameOrConnectionString) : base(nameOrConnectionString) { }
    public BloggingContext(DbConnection connection) : base(connection, true) { }
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }
}

不会获得数据库的连接字符串,而是获得一个DbConnection对象。因此,在BloggingContext中有第三个构造函数。

在这个构造函数中传递给父类的布尔值是告诉DbContext它拥有这个连接:当被释放时,DbContext应该关闭和释放这个连接。

这是与正常的DbContext使用的唯一区别。对DbContext和DbSets的所有其他调用都是正常的。

示例

static void Main(string[] args)
{
    var connection = Effort.DbConnectionFactory.CreateTransient();

    using (var dbContext = new BloggingContext(connection))
    {
        var addedBlog = dbContext.Blogs.Add(new Blog[]
        {
            Name = "1",
            Posts = new Post[]
            { 
                new Post() {Title = "1st", Content = "a"},
                new Post() {Title = "2nd", Content = "b"},
                new Post() {Title = "3rd", Content = "c"},
            },
        });
        dbContext.SaveChanges();
    }

    using (var dbContext = new BloggingContext(connection))
    {
        var allPosts = dbContext.Posts.ToList();
        foreach (var post in allPosts)
        {
            Console.WriteLine($"{post.Id}: {post.Title}");
        }
    }

一个提示:在开发过程中,有时很难确定测试失败是因为错误的测试数据还是被测试的代码有问题。在调试过程中很难检查测试期间数据库中的内容。因此,我倾向于使用填充了测试值的真实数据库来开发测试,并且一旦测试的调试几乎不再需要,就切换到内存数据库。实际上,对我来说,这是在第二个或第三个DbContext构造函数之间进行切换。

哇,这几行简单的代码让我惊叹!现在它可以工作了 :) 谢谢!! - everydayXpert
这太棒了!.net 4.8 + EF6 + Effort内存数据库。太简单了! Nuget - Effort.EF6由ZZZ项目的tamasflamich提供,NMemory由ZZZ项目的tamasflamich提供。 - Valamas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接