Модульное тестирование и nhibernate? - PullRequest
9 голосов
/ 02 февраля 2011

Мне интересно, как обойти это.Я использую nhibernate и свободно.

У меня есть класс домена, подобный этому

public class User
{
   public virtual int UserId {get; private set;}
}

Это похоже на соглашение при выполнении nhibernate, так как это мешает людям устанавливать и идентифицировать, поскольку это автоматическиГенерируемый.

Теперь проблема возникает, когда я тестирую модули.

У меня есть весь мой код nhibernate в репозитории, который я копирую, поэтому я тестирую только свой сервисный уровень.Проблема возникает, когда это происходит.

User user = repo.GetUser(email);

это должно вернуть пользовательский объект.

Поэтому я хочу использовать moq, чтобы сделать это

repo.Setup(x => x.GetUser(It.IsAny<string>())).Return(/* UserObject here */)

теперь здесьпроблема

Мне нужно создать объект User и поместить его в часть Return.

Так что я бы сделал что-то вроде

User user =  new User()
{
   UserId = 10,
}

Но здесь проблемаЛожь, мне нужно установить Id, потому что я на самом деле использую его позже, чтобы сделать linq для некоторых коллекций (на уровне сервисов, так как он не попадает в мою базу данных, поэтому его не должно быть в моем репо), поэтому мне нужно его установить, ноЯ не могу установить его, потому что это частный набор.

Так что мне делать?Должен ли я просто удалить приват или есть какой-то другой способ?

Ответы [ 4 ]

15 голосов
/ 02 февраля 2011

Вы можете иметь поддельный Repository объект, возвращающий поддельный User объект:

var stubUser = new Mock<User>();
stubUser.Setup(s => s.UserId).Returns(10);

var stubRepo = new Mock<IUserRepository>();
stubRepo.Setup(s => s.GetUser(It.IsAny<string>())).Return(stubUser);

Здесь есть пара вещей, на которые стоит обратить внимание:

  1. Moq может подделывать элементы конкретных классов, только если они помечены как виртуальные . Это может быть неприменимо в некоторых сценариях, и в этом случае единственный способ подделать объект через Moq - это реализовать интерфейс.
    В этом случае, однако, решение работает хорошо, потому что NHibernate уже навязывает то же самое требование к свойствам класса User для выполнения отложенной загрузки.
  2. Наличие поддельных объектов, возвращающих другие подделки, может иногда приводить к чрезмерным указанным юнит-тестам. В этих ситуациях построение богатых объектных моделей, состоящих из заглушек и макетов, возрастает до такой степени, что становится трудно определить что именно тестируется, делает сам тест нечитаемым и трудным в обслуживании. Это очень хорошая практика модульного тестирования, если быть ясным, но она должна использоваться осознанно.

Связанные ресурсы:

2 голосов
/ 14 декабря 2011

Другая альтернатива, если вы предпочитаете не насмехаться над классами сущностей, - это установить закрытый / защищенный идентификатор с помощью отражения.

Да, я знаю, что на это обычно не обращают особого внимания, и его часто называют признаком плохого дизайна.Но в этом случае наличие защищенного идентификатора на ваших объектах NHibernate является стандартной парадигмой, поэтому это кажется вполне разумным решением.

Мы можем попытаться реализовать его как минимум.В моем случае 95% моих сущностей все используют один Guid в качестве уникального идентификатора, и лишь немногие используют целое число.Поэтому наши классы сущностей обычно реализуют очень простой HasID интерфейс:

public interface IHasID<T>
{
    T ID { get; }
}

В реальном классе сущностей мы можем реализовать его следующим образом:

public class User : IHasID<Guid>
{
    Guid ID { get; protected set; }
}

Этот идентификатор сопоставлен сNHibernate в качестве первичного ключа обычным способом.

Для установки этого в наших модульных тестах мы можем использовать этот интерфейс для предоставления удобного метода расширения:

public static T WithID<T, K>(this T o, K id) where T : class, IHasID<K>
{
    if (o == null) return o;
    o.GetType().InvokeMember("ID", BindingFlags.SetProperty | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, o, new object[] { id });
    return o;
}

Мы неДля этого необходим интерфейс HasID, но это означает, что мы можем пропустить немного лишнего кода - например, нам не нужно проверять, поддерживается ли идентификатор на самом деле.

Метод расширениятакже возвращает исходный объект, поэтому при использовании я обычно просто цепляю его в конце конструктора:

var testUser = new User("Test User").WithID(new Guid("DC1BA89C-9DB2-48ac-8CE2-E61360970DF7"));

Или на самом деле, поскольку для Guids мне все равно, какой идентификатор на самом деле, у меня есть другойметод расширения:

public static T WithNewGuid<T>(this T o) where T : class, IHasID<Guid>
{
    if (o == null) return o;
    o.GetType().InvokeMember("ID", BindingFlags.SetProperty | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, o, new object[] { Guid.NewGuid() });
    return o;
}

и в использовании:

var testUser = new User("Test User").WithNewGuid();
2 голосов
/ 02 февраля 2011

Ответ Энрико готов для модульного тестирования.Я предлагаю другое решение, потому что эта проблема возникает и в других обстоятельствах, когда вы, возможно, не захотите использовать Moq.Я регулярно использую эту технику в производственном коде, где общий шаблон использования предназначен для члена класса только для чтения, но некоторые другие классы должны изменить его.Одним примером может быть поле состояния, которое обычно доступно только для чтения и должно устанавливаться только конечным автоматом или классом бизнес-логики.

В основном вы предоставляете доступ к закрытому члену через статический вложенный класс, который содержитметод для установки свойства.Пример стоит тысячи слов:

public class User {
    public int Id { get; private set; }

    public static class Reveal {
        public static void SetId(User user, int id) {
            user.Id = id;
        }
    }
}

Вы используете его так:

User user = new User();
User.Reveal.SetId(user, 43);

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

  • нет запроса Intellisense для установщика свойства или метода SetId ()
  • программисты должны явно использовать странный синтаксис для установки свойства с помощью *Класса 1014 *, тем самым подсказывая им, что они, вероятно, не должны делать этого
  • Вы можете легко выполнить статический анализ использования класса Reveal, чтобы увидеть, какой код обходит стандартные шаблоны доступа

Если вы хотите изменить частное свойство только для целей модульного тестирования и можете использовать объект Moq, тогда я все равно рекомендую предложение Энрико;но вы можете время от времени находить эту технику полезной.

0 голосов
/ 02 февраля 2011

Вместо того, чтобы пытаться смоделировать ваш репозиторий, я бы посоветовал вам использовать для тестирования базу данных SQLite в памяти.Это даст вам скорость, которую вы ищете, и это сделает вещи намного проще.Если вы хотите посмотреть рабочий образец, вы можете взглянуть на один из моих проектов GitHub: https://github.com/dlidstrom/GridBook.

...