我有一个方法,我正在尝试单元测试,它使用 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但是我无法按照我的实际例子进行操作。
我希望有人能够看一看这个并告诉我如何实现这种方法的成功测试。我觉得我需要很多背景但是我无法将它们全部拉到一起。
答案 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。