改装依赖注入和自动测试:工厂和加载/列表方法混淆

本文关键字:列表 方法 加载 依赖 注入 自动测试 工厂 | 更新日期: 2023-09-27 17:59:02

我正在MVC项目的新阶段工作。第一阶段相当小,也不是很复杂。下一阶段将是非常复杂和更大的。在第一阶段,由于各种原因,我们讨论并放弃了进行彻底自动化测试的想法。

在新阶段,我希望我们的团队投资于自动化测试。我认为如果没有依赖项注入,我们就无法做任何有用的事情,因为我们希望截断数据库工作,使我们的测试可控。

我所面临的困难是将依赖项注入改造到我的业务层中。我们现有的业务对象具有状态、行为,并负责加载它们自己的实例(通过静态方法)。例如,我们可能有一个过于简化的例子:

public class User
{
    public User(string userName)
    {}
    public bool Authenticate()
    {}
    public static User GetByUserName(string userName)
    {
      //Do some DB querying, and then map the data object to a new instance:
      UserRepository repository = new UserRepository();
      var databaseObject = repository.list(u => u.userName = userName);
      User user = new User();
      user.userName = databaseObject.userName;
      // populate other fields
      return user;
    }
 }

从一个快速的一瞥,似乎我将需要:

  1. 将存储库作为依赖项注入构造函数
  2. 将GetByUserName更改为不再是User中的静态方法
  3. 执行某种工厂操作,以便能够在GetByUserName方法中创建User的实例

我不知道如何以合理的方式完成这一切#1是微不足道的,但#2和#3则不那么微不足道。以下是我正在思考的如何做#2:

  1. 保持结构基本不变,只删除static关键字。这意味着,为了按用户名获取User,您必须创建User对象的实例,然后调用GetByUserName方法。这不能通过我高度复杂的嗅觉测试
  2. 将加载和保存用户的责任转移到某种UserCollection对象。这在我看来是可疑的,因为这意味着几乎每个业务对象都需要一个底层集合对象,而且这会使我的域模型更加贫血
  3. 直接与存储库接口。现在,控制器可能会调用User.GetByUserName()来获取一个User域对象以完成一些工作。相反,控制器会意识到存储库,并调用repository.list()。这对我来说似乎很漏洞百出——控制器突然觉得自己在做生意

然后是#3。我的选择似乎是要么使用我的IOC容器注入工厂方法(例如autofac工厂委托),要么为每个对象手动创建工厂。使用autofac方法似乎很漏洞百出,因为这样我要么在单元测试中使用autopac,要么为每个对象截取一个工厂委托。为所有对象创建工厂似乎很麻烦,因为在某种程度上,我给开发人员带来了巨大的工作量,只是为了能够在不知道我的IOC容器的情况下使用DI。

我已经尽我所能搜索这些主题,但我真的没有看到很多不涉及上述缺点的具体例子。

在不损失"好的设计"或"工作量太大"的情况下,解决#2和#3的一些技巧是什么?

改装依赖注入和自动测试:工厂和加载/列表方法混淆

User类不应该知道存储库IMHO。如果在存储库中连线,则很难对User类进行单元测试,并且如果您决定将存储库放在不同的程序集中,则会创建一个循环引用。

典型的设计是使用IUserRepository接口来定义CRUD方法(例如GetUserByUserName)和一个或多个指向实际数据源的UserRepository类。

当您有任何东西需要使用UserRepository(如控制器)时,将其作为IUserRepository注入。这样,您就可以使用mock/stub存储库对控制器进行单元测试。

您的存储库将了解您的业务类,并且可能(松散地)与其他存储库耦合(例如,Role存储库可能注入IUserRepository以让用户扮演角色。

如果你想为所有类从一个"主"存储库开始,这很好,但你会冒着以后必须拆分存储库的风险(这可能不是什么大不了的事)。

因此,我推荐选项#2和子选项#2(称之为UserRepository而非UserCollection)。

如何实现这一点主要取决于从UserRepository检索数据库对象并从中填充User实例的逻辑。有些人喜欢拥有可以检索自己数据的"智能"模型,但另一些人则不喜欢。

听起来你想要一个有点聪明的User类。我认为您可以通过将UserRepository注入User类的构造函数来实现这一点,而不是创建另一个User实例,只需使用存储库来填充这个User实例。

下面是一个示例实现。我的期望是,你正在使用一个IoC容器,并且你将在其中注册IUserRepository接口

public class User
{
    public User(IUserRepository repository, string userName)
    {
      var databaseObject = repository.list(u => u.userName = userName);
      this.userName = databaseObject.userName;
      // populate other fields
    }
    public bool Authenticate()
    {}
 }

现在,您应该能够通过IoC容器实例化User对象。例如,如果您使用的是Ninject,那么我认为语法如下:

var user = kernel.Get<User>(new ConstructorArgument("userName", "<actual username value>"));

对于Unity,我相信它会是这样的:

container.Resolve<User>(new ParameterOverride("userName", "<actual username value>"))

根据您的IoC容器,您可能还需要注册User类,即使它没有实现接口,而且您也没有真正尝试将其与任何东西交换。可能有必要将已注册的IUserRepository与User类的构造函数参数连接起来。

如果你真的想保持实例化代码的干净,那么你可以在User类上保留静态方法,让它做上面的一行。

免责声明:我还没有测试过这个代码。