0

我正在为使用 Ninject 和通用存储库的项目引入使用 NUnit、NSubstitute 的自动测试。

对于回归测试,我将通用存储库替换为内存中的存储库,以防止使用数据库。

此外,为了测试服务的安全约束,我正在模拟如下所示的安全服务:

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在我的情况下),但保持其他方法不变。

每个被测试的服务都会在这个初始化之后被实例化,所以我确信注入了合适的实例。

问题:可以像这样模拟服务还是反模式?

4

1 回答 1

1

您对这种方法有特别的担忧吗?在这种情况下似乎可行。

就我个人而言,我喜欢避免部分模拟,因为这样我必须更仔细地跟踪哪些部分是真实的/将调用真实代码与哪些部分是伪造的。如果您可以灵活地在此处更改代码,则可以将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被测试的都在调用它的真实代码,并且我可以操纵测试依赖项来运行该代码的不同部分。代价是我们现在要担心另一个类(可能还有另一个接口;我使用过一个,但您可以坚持使用带有虚拟方法的单个类),这会稍微增加解决方案的复杂性。

希望这可以帮助。

于 2017-01-21T22:40:19.783 回答