简体   繁体   中英

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

I have a method I am attempting to Unit Test which makes use of HttpContext.Current.Server.MapPath as well as File.ReadAllLines as follows:

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

I have a Unit Test setup which (as expected) fails:

[TestMethod()]
public void ProductCSVfileReturnsResult()
{
    ProductsCSV productCSV = new ProductsCSV();
    List<ProductItem> result = productCSV.GetAllProductsFromCSV();
    Assert.IsNotNull(result);
}

I have since done a lot of reading on Moq and Dependancy Injection which I just dont seem to be able to implement. I have also seen a few handy answers on SO such as: How to avoid HttpContext.Server.MapPath for Unit Testing Purposes however I am just unable to follow it for my actual example.

I am hoping someone is able to take a look at this and tell me exactly how I might go about implementing a successful test for this method. I feel I have a lot of the background required but am unable to pull it all together.

In its current form, the method in question is too tightly coupled to implementation concerns that are difficult to replicate when testing in isolation.

For your example, I would advise abstracting all those implementation concerns out into its own service.

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

And explicitly inject that as a dependency into the class in question

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

Note how the class now is not concerned with where or how it gets the data. Only that it gets the data when asked.

This could be simplified even further but that is outside of the scope of this question. (Read up on SOLID principles)

Now you have the flexibility to mock the dependency for testing at a high level, expected behavior.

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

For completeness here is what a production version of the dependency could look like.

public class DefaultProductsCsvReader : IProductsCsvReader {
    public string[] ReadAllLines(string virtualPath) {
        var productFilePath = HttpContext.Current.Server.MapPath(virtualPath);
        String[] csvData = File.ReadAllLines(productFilePath);
        return csvData;
    }
}

Using DI just make sure that the abstraction and the implementation are registered with the composition root.

The use of HttpContext.Current makes you assume that the productFilePath is runtime data , but in fact it isn't. It is configuration value, because it _will not change during the lifetime of the application. You should instead inject this value into the constructor of the component that needs it.

That will obviously be a problem in case you use HttpContext.Current , but you can call HostingEnvironment.MapPath() instead ; no HttpContext is required:

public class ProductReader
{
    private readonly string path;

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

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

You can construct your class as follows:

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

var reader = new ProductReader(productCsvPath);

This doesn't solve the tight coupling with File , but I'll refer to Nkosi's excellent answer for the rest.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM