В реальных проектах, сколь бы малы они ни были, мы всегда имеем дело не с отдельными классами, а с цепочками и гроздьями взаимосвязанных классов. Глубина этой системы может достигать и пяти, и семи, а в особо тяжелых случаях и 10+ уровней. Тестировать такое на первый взгляд, кажется, крайне затруднительно. И именно такие ситуации зачастую закладывают первые динамитные шашки прямо в фундамент тестирующего кода...
В попытке обеспечить работу тестирующего класса начинающие программисты склонны писать собственные поддельные реализации классов, от которых зависит тестируемый код. Или, что намного хуже, вовсе не подделывают ничего и запускают всю цепочку в тестирование "как есть", раздувая отдельный тест до десятков и сотен строк. Оба подхода не ведут никуда, первый в силу нарастания хрупкости тестирующего кода, второй - по причине сложности написания и поддержания тестов, которая нарастает по экспоненте при росте количества слоев вложенности.
При этом стоит оговориться, что первый подход имеет право на существование и, живи мы в 80-е годы, стал бы "серебряной пулей". Но в наш век ленивых разработчиков... все уже придумано за нас)
Рассмотрим простую ситуацию с двумя классами.
Имеем интерфейс
public interface IFileReader
{
string Read( string filePath);
}
Класс реализующий этот интерфейс
public class FileReader
{
public string Read(string filePath)
{
return File.ReadAllText(filePath);
}
}
И зависящий от него
public class StringDecorator
{
private readonly IFileReader _reader;
public StringDecorator(IFileReader reader)
{
_reader = reader;
}
public string Get(string filePath)
{
var text = _reader.Read(filePath);
return text.ToUpper();
}
}
Наша задача - протестировать класс StringDecorator. И на выручку здесь к нам приходит библиотека Moq, из "джентельмеского набора". В базовом варианте (а иные мы пока и не рассматриваем) можем сказать, что ее предназначение - создавать поддельные реализации интерфейсов прямо на лету, с возможностью тонкой настройки поведения.
Дабы не лить лишнюю воду, рассмотрим все на примере.
[TestFixture]
public class StringDecoratorTests
{
private <MockIFileReader> _reader;
private StringDecorator _decorator;
[SetUp]
public void SetUp()
{
_reader = new Mock<IFileReader>(MockBehavior.Strict);
_decorator = new StringDecorator(_reader.Object);
}
[Theory, AutoData]
public void Get_Success(string filePath)
{
var text = "aaa";
var expected = "AAA";
_reader.Setup(e => e.Read(filePath)).Returns(text);
//
var result = _decorator.Get(filePath);
//
Assert.True(result == expected);
}
}
*Пример реализован с использованием nUnit, переписывать то же самое на xUnit не вижу никакого смысла - достаточно убрать атрибут TestFixture над классом и код можно использовать в xUnit-проекте.
О SetUp-методах мы поговорим более подробно в будущем, сейчас же достаточно пояснить, что код этого метода выполняется перед каждым запуском каждого теста в классе StringDecoratorTests.
Происходящее внутри более-менее очевидно: мы создаем подделку реализации IFileReader с помощью библиотеки Moq и используем эту подделку при создании экземпляра, тестируемого нами StringDecorator'а. Обратите внимание на MockBehavior.Strict, используйте этот подход везде и всюду - этой настройкой мы указываем движку выбрасывать исключение всякий раз, когда тестируемый код пытается обратиться к не настроенным методам зависимостей. Разберем эту мысль при изучении настройки поведения для метода Read(). text и expected в комментариях не нуждаются - мы разбирали этот момент в более ранней статье.
Вся магия заключена в
_reader.Setup(e => e.Read(filePath)).Returns(text);
Здесь мы настраиваем поведение нашего поддельного IFileReader, указывая, что метод Read получив на вход filePath должен отдать text.
Теперь когда наш _decorator будет обращаться к _reader.Read(), он будет вызывать именно нашу подделку и получать именно то, что мы указали в настройке.
Важный момент: именно для этого принципиально использовать указанную запись MockBehavior.Strict. Если мы по каким-то причинам не настроим поведение метода Read(), то движок немедленно выбросит исключение при попытке обратиться к нему и потребует настроить подделку. Если оставить конструктор пустым, то никакого исключения выброшено не будет, метод Read() всегда будет возвращать просто null, а разработчик клясть все на свете пытаясь понять почему не проходит тест)
Дальнейшее, полагаю, уже очевидно - вызываем проверяемый метод и сравниваем результат с ожиданием. Код выглядит чисто, не требует реализаций подделок вручную. При том такой подход работает независимо от глубины вложенности классов. Даже будь в FileReader вложено еще сотня классов-матрешек, это не имело бы никакого значения - мы подделываем поведение, декларируемое интерфейсом, а потому можем не принимать во внимание зависимости самого FileReader.