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, но я буду ссылаться на отличный ответ Нкоси для остальных.

Другие вопросы по тегам