在我的ViewModel中,根据登录用户的权限来启用/禁用部分功能。 ViewModel依赖于注入依赖项的ISecurity对象来检查用户是否具有特定权限。功能的不同部分需要不同的权限。
public Interface ISecurity
{
bool UserHasPermision(int userId, string permission);
}
在我的生产代码中,ISecurity的具体实现与外部应用程序交互,该应用程序不允许我更改个人的权限。我创建了一个
FakeSecurity
类,该类允许我在单元测试中执行此操作。class FakeSecurity: ISecurity
{
private Dictionary<int, List<string>> permissions = new Dictionary<int, List<string>>();
public bool UserHasPermission(int userId, string permission)
{
return permissions.ContainsKey(userId) &&
permissions[userId].Contains(permission);
}
//Not defined in ISecurity
public void SetPermission(int userId, string permission, bool hasPermission)
{
if (!permissions.ContainsKey(userId))
{
permissions[userId] = new List<string>();
}
List<string> userPermissions = permissions[userId];
if (hasPermission)
{
userPermissions.Add(permission);
}
else
{
userPermissions.Remove(permission);
}
}
}
这里的问题是在
SetPermission()
接口中未定义ISecurity
,因此,为了让我的单元测试设置个人权限,我需要将在IUnityContainer中注册的ISecurity对象转换为FakeSecurity对象。有人告诉我,单元测试应该不了解用于特定接口的特定类型的实现,并且未在接口中定义的调用方法是一种反模式。[TestMethod]
public void UserDoesNotHavePermission()
{
// test setup
IUnityContainer iocContainer = GetIocContainer();
ISecurity sec = iocContainer.Resolve<ISecurity>(); //registered singleton
(sec as FakeSecurity).SetPermission(GetCurrentUser().Id, "Save Colors", false);
var viewModel = iocContainer.Resolve<MaintainColorsViewModel>(); //per-request
// asserts
Assert.IsFalse(viewModel.CanSave);
}
[TestMethod]
public void UserHasPermission()
{
// test setup
IUnityContainer iocContainer = GetIocContainer();
ISecurity sec = iocContainer.Resolve<ISecurity>(); //registered singleton
(sec as FakeSecurity).SetPermission(GetCurrentUser().Id, "Save Colors", true);
var viewModel = iocContainer.Resolve<MaintainColorsViewModel>(); //per-request
// asserts
Assert.IsTrue(viewModel.CanSave);
}
这是不好的做法吗?我意识到我不应该将ISecurity instace转换为应用程序代码中的特定类型,但这真的是单元测试的问题吗?
最佳答案
有人告诉我,单元测试应该不了解具体的实现类型
这是不正确的。让测试直接使用伪实现和被测类是完全正常和良好的做法。
但是,您在单元测试中使用的是DI容器,这实际上是不好的做法。尽管在编写集成测试时可以使用DI容器(因为您想测试与其他组件集成的组件),但是在单元测试中使用DI库会导致难以阅读和维护测试。使用单元测试,您可以独立测试代码。这意味着您通常手动创建要测试的类,并注入所需的伪依赖项以使测试运行。
因此,我希望这种单元测试看起来像这样:
public void CanSave_CurrentUserHasNoPermission_ReturnsFalse() {
// Arrange
var noPermission = new FakeSecurity { CurrentUserHasPermission = false };
var viewModel = new MaintainColorsViewModel(noPermission);
// Act
bool actualResult = viewModel.CanSave;
// Assert
Assert.IsFalse(actualResult);
}
public void CanSave_CurrentUserHasPermission_ReturnsTrue() {
// Arrange
var hasPermission = new FakeSecurity { CurrentUserHasPermission = true };
var viewModel = new MaintainColorsViewModel(hasPermission);
// Act
bool actualResult = viewModel.CanSave;
// Assert
Assert.IsTrue(actualResult);
}
public void CanSave_Always_QueriesTheSecurityForTheSaveColorsPermission() {
// Arrange
var security = new FakeSecurity();
var viewModel = new MaintainColorsViewModel(security);
// Act
bool temp = viewModel.CanSave;
// Assert
Assert.IsTrue(security.RequestedPermissions.Contains("Save Colors"));
}
关于此代码,需要注意以下几点:
FakeSecurity
和MaintainColorsViewModel
都直接在此处的测试中创建;没有使用DI库。这使测试更具可读性和可维护性(并且更快)。我大大简化了
FakeSecurity
类(如下所示),因为您希望假类尽可能简单。添加了第三项测试以明确检查
MaintainColorsViewModel
是否请求期望的权限。AAA模式(排列/动作/声明)被明确实现。
为了使这些测试按原样编写,对
ISecurity
抽象进行了以下更改:interface ISecurity
{
bool UserHasPermission(string permission);
}
userId
参数已从UserHasPermission
方法中删除。原因是ISecurity
实现将能够找出当前用户本身是谁。允许ISecurity
的使用者传递此参数仅意味着API变得越来越复杂,编写了更多代码,编程错误的可能性更大,因此我们需要更多的支持测试。换句话说,仅此userId
属性的添加会迫使大量额外的生产和测试代码编写和维护。这是简化的
FakeSecurity
类:class FakeSecurity : ISecurity
{
public bool CurrentUserHasPermission;
public List<string> RequestedPermissions = new List<string>();
public bool UserHasPermission(string permission)
{
this.RequestedPermissions.Add(permission);
return this.CurrentUserHasPermission;
}
}
现在,
FakeSecurity
类只有很少的代码,仅通过查看它就可以非常容易地检查其正确性。请记住,测试代码应尽可能简单。旁注:用生成的模拟对象替换此类不会使我们的代码更容易。在大多数情况下,这实际上会使我们的单元测试更加难以阅读,理解和维护。开发人员开始在单元测试中使用DI容器的原因之一是因为手动创建被测类(及其所有虚假依赖项)会导致测试中的维护问题。实际上,这是真的。如果
MaintainColorsViewModel
具有多个依赖项,并且我们将在每个测试中创建该MaintainColorsViewModel
,则添加单个依赖项将导致我们更改所有MaintainColorsViewModel
测试。这通常是开发人员使用DI容器或还原为模拟框架的原因。但是,这不是开始使用DI容器或模拟库的好理由。简单的重构可以完全消除维护问题;我们只需要创建一个工厂方法,如下所示:
private static MaintainColorsViewModel CreateViewModel(params object[] dependencies) {
return new MaintainColorsViewModel(
dependencies.OfType<ISecurity>().SingleOrDefault() ?? new FakeSecurity(),
dependencies.OfType<ILogger>().SingleOrDefault() ?? new FakeLogger(),
dependencies.OfType<ITimeProvider>().SingleOrDefault() ?? new FakeTimeProvider(),
dependencies.OfType<IUserContext>().SingleOrDefault() ?? new FakeUserContext());
}
在这里,我假设
MaintainColorsViewModel
包含4个依赖项(即ISecurity
,ILogger
,ITimeProvider
和IUserContext
)。 CreateViewModel
工厂方法允许使用params
数组传入所有依赖关系,并且该方法尝试从数组中获取每个抽象,当缺少时将其替换为默认的伪实现。有了这个工厂,我们现在可以将测试重写为以下内容:
[TestMethod]
public void CanSave_CurrentUserHasNoPermission_ReturnsFalse()
{
// Arrange
var noPermission = new FakeSecurity { CurrentUserHasPermission = false };
MaintainColorsViewModel viewModel = CreateViewModel(noPermission);
// Act
bool actualResult = viewModel.CanSave;
// Assert
Assert.IsFalse(actualResult);
}
或者如果测试需要这样做,我们可以传递多个依赖项:
[TestMethod]
public void CanSave_CurrentUserHasNoPermission_LogsWarning()
{
// Arrange
var logger = new FakeLogger();
var noPermission = new FakeSecurity { CurrentUserHasPermission = false };
MaintainColorsViewModel viewModel = CreateViewModel(logger, noPermission);
// Act
bool temp = viewModel.CanSave;
// Assert
Assert.IsTrue(logger.Entries.Any());
}
请注意,此测试仅出于教育目的。我不建议视图模型实际进行日志记录。那不应该是它的责任。
这个故事的寓意实际上是,好的设计可以大大简化您的测试工作,以至于您可以编写更少的代码和更少的测试,同时提高软件的质量。
关于c# - 在单元测试中使用伪造接口(interface)中未定义的方法,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33812284/