Модульное тестирование с использованием Moq не проходит, объект пуст, я что-то пропустил? - PullRequest
2 голосов
/ 26 декабря 2009

Класс, который я хочу протестировать, это мой класс ArticleManager, в частности метод LoadArticle:

public class ArticleManager : IArticleManager
{
      private IArticle _article;

      public ArticleManger(IDBFactory dbFactory)
      {
            _dbFactory = dbFactory;
      }

      public void LoadArticle(string title)
      {
            _article = _dbFactory.GetArticleDAO().GetByTitle(title);

      }
}

Моя ArticleDAO выглядит так:

public class ArticleDAO : GenericNHibernateDAO<IArticle, int>, IArticleDAO
{
       public virtual Article GetByTitle(string title)
       {
           return Session.CreateCriteria(typeof(Article))
               .Add(Expression.Eq("Title", title))
               .UniqueResult<Article>();
       }
}

Мой тестовый код с использованием NUnit и Moq :

[SetUp]
public void SetUp()
{
        _mockDbFactory = new Mock<IDBFactory>();
        _mockArticleDao = new Mock<ArticleDAO>();

        _mockDbFactory.Setup(x => x.GetArticleDAO()).Returns(_mockArticleDao.Object);

        _articleManager = new ArticleManager(_mockDbFactory.Object);
}


[Test]
public void load_article_by_title()
{
     var article1 = new Mock<IArticle>();

     _mockArticleDao.Setup(x => x.GetByTitle(It.IsAny<string>())).Returns(article1.Object);

     _articleManager.LoadArticle("some title");

     Assert.IsNotNull(_articleManager.Article);
}

Модульный тест не пройден, объект _articleManager.Article возвращает NULL.

Правильно ли я все сделал?

Это один из моих первых модульных тестов, поэтому я, вероятно, упускаю что-то очевидное?

Одна проблема, с которой я столкнулся, заключалась в том, что я хотел смоделировать IArticleDao, но поскольку класс ArticleDao также наследовал от абстрактного класса, если я просто высмеял IArticleDao, то методы в GenericNHibernateDao недоступны?

Ответы [ 4 ]

5 голосов
/ 29 декабря 2009

Предисловие: я не знаком с использованием Moq (здесь пользователь Rhino Mocks), поэтому я могу пропустить несколько трюков.

Я изо всех сил пытаюсь следовать некоторому коду здесь; как отметил Марк Симанн, я не понимаю, почему это могло бы скомпилироваться в его текущем состоянии. Можете ли вы дважды проверить код, пожалуйста?

Одна вещь, которая бросается в глаза, это то, что вы вводите макет IDBFactory в менеджер статей. Затем вы делаете цепной вызов:

_article = _dbFactory.GetArticleDAO().GetByTitle(title)

Вы не предоставили реализацию GetArticleDAO. Вы только высмеяли бит LoadByTitle, который происходит после вызова GetARticleDAO. Комбинация ложных и цепных вызовов в тесте обычно является признаком того, что тест может стать болезненным.

Закон Деметры

Важный момент здесь: Уважайте Закон Деметры . ArticleManager использует IArticleDAO, возвращенный IDBFactory. Если IDBFactory не делает что-то действительно важное, вы должны внедрить IArticleDAO в ArticleManager.

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

Кроме того, почему вы храните возвращенную статью в ArticleManager как поле? Не могли бы вы просто вернуть его?

Если эти изменения будут возможны, это упростит код и упростит тестирование в 10 раз.

Ваш код станет:

public class ArticleManager : IArticleManager
{
      private IArticleDAO _articleDAO

      public ArticleManger(IArticleDAO articleDAO)
      {
            _articleDAO = articleDAO;
      }

      public IArticle LoadArticle(string title)
      {
            return _articleDAO.GetByTitle(title);
      } 
}

Тогда у вас будет более простой API, и его будет намного проще тестировать, поскольку вложение прошло.

Упрощение тестирования при использовании постоянства

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

Использование этого вида техники позволяет вашему ArticleManager использовать механизм поддельной персистентности, который ведет себя очень похоже на БД для целей тестирования. Затем вы можете легко заполнить репозиторий данными, которые помогут вам безболезненно протестировать ArticleManager.

Среды Mocking - действительно хорошие инструменты, но они не всегда подходят для установки и проверки сложных или согласованных взаимодействий; если вам нужно смоделировать / заглушить несколько вещей (особенно вложенных!) в одном тесте, это часто является признаком того, что тест переоценен или что лучше сделать сданный вручную двойной тест.

