【问题标题】:Unit testing database using EF使用 EF 的单元测试数据库
【发布时间】:2019-01-11 20:17:05
【问题描述】:

如何对连接到数据库的服务进行单元测试?

我在数据访问层有一个playerRepository 类,它直接与数据库交互,在业务层有一个playerService 类,它创建一个playerRepository 的实例并为随机的东西提供服务,例如 - 删除播放器,保存播放器,获取所有玩家,通过 id/name 获取玩家 yadda yadda。

我想在不使用真实数据库的情况下对playerService 进行单元测试,而是使用 EF 提供的内存数据库。

问题是,我不知道如何设置它。

我有一个 PlayerContext : DbContext 类,用作代码优先的模型(通过 EF 教程完成这部分)。而且我必须在构造函数DbContextOptionsBuilder<PlayerContext> 中添加一个参数,但我不知道从哪里开始。我不知道如何以及在哪里设置连接字符串,“默认”数据库本身存储在哪里。

我想通过使用没有 NSubstitute 或 Moq 的 EF 来做到这一点,我这样做是为了了解它是如何在不使用除 EF 之外的其他框架的情况下完成的。

public class PlayerContext : DbContext
{
    public PlayerContext() : base()
    {

    }

    public DbSet<Player> Players { get; set; }
}

public class Player
{
    public string Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int DciNumber { get; set; }
}

我正在使用 Visual Studio 提供的单元测试

[TestClass]
public class PlayerServiceTest
{
    // Should get all the players from database
    [TestMethod]
    public void GetAllPlayers()
    {
        // arrange - act
        // TODO: Return all the players from the database

        // assert
        // TODO: Should return empty list
    }

PlayerService 类看起来像这样

public class PlayerService
{
    private PlayerRepository _playerRepository = new PlayerRepository();


    public List<Player> GetAllPlayers()
    {
        var players = _playerRepository.GetAllPlayers();

        return players;
    }

播放器存储库

public class PlayerRepository
{
    public List<Player> GetAllPlayers()
    {
        using (var context = new PlayerContext())
        {
            var players = context.Players.ToList();

            return players;
        }
    }

一般我的问题是:

  1. 如何使PlayerContext 与另一个连接字符串在单元测试的情况下连接到内存数据库,以及如何在不通过单元测试运行时为其提供正确的连接字符串

  2. 如何更改数据库的位置,因为它使用C:\Users 中的默认路径。

我不是在寻找与 DAL 的 PlayerRepository 的集成测试,我只是想测试业务层,我所需要的只是,当我运行 PlayerService 使用连接到内存的 PlayerRepository 的测试时数据库,就是这样。否则它应该连接到存储在与应用程序的主 exe 相同的文件夹中的本地数据库

需要帮助!

【问题讨论】:

  • 我建议您检查github.com/v-zubritsky/Reseed,一旦确定内存不足以进行您的测试。内存数据库显然具有与真实数据库不同的行为,因此通常不能将其用作替代品,即使用它会牺牲测试质量。这并不意味着您不需要通过使用依赖注入等方法将逻辑与数据访问层解耦来改进设计。

标签: c# database winforms entity-framework unit-testing


【解决方案1】:

缺少的部分是依赖注入/IoC。这里的原则是使用可以模拟出来的合约接口来定义你的依赖项(Repository)。将该依赖注入到依赖它的类中。通过这种方式,您可以用返回已知状态、抛出预期异常等的模拟对象替换数据库、文件处理等具体实现,以测试您的业务逻辑对这些场景的处理。

public class PlayerService
{
    private readonly IPlayerRepository _playerRepository = null;

    public PlayerService(IPlayerRepository playerRepository)
    {
         _playerRepository = playerRepository ?? throw new ArgumentNullException("playerRepository");
    }


    public List<Player> GetAllPlayers()
    {
        var players = _playerRepository.GetAllPlayers();

        return players;
    }
}

查看 Autofac、Unity、Ninject 等 IoC 容器,了解如何在构建容器时自动识别并注入具体类实例到您的服务中。

当您编写单元测试时,您会创建 IPlayerRepository 类的模拟(请参阅:例如 Moq)并将其传递给您的测试服务。即:

[Test]
public void TestService()
{
   var mockRepository = new Mock<IPlayerRepository>();
   mockRepository.Setup(x => x.GetPlayers()).Returns(buildTestPlayerList());
   var serviceUnderTest = new PlayerService(mockRepository.Object);
   // continue with test...
}

在最坏的情况下:如果您想放弃容器,这也可以:

public class PlayerService
{
    private IPlayerRepository _playerRepository = null;
    public IPlayerRepository PlayerRepository
    {
        get { return _playerRepository ?? (_playerRepository = new PlayerRepository()); }
        set { _playerRepository = value; }
    }

    // ...
}

...然后是测试...

[Test]
public void TestService()
{
   var mockRepository = new Mock<IPlayerRepository>();
   mockRepository.Setup(x => x.GetPlayers()).Returns(buildTestPlayerList());
   var serviceUnderTest = new PlayerService { PlayerRepository = mockRepository.Object };
   // continue with test...
}

这是一种我称之为“惰性属性注入”的模式,您可以在其中选择发送依赖项,但默认情况下它只会创建默认依赖项。当将依赖替换和单元测试引入严重依赖于更新类中间代码的遗留代码时,这可能是一种有用的模式。从某种意义上说,它是懒惰的,依赖项仅在第一次访问时才被“更新”。如果您在服务中调用不需要依赖项的方法,则不会对每个依赖项进行初始化,只有使用的那些。我强烈建议阅读 IoC 容器,因为它们有助于自动连接依赖项。

【讨论】:

  • 依赖注入还是依赖倒置?我会调用你在这里的大部分内容(我大部分时间都使用相同的模式)依赖注入,因为调用代码没有授予依赖任何控制权,它只是通过注入来替换依赖。
  • 非常感谢,我下载了 Castle.Windsor 用于依赖注入,最终弄明白了 :)
  • 非常好,这是一个很好的模式来启用单元测试,以及管理可替代的依赖项。您还可以快速浏览一下遵循更现代惯例的 Autofac。 (泛型和流畅)Windsor 是最早的容器之一,我不确定它是如何“跟上”现代语法的。 Autofac 还支持使用延迟依赖项,这带来了我上面概述的延迟注入场景的好处。 (依赖项只有在实际使用时才会启动。)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-10-15
  • 1970-01-01
  • 2014-11-19
  • 1970-01-01
  • 2015-04-10
相关资源
最近更新 更多