部分嘲笑安全是一种好习惯吗?
Is partial mocking of a security a good practice?
我正在介绍使用 NUnit 的自动测试,NSubstitute 用于使用 Ninject 和通用存储库的项目。
对于回归测试,我将通用存储库替换为内存中的存储库,以防止使用数据库。
此外,为了测试服务的安全约束,我模拟了如下所示的安全服务:
public class SecurityService : ISecurityService
{
#region Properties
private IScopedDataAccess DataAccess { get; }
private IMappingService MappingService { get; }
#endregion
#region Constructor
public SecurityService(IScopedDataAccess scopedDataAccess, IMappingService mappingService)
{
DataAccess = scopedDataAccess;
MappingService = mappingService;
}
#endregion
#region Methods
public virtual string GetUsername()
{
return HttpContext.Current.User.Identity.Name;
}
public AppUserSecurityProfileServiceModel GetCurrentUserData()
{
var username = GetUsername();
var userDataModel = DataAccess.AppUserRepository.AllNoTracking.FirstOrDefault(u => u.Username == username);
if (userDataModel == null)
return null;
var ret = MappingService.Mapper.Map<AppUserSecurityProfileServiceModel>(userDataModel);
return ret;
}
public virtual int GetCurrentUserId()
{
var userData = GetCurrentUserData();
if (userData == null)
throw new SecurityException($"No user data could be fetched for - {GetUsername()}");
return userData.AppUserId;
}
public bool IsInRole(UserRoleEnum role, int? userId = null)
{
int actualUserId = userId ?? GetCurrentUserId();
var hasRole = DataAccess.AppUserXUserRoleRepository.AllNoTracking.Any(x => x.AppUserId == actualUserId && x.UserRoleId == (int) role);
return hasRole;
}
public bool CanPerformAction(UserActionEnum action, int? userId = null)
{
int actualUserId = userId ?? GetCurrentUserId();
var hasAction = DataAccess.AppUserXUserRoleRepository.AllNoTracking
.Where(x => x.AppUserId == actualUserId)
.Join(DataAccess.UserRoleRepository.AllNoTracking, xRole => xRole.UserRoleId, role => role.UserRoleId, (xRole, role) => role)
.Join(DataAccess.UserRoleXUserActionRepository.AllNoTracking, xRole => xRole.UserRoleId, xAction => xAction.UserRoleId,
(role, xAction) => xAction.UserActionId)
.Contains((int) action);
return hasAction;
}
// other methods can appear here in the future
#endregion
}
每个回归测试都像这样伪造当前用户:
public void FakeCurrentUser(int userId)
{
var userRef = DataAccess.AppUserRepository.AllNoTracking.FirstOrDefault(u => u.AppUserId == userId);
var securitySubstitude = Substitute.ForPartsOf<SecurityService>(Kernel.Get<IScopedDataAccess>(), Kernel.Get<IMappingService>());
securitySubstitude.When(x => x.GetUsername()).DoNotCallBase();
securitySubstitude.GetUsername().Returns(userRef?.Username ?? "<none>");
securitySubstitude.When(x => x.GetCurrentUserId()).DoNotCallBase();
securitySubstitude.GetCurrentUserId().Returns(userId);
Kernel.Rebind<ISecurityService>().ToConstant(securitySubstitude);
}
基本上,它会注意替换基于上下文的方法(即在我的例子中 HttpContext
),但保持其他方法不变。
每次测试的服务都会在这次初始化后实例化,所以我确信注入了合适的实例。
问题:这样模拟服务可以吗还是反模式?
您是否特别担心这种方法?在这种情况下似乎可行。
就我个人而言,我喜欢避免部分模拟,因为这样我就必须更加仔细地跟踪哪些部分是真实的/将调用真实代码,哪些部分是伪造的。如果您可以灵活地更改此处的代码,则可以将 HttpContext
相关内容推送到另一个依赖项(我认为是策略模式),然后将其伪造出来。
类似于:
public interface IUserInfo {
string GetUsername();
int GetCurrentUserId();
}
public class HttpContextUserInfo : IUserInfo {
public string GetUsername() { return HttpContext.Current.User.Identity.Name; }
public int GetCurrentUserId() { ... }
}
public class SecurityService : ISecurityService
{
private IScopedDataAccess DataAccess { get; }
private IMappingService MappingService { get; }
// New field:
private IUserInfo UserInfo { get; }
// Added ctor argument:
public SecurityService(IScopedDataAccess scopedDataAccess, IMappingService mappingService, IUserInfo userInfo)
{ ... }
public AppUserSecurityProfileServiceModel GetCurrentUserData()
{
var username = UserInfo.GetUsername();
var userDataModel = DataAccess.AppUserRepository.AllNoTracking.FirstOrDefault(u => u.Username == username);
...
return ret;
}
public bool IsInRole(UserRoleEnum role, int? userId = null)
{
int actualUserId = userId ?? UserInfo.GetCurrentUserId();
var hasRole = ...;
return hasRole;
}
public bool CanPerformAction(UserActionEnum action, int? userId = null)
{
int actualUserId = userId ?? UserInfo.GetCurrentUserId();
var hasAction = ...;
return hasAction;
}
}
现在您可以自由地为您的测试传递 IUserInfo
的替代实现(可以手动实现或使用模拟库)。这解决了我最初对部分模拟的担忧,因为我知道所有被测试的 SecurityService
都在调用其真实代码,并且我可以操纵测试依赖项来执行该代码的不同部分。代价是我们现在有另一个 class 需要担心(可能还有另一个接口;我已经使用了一个,但你可以使用虚拟方法坚持使用一个 class),这增加了解决方案的复杂性有一点。
希望这对您有所帮助。
我正在介绍使用 NUnit 的自动测试,NSubstitute 用于使用 Ninject 和通用存储库的项目。
对于回归测试,我将通用存储库替换为内存中的存储库,以防止使用数据库。
此外,为了测试服务的安全约束,我模拟了如下所示的安全服务:
public class SecurityService : ISecurityService
{
#region Properties
private IScopedDataAccess DataAccess { get; }
private IMappingService MappingService { get; }
#endregion
#region Constructor
public SecurityService(IScopedDataAccess scopedDataAccess, IMappingService mappingService)
{
DataAccess = scopedDataAccess;
MappingService = mappingService;
}
#endregion
#region Methods
public virtual string GetUsername()
{
return HttpContext.Current.User.Identity.Name;
}
public AppUserSecurityProfileServiceModel GetCurrentUserData()
{
var username = GetUsername();
var userDataModel = DataAccess.AppUserRepository.AllNoTracking.FirstOrDefault(u => u.Username == username);
if (userDataModel == null)
return null;
var ret = MappingService.Mapper.Map<AppUserSecurityProfileServiceModel>(userDataModel);
return ret;
}
public virtual int GetCurrentUserId()
{
var userData = GetCurrentUserData();
if (userData == null)
throw new SecurityException($"No user data could be fetched for - {GetUsername()}");
return userData.AppUserId;
}
public bool IsInRole(UserRoleEnum role, int? userId = null)
{
int actualUserId = userId ?? GetCurrentUserId();
var hasRole = DataAccess.AppUserXUserRoleRepository.AllNoTracking.Any(x => x.AppUserId == actualUserId && x.UserRoleId == (int) role);
return hasRole;
}
public bool CanPerformAction(UserActionEnum action, int? userId = null)
{
int actualUserId = userId ?? GetCurrentUserId();
var hasAction = DataAccess.AppUserXUserRoleRepository.AllNoTracking
.Where(x => x.AppUserId == actualUserId)
.Join(DataAccess.UserRoleRepository.AllNoTracking, xRole => xRole.UserRoleId, role => role.UserRoleId, (xRole, role) => role)
.Join(DataAccess.UserRoleXUserActionRepository.AllNoTracking, xRole => xRole.UserRoleId, xAction => xAction.UserRoleId,
(role, xAction) => xAction.UserActionId)
.Contains((int) action);
return hasAction;
}
// other methods can appear here in the future
#endregion
}
每个回归测试都像这样伪造当前用户:
public void FakeCurrentUser(int userId)
{
var userRef = DataAccess.AppUserRepository.AllNoTracking.FirstOrDefault(u => u.AppUserId == userId);
var securitySubstitude = Substitute.ForPartsOf<SecurityService>(Kernel.Get<IScopedDataAccess>(), Kernel.Get<IMappingService>());
securitySubstitude.When(x => x.GetUsername()).DoNotCallBase();
securitySubstitude.GetUsername().Returns(userRef?.Username ?? "<none>");
securitySubstitude.When(x => x.GetCurrentUserId()).DoNotCallBase();
securitySubstitude.GetCurrentUserId().Returns(userId);
Kernel.Rebind<ISecurityService>().ToConstant(securitySubstitude);
}
基本上,它会注意替换基于上下文的方法(即在我的例子中 HttpContext
),但保持其他方法不变。
每次测试的服务都会在这次初始化后实例化,所以我确信注入了合适的实例。
问题:这样模拟服务可以吗还是反模式?
您是否特别担心这种方法?在这种情况下似乎可行。
就我个人而言,我喜欢避免部分模拟,因为这样我就必须更加仔细地跟踪哪些部分是真实的/将调用真实代码,哪些部分是伪造的。如果您可以灵活地更改此处的代码,则可以将 HttpContext
相关内容推送到另一个依赖项(我认为是策略模式),然后将其伪造出来。
类似于:
public interface IUserInfo {
string GetUsername();
int GetCurrentUserId();
}
public class HttpContextUserInfo : IUserInfo {
public string GetUsername() { return HttpContext.Current.User.Identity.Name; }
public int GetCurrentUserId() { ... }
}
public class SecurityService : ISecurityService
{
private IScopedDataAccess DataAccess { get; }
private IMappingService MappingService { get; }
// New field:
private IUserInfo UserInfo { get; }
// Added ctor argument:
public SecurityService(IScopedDataAccess scopedDataAccess, IMappingService mappingService, IUserInfo userInfo)
{ ... }
public AppUserSecurityProfileServiceModel GetCurrentUserData()
{
var username = UserInfo.GetUsername();
var userDataModel = DataAccess.AppUserRepository.AllNoTracking.FirstOrDefault(u => u.Username == username);
...
return ret;
}
public bool IsInRole(UserRoleEnum role, int? userId = null)
{
int actualUserId = userId ?? UserInfo.GetCurrentUserId();
var hasRole = ...;
return hasRole;
}
public bool CanPerformAction(UserActionEnum action, int? userId = null)
{
int actualUserId = userId ?? UserInfo.GetCurrentUserId();
var hasAction = ...;
return hasAction;
}
}
现在您可以自由地为您的测试传递 IUserInfo
的替代实现(可以手动实现或使用模拟库)。这解决了我最初对部分模拟的担忧,因为我知道所有被测试的 SecurityService
都在调用其真实代码,并且我可以操纵测试依赖项来执行该代码的不同部分。代价是我们现在有另一个 class 需要担心(可能还有另一个接口;我已经使用了一个,但你可以使用虚拟方法坚持使用一个 class),这增加了解决方案的复杂性有一点。
希望这对您有所帮助。