Что-то не то с тестированием в .NET (Java и т.д.)

от автора

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

Если ваш язык программирования строго-типизированный и в нем есть интерфейсы — почти наверняка вы будете работать с абстракциями. В динамических языках разработчики предпочитают работать с реальной базой.

В .net интерфейсы есть, а значит выбор очевиден. Я взял пример из замечательной книги Марка Симана “Внедрение зависимостей в .Net”, чтобы показать некоторые проблемы, которые есть в данном подходе.

Необходимо отобразить простой список рекомендуемых товаров, если список просматривает привилегированный пользователь, то цена всех товаров должна быть снижена на 5 процентов.

Реализуем самым простым способом:

public class ProductService {         private readonly DatabaseContext _db = new DatabaseContext();              public List<Product> GetFeaturedProducts(bool isCustomerPreffered)         {             var discount = isCustomerPreffered ? 0.95m : 1;             var products = _db.Products.Where(x => x.IsFeatured);                  return products.Select(p => new Product             {                 Id = p.Id,                 Name = p.Name,                 UnitPrice = p.UnitPrice * discount             }).ToList();         } } 

Чтобы протестировать этот метод нужно убрать зависимость от базы — создадим интерфейс и репозиторий:

public interface IProductRepository {     IEnumerable<Product> GetFeaturedProducts(); }  public class ProductRepository : IProductRepository {     private readonly DatabaseContext _db = new DatabaseContext();     public IEnumerable<Product> GetFeaturedProducts()     {         return _db.Products.Where(x => x.IsFeatured);     } } 

Изменим сервис, чтобы он использовал их:

public class ProductService {     IProductRepository _productRepository;     public ProductService(IProductRepository productRepository)     {         _productRepository = productRepository;     }      public List<Product> GetFeaturedProducts(bool isCustomerPreffered)     {         var discount = isCustomerPreffered ? 0.95m : 1;         var products = _productRepository.GetFeaturedProducts();          return products.Select(p => new Product         {             Id = p.Id,             Name = p.Name,             UnitPrice = p.UnitPrice * discount         }).ToList();     } } 

Все готово для написания теста. Используем mock для создания тестового сценария и проверим, что все работает как ожидается:

[Test] public void IsPrefferedUserGetDiscount() {     var mock = new Mock<IProductRepository>();     mock.Setup(f => f.GetFeaturedProducts()).Returns(new[] {         new Product { Id = 1, Name = "Pen", IsFeatured = true, UnitPrice = 50}     });          var service = new ProductService(mock.Object);     var products = service.GetFeaturedProducts(true);          Assert.AreEqual(47.5, products.First().UnitPrice); } 

Выглядит просто замечательно, что же тут не так? На мой взгляд… практически все.

Сложность и разделение логики

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

Множество сущностей и трудоемкость

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

Dependency Injection

Позитивным побочным эффектом мы получили уменьшение связности кода и улучшили архитектуру. На самом деле скорее всего нет. Все действия были продиктованы желанием избавиться от базы, а не улучшением архитектуры и понятности кода. Так как база данных очень сильно связана с логикой, не уверен, что это приведет к улучшению архитектуры. Это настоящий карго культ — добавить интерфейсы и считать, что архитектура улучшилась.

Протестирована только половина

Это самая серьезная проблема — не протестирован репозиторий. Все тесты проходят, но приложение может работать не корректно (из-за внешних ключей, тригеров или ошибках в самих репозиториях). То есть нужно писать еще и тесты для репозиториев? Не слишком ли уже много возни, ради одного метода? К тому же репозиторий все равно придется абстрагировать от реальной базы и все что мы проверим, как хорошо, он работает с ORM библиотекой.

Mock

Выглядят здорово пока все просто, выглядят ужасно когда все сложно. Если код сложный и выглядит ужасно, его никто не будет поддерживать. Если вы не поддерживаете тесты, то у вас нет тестов.

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

Абстракции протекают

Если вы спрятали свою ORM за интерфейс, то с одной стороны, она не использует всех своих возможностей, а с другой ее возможности могут протечь и сыграть злую шутку. Это касается подгрузки связанных моделей, сохранение контекста … и т.д.

Как видите довольно много проблем с этим подходом. А что насчет второго, с реально базой? Мне кажется он намного лучше.

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

[Test] public void IsPrefferedUserGetDiscount() {     using (var db = new DatabaseContext())     {         db.Products.Add(new Product { Id = 1, Name = "Pen", IsFeatured = true, UnitPrice = 50});         db.SaveChanges();     };          var products = new ProductService().GetFeaturedProducts(true);          Assert.AreEqual(47.5, products.First().UnitPrice); } 

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

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

Для этого есть решение: начальные «фикстуры» — текстовые файлы (чаще всего в json), содержащие начальный минимальный набор данных. Большим минусом такого решения является необходимость поддерживать эти файлы вручную (изменения в структуре данных, связь начальных данных друг с другом и с кодом тестов).

При правильном подходе тестирование с реальной базой на порядок проще абстрагирования. А самое главное, что упрощается код сервисов, меньше лишнего бойлерплейт кода. В следующей статье, я расскажу как мы организовали тестовый фреймворк и применили несколько улучшений (например, к фикстурам).
ссылка на оригинал статьи https://habrahabr.ru/post/318642/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *