MVC Mocking (Moq) - HttpContext.Current.Server.MapPath
У меня есть метод, который я пытаюсь выполнить модульный тест, который использует HttpContext.Current.Server.MapPath, а также File.ReadAllLines следующим образом:
public List<ProductItem> GetAllProductsFromCSV()
{
var productFilePath = HttpContext.Current.Server.MapPath(@"~/CSV/products.csv");
String[] csvData = File.ReadAllLines(productFilePath);
List<ProductItem> result = new List<ProductItem>();
foreach (string csvrow in csvData)
{
var fields = csvrow.Split(',');
ProductItem prod = new ProductItem()
{
ID = Convert.ToInt32(fields[0]),
Description = fields[1],
Item = fields[2][0],
Price = Convert.ToDecimal(fields[3]),
ImagePath = fields[4],
Barcode = fields[5]
};
result.Add(prod);
}
return result;
}
У меня есть настройка модульного теста, которая (как и ожидалось) не проходит:
[TestMethod()]
public void ProductCSVfileReturnsResult()
{
ProductsCSV productCSV = new ProductsCSV();
List<ProductItem> result = productCSV.GetAllProductsFromCSV();
Assert.IsNotNull(result);
}
С тех пор я много читал о Moq и Dependancy Injection, которые мне просто не удалось реализовать. Я также видел несколько удобных ответов на SO, таких как: Как избежать HttpContext.Server.MapPath для целей модульного тестирования, однако я просто не могу следовать этому для моего реального примера.
Я надеюсь, что кто-то сможет взглянуть на это и сказать мне, как именно я могу реализовать успешный тест для этого метода. Я чувствую, что мне нужно много фона, но я не могу собрать все это вместе.
2 ответа
В своей нынешней форме рассматриваемый метод слишком тесно связан с проблемами реализации, которые трудно воспроизвести при тестировании в изоляции.
Для вашего примера я бы посоветовал абстрагировать все эти проблемы реализации в свой собственный сервис.
public interface IProductsCsvReader {
public string[] ReadAllLines(string virtualPath);
}
И явно ввести это в качестве зависимости в рассматриваемом классе
public class ProductsCSV {
private readonly IProductsCsvReader reader;
public ProductsCSV(IProductsCsvReader reader) {
this.reader = reader;
}
public List<ProductItem> GetAllProductsFromCSV() {
var productFilePath = @"~/CSV/products.csv";
var csvData = reader.ReadAllLines(productFilePath);
var result = parseProducts(csvData);
return result;
}
//This method could also eventually be extracted out into its own service
private List<ProductItem> parseProducts(String[] csvData) {
List<ProductItem> result = new List<ProductItem>();
//The following parsing can be improved via a proper
//3rd party csv library but that is out of scope
//for this question.
foreach (string csvrow in csvData) {
var fields = csvrow.Split(',');
ProductItem prod = new ProductItem() {
ID = Convert.ToInt32(fields[0]),
Description = fields[1],
Item = fields[2][0],
Price = Convert.ToDecimal(fields[3]),
ImagePath = fields[4],
Barcode = fields[5]
};
result.Add(prod);
}
return result;
}
}
Обратите внимание, что теперь класс не имеет отношения к тому, где и как он получает данные. Только то, что он получает данные по запросу.
Это можно упростить еще больше, но это выходит за рамки этого вопроса. (Читайте о принципах SOLID)
Теперь у вас есть возможность смоделировать зависимость для тестирования на высоком уровне, ожидаемое поведение.
[TestMethod()]
public void ProductCSVfileReturnsResult() {
var csvData = new string[] {
"1,description1,Item,2.50,SomePath,BARCODE",
"2,description2,Item,2.50,SomePath,BARCODE",
"3,description3,Item,2.50,SomePath,BARCODE",
};
var mock = new Mock<IProductsCsvReader>();
mock.Setup(_ => _.ReadAllLines(It.IsAny<string>())).Returns(csvData);
ProductsCSV productCSV = new ProductsCSV(mock.Object);
List<ProductItem> result = productCSV.GetAllProductsFromCSV();
Assert.IsNotNull(result);
Assert.AreEqual(csvData.Length, result.Count);
}
Для полноты, вот как может выглядеть производственная версия зависимости.
public class DefaultProductsCsvReader : IProductsCsvReader {
public string[] ReadAllLines(string virtualPath) {
var productFilePath = HttpContext.Current.Server.MapPath(virtualPath);
String[] csvData = File.ReadAllLines(productFilePath);
return csvData;
}
}
Используя DI, просто убедитесь, что абстракция и реализация зарегистрированы в корне композиции.
Использование HttpContext.Current
заставляет вас предположить, что productFilePath
это данные времени выполнения, но на самом деле это не так. Это значение конфигурации, потому что оно не изменится в течение времени жизни приложения. Вместо этого вы должны ввести это значение в конструктор компонента, который нуждается в нем.
Это, очевидно, будет проблемой в случае, если вы используете HttpContext.Current
, но вместо этого вы можете вызвать HostingEnvironment.MapPath(); нет HttpContext
необходимо:
public class ProductReader
{
private readonly string path;
public ProductReader(string path) {
this.path = path;
}
public List<ProductItem> GetAllProductsFromCSV() { ... }
}
Вы можете построить свой класс следующим образом:
string productCsvPath = HostingEnvironment.MapPath(@"~/CSV/products.csv");
var reader = new ProductReader(productCsvPath);
Это не решает тесную связь с File
, но я буду ссылаться на отличный ответ Нкоси для остальных.