0

Я представляю автоматическое тестирование с использованием 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 в моем случае), но оставляет другой метод неповрежденным.

Каждая тестируемая служба будет создана после этой инициализации, поэтому я уверен, что соответствующий экземпляр вводится.

Вопрос: это нормально, чтобы издеваться над этим сервисом, или это анти-шаблон?

ответ

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 вызывают его реальный код, и я могу манипулировать зависимостями теста для реализации разных частей этого кода. Стоимость заключается в том, что у нас теперь есть еще один класс, о котором можно беспокоиться (и, возможно, еще один интерфейс, я использовал его, но вы можете придерживаться одного класса с виртуальными методами), что немного усложняет его решение.

Надеюсь, это поможет.

+0

Да, я могу внести любые изменения в код. Наличие отдельной зависимости - очень хорошая идея, так как я буду точно издеваться над тем, что порождает контекст. Все остальное в зависимости от безопасности может быть «реальным». Благодарю. – Alexei