改装依赖注入和自动测试:工厂和加载/列表方法混淆
本文关键字:列表 方法 加载 依赖 注入 自动测试 工厂 | 更新日期: 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;
}
}
从一个快速的一瞥,似乎我将需要:
- 将存储库作为依赖项注入构造函数
- 将GetByUserName更改为不再是User中的静态方法
- 执行某种工厂操作,以便能够在GetByUserName方法中创建User的实例
我不知道如何以合理的方式完成这一切#1是微不足道的,但#2和#3则不那么微不足道。以下是我正在思考的如何做#2:
- 保持结构基本不变,只删除static关键字。这意味着,为了按用户名获取User,您必须创建User对象的实例,然后调用GetByUserName方法。这不能通过我高度复杂的嗅觉测试
- 将加载和保存用户的责任转移到某种UserCollection对象。这在我看来是可疑的,因为这意味着几乎每个业务对象都需要一个底层集合对象,而且这会使我的域模型更加贫血
- 直接与存储库接口。现在,控制器可能会调用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类上保留静态方法,让它做上面的一行。
免责声明:我还没有测试过这个代码。