MVC Mocking(Moq) - HttpContext.Current.Server.MapPath

时间:2017-08-06 11:56:13

标签: c# unit-testing dependency-injection moq httpcontext

我有一个方法,我正在尝试单元测试,它使用 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上看到了一些方便的答案,例如:How to avoid HttpContext.Server.MapPath for Unit Testing Purposes但是我无法按照我的实际例子进行操作。

我希望有人能够看一看这个并告诉我如何实现这种方法的成功测试。我觉得我需要很多背景但是我无法将它们全部拉到一起。

2 个答案:

答案 0 :(得分: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只需确保抽象和实现是使用组合根注册的。

答案 1 :(得分:1)

使用HttpContext.Current会让您假设productFilePath运行时数据,但事实上它并非如此。它是配置值,因为它在应用程序的生命周期内不会改变。您应该将此值注入需要它的组件的构造函数中。

如果您使用HttpContext.Current,这显然会出现问题,但您可以致电HostingEnvironment.MapPath() instead;不需要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的紧密耦合,但我会在其余时间引用Nkosi's excellent answer