Тестирование сложное

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

1 голос
/ 29 декабря 2009

Поскольку вы в данный момент представляете код, я не вижу, чтобы он компилировался - по двум причинам.

Первый, вероятно, просто недосмотр, но класс ArticleManager не имеет свойства Article, но я предполагаю, что он просто возвращает поле _article.

Другая проблема заключается в следующей строке кода:

_mockArticleDao.Setup(x => x.GetByTitle(It.IsAny<string>())).Returns(article1.Object);

Насколько я вижу, это не должно компилироваться вообще, поскольку ArticleDAO.GetByTitle возвращает Article, но вы говорите ему возвращать экземпляр IArticle (интерфейс, а не конкретный класс).

Вы что-то упустили в своем описании кода?

В любом случае, я подозреваю, что проблема заключается в этом Setup вызове.Если вы неправильно указали настройку, она никогда не будет вызвана, и Moq по умолчанию использует свое поведение по умолчанию, которое возвращает значение по умолчанию для типа (то есть, ноль для ссылочных типов).можно изменить, установив свойство DefaultValue следующим образом:

 myMock.DefaultValue = DefaultValue.Mock;

Однако это вряд ли решит вашу проблему, поэтому вы можете решить проблемы, которые я указал выше, и я уверен, что мымогу понять что не так.

0 голосов
/ 03 января 2010

Как отметил Марк Симан, я не смог заставить его скомпилировать "как есть", поскольку ожидание .GetByTitle возвращает неправильный тип, что приводит к ошибке во время компиляции.

После исправления этого и добавления отсутствующего свойства Article тест прошел - и я думаю, что суть вашей проблемы как-то потерялась при переводе, когда вы ее написали на SO.

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

Факт, что вы получаете ноль _articleManager.Article, почти наверняка, потому что нет ожидаемого соответствия .GetByTitle. Другими словами, тот, который вы укажете, не соответствует.

Переключая свой макет в режим строгий , Moq выдаст ошибку, как только будет сделан вызов, который не соответствует ожиданиям. Что еще более важно, он даст вам полную информацию о том, что был непревзойденный вызов, в том числе значение любых аргументов. С помощью этой информации вы сможете сразу определить, почему ваши ожидания не совпадают.

Попробуйте запустить тест с установленным строгим условием "сбой" и посмотрите, дает ли он информацию, необходимую для решения проблемы.

Вот переписывание вашего теста со строгим макетом (свернутым в один метод для экономии места):

[Test]
public void load_article_by_title()
{
    var article1 = new Mock<Article>();
    var mockArticleDao = new Mock<ArticleDAO>(MockBehavior.Strict); //mock set up as strict
    var mockDbFactory = new Mock<IDBFactory>(MockBehavior.Strict); //mock set up as strict

    mockDbFactory.Setup(x => x.GetArticleDAO()).Returns(mockArticleDao.Object);
    mockArticleDao.Setup(x => x.GetByTitle(It.IsAny<string>())).Returns(article1.Object);

    var articleManager = new ArticleManager(mockDbFactory.Object);
    articleManager.LoadArticle("some title");

    Assert.IsNotNull(articleManager.Article);
}
0 голосов
/ 31 декабря 2009

Я не эксперт по Moq, но мне кажется, что проблема в том, что вы издеваетесь над ArticleDAO, где вы должны насмехаться над IAr ArticleDAO.

это связано с вашим вопросом:

Одна проблема, с которой я столкнулся, заключалась в том, что я хотел смоделировать IArticleDao, но поскольку класс ArticleDao также наследовал от абстрактного класса, если я просто высмеял IArticleDao, то методы в GenericNHibernateDao недоступны?

В объекте-макете вам не нужны методы, унаследованные от класса GenericNHibernateDao. Вам просто нужен фиктивный объект для предоставления методов, которые принимают участие в вашем тесте, а именно: GetByTitle. Вы предоставляете поведение этого метода с помощью насмешек.

Moq не будет имитировать методы, если они уже существуют в типе, который вы пытаетесь смоделировать. Как указано в API документах :

Любой тип интерфейса может использоваться для имитации, но для классов могут быть смоделированы только абстрактные и виртуальные члены.

В частности, ваша насмешка над GetByTitle будет проигнорирована, так как смоделированный тип ArticleDao предлагает (неабстрактную) реализацию этого метода.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...