Как можно избежать InternalsVisibleTo при написании юнит-тестов?

Пытаюсь покрыть следующий класс тестами:

public sealed class VdiRunHistoryDataManager : IVdiRunHistoryDataManager
{
    private const string InvalidValueErrorMessage = "Invalid value";

    private readonly IDbConnectionWrapper _connection;

    public VdiRunHistoryDataManager(IDbConnectionWrapper connection)
    {
        _connection = connection;
    }

    public DateTime GetVdiLastRunDateTime(string vdiRunTypeCode, string vdiRunScopeCode, int operatorId)
    {
        if (!IsValidVdiRunTypeCode(vdiRunTypeCode))
        {
            throw new ArgumentException(InvalidValueErrorMessage, nameof(vdiRunTypeCode));
        }

        if (!IsValidVdiRunScopeCode(vdiRunScopeCode))
        {
            throw new ArgumentException(InvalidValueErrorMessage, nameof(vdiRunScopeCode));
        }

        var vdiRunScope = GetVdiRunScopeByCode(vdiRunScopeCode);
        var vdiRunType = GetVdiRunTypeByCode(vdiRunTypeCode);
        var vdiRunHistory = FindVdiRunHistory(operatorId, vdiRunType.Id, vdiRunScope.Id);

        var longTimeAgo = DateTime.UtcNow.AddYears(-100);
        var lastRun = vdiRunHistory?.LastRun ?? longTimeAgo;
        return lastRun;
    }

    public void SaveVdiRunHistory(VdiRunHistoryDataModel historyDataModel)
    {
        if (historyDataModel == null)
        {
            throw new ArgumentNullException(nameof(historyDataModel));
        }

        if (!IsValidVdiRunTypeCode(historyDataModel.VdiRunTypeCode))
        {
            throw new ArgumentException(InvalidValueErrorMessage, nameof(historyDataModel.VdiRunTypeCode));
        }

        if (!IsValidVdiRunScopeCode(historyDataModel.VdiRunScopeCode))
        {
            throw new ArgumentException(InvalidValueErrorMessage, nameof(historyDataModel.VdiRunScopeCode));
        }

        var vdiRunType = GetVdiRunTypeByCode(historyDataModel.VdiRunTypeCode);
        var vdiRunScope = GetVdiRunScopeByCode(historyDataModel.VdiRunScopeCode);
        var historyInternalModel = FindVdiRunHistory(historyDataModel.OperatorId, vdiRunType.Id, vdiRunScope.Id) ??
                                   CreateVdiRunHistoryInternalModel(historyDataModel.OperatorId, vdiRunType.Id,
                                       vdiRunScope.Id);

        historyInternalModel.LastRun = historyDataModel.LastRun;

        _connection.Save(historyInternalModel);
    }

    private VdiRunScopeLookupInternalDataModel GetVdiRunScopeByCode(string code)
    {
        var runScope = _connection.SingleOrDefault<VdiRunScopeLookupInternalDataModel>(p => p.Code == code);
        ThrowInvalidOperationExceptionIfNull(runScope, CreateNotFoundErrorMessage("VDIRunScope", code));

        return runScope;
    }

    private VdiRunTypeLookupInternalDataModel GetVdiRunTypeByCode(string code)
    {
        var runType = _connection.SingleOrDefault<VdiRunTypeLookupInternalDataModel>(p => p.Code == code);
        ThrowInvalidOperationExceptionIfNull(runType, CreateNotFoundErrorMessage("VDIRunType", code));

        return runType;
    }

    [AssertionMethod]
    private static void ThrowInvalidOperationExceptionIfNull<T>(T instance, string exceptionMessage) where T : class
    {
        if (instance == null)
        {
            throw new InvalidOperationException(exceptionMessage);
        }
    }

    private static string CreateNotFoundErrorMessage(string tableName, string code)
    {
        var errorMessage =  $"{tableName} does not contain required record with field Code={code}";
        return errorMessage;
    }

    private static bool IsValidVdiRunScopeCode(string vdiRunScopeCode)
    {
        return vdiRunScopeCode == VdiRunScopeCodeDataModel.Full || vdiRunScopeCode == VdiRunScopeCodeDataModel.Partial;
    }

    private static bool IsValidVdiRunTypeCode(string vdiRunTypeCode)
    {
        return vdiRunTypeCode == VdiRunTypeCodeDataModel.Products;
    }

    private static VdiRunHistoryInternalDataModel CreateVdiRunHistoryInternalModel(int operatorId, int vdiRunTypeId, int vdiRunScopeId)
    {
        return new VdiRunHistoryInternalDataModel
        {
            OperatorId = operatorId,
            VdiRunTypeId = vdiRunTypeId,
            VdiRunScopeId = vdiRunScopeId
        };
    }

    private VdiRunHistoryInternalDataModel FindVdiRunHistory(int operatorId, int vdiRunTypeId, int vdiRunScopeId)
    {
        var foundVdiRunHistory = _connection.SingleOrDefault<VdiRunHistoryInternalDataModel>(p =>
            p.OperatorId == operatorId &&
            p.VdiRunTypeId == vdiRunTypeId &&
            p.VdiRunScopeId == vdiRunScopeId);

        return foundVdiRunHistory;
    }
}

Есть такой вот IDbConnectionWrapper, который инжектится в дата-менеджер:

public interface IDbConnectionWrapper
{
    void ExecuteSql(string sql);
    IReadOnlyCollection<T> Query<T>(string sql, object anonymousType);
    T SingleOrDefault<T>(Expression<Func<T, bool>> predicate);
    void Save<T>(T instance) where T : new();
}

Использую Moq библиотеку для написания тестов. Проблема заключается в том, что модели (классы) *InternalDataModels - непубличные "сырые" модели, которые не должны быть видимы вне тестируемой сборки. И именно на этих internal-моделях вызываются методы SingleOrDefault<T> и др. Это модели промежуточные, которые затем используются для получения конечного результата. При попытке замокать эти методы будет логичная ошибка, что модели недоступны для тестируемой сборки.

Вопрос: хорошо ли в данном случае использовать InternalVisibleTo атрибут сборки? Или все же есть какие-то варианты, как можно написать тесты, не пользуясь этим атрибутом? Есть ли у кого идеи, как можно порефакторить этот код так, чтобы он стал тестируемым?


Ответы (1 шт):

Автор решения: Alexander Petrov

Атрибут InternalVisibleTo именно для этого и предназначен. Поэтому пользуйтесь смело.


Изолирующие фреймворки (Isolation Frameworks) есть двух видов: ограниченные (constrained) и неограниченные (unconstrained).

Moq, наряду с NSubstitute и FakeItEasy относится к ограниченным. Они позволяют замокать только открытые типы и члены.

К неограниченным относятся TypeMock Isolator, JustMock (оба платные) и Microsoft Fakes (входит в состав Visual Studio Enterprise, отсутствует в версиях Community и даже Professional).

Последние позволяют замокать что угодно: скрытые и внутренние, статические и запечатанные типы и члены. В какой-то мере это облегчает тестирование, но развращает и в итоге можно скатиться к плохой архитектуре без чёткого разделения на области видимости и т. п.

→ Ссылка