MVC 模拟(最小起订量)- HttpContext.Current.Server.MapPath

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

我有一个方法正在尝试使用 HttpContext.Current.Server.MapPath 以及 File.ReadAllLines[=24 进行单元测试=]如下:

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);
}

从那以后,我阅读了很多关于起订量和依赖注入的文章,但我似乎无法实施。我还在 SO 上看到了一些方便的答案,例如: 但是我无法按照我的实际示例进行操作。

我希望有人能够看一下这个并确切地告诉我我将如何为这个方法实施成功的测试。我觉得我有很多需要的背景知识,但无法将它们整合在一起。

在目前的形式中,所讨论的方法与实现问题的耦合过于紧密,在单独测试时难以复制。

对于您的示例,我建议将所有这些实施问题抽象到它自己的服务中。

public interface IProductsCsvReader {
    public string[] ReadAllLines(string virtualPath);
}

并明确地将其作为依赖项注入到有问题的 class 中

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;
    }
}

请注意 class 现在如何不关心从何处或如何获取数据。只是它在被询问时获取数据。

这可以进一步简化,但这超出了这个问题的范围。 (阅读 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() instead;不需要 HttpContext

public class ProductReader
{
    private readonly string path;

    public ProductReader(string path) {
        this.path = path;
    }

    public List<ProductItem> GetAllProductsFromCSV() { ... }
}

您可以按如下方式构建您的 class:

string productCsvPath = HostingEnvironment.MapPath(@"~/CSV/products.csv");

var reader = new ProductReader(productCsvPath);

这并没有解决与 File 的紧耦合问题,但我会参考 来解决其余问题。