为Epplus Excel文件创建Moq

3

这是我的第一个问题。我已经查看了我的查询,但找不到有用的答案。

我的任务是为我的Excel文件编写单元测试用例。我面临的问题是,我们使用Epplus来处理Excel文件,我不确定如何为此编写单元测试用例。我查阅了资料,发现我们也可以使用MOQ进行模拟。但是我找不到关于使用Epplus的Excel文件进行模拟的有用链接。我找到了这个链接Unit testing classes that use EPPlus,但我不确定如何实现它。

如果有人能提供一个简单的Excel文件单元测试样例,我将不胜感激。测试可以检查上传的文件是否是Excel文件,检查Excel文件是否为空等。

很抱歉,目前我没有任何样例可分享。我可以分享的是我读取Excel文件的代码:

public class MyController : Controller
{
  [HttpPost("upload")]
  public async Task<IActionResult> UploadFile(IFormFile file)
  {
   JArray data = new JArray();
    using (ExcelPackage package = new ExcelPackage(file.OpenReadStream()))
    {
      ExcelWorksheet worksheet = package.Workbook.Worksheets[1];
      //Check if excel is empty.
      if (worksheet.Dimension == null)
      {
         return BadRequest("File is blank.");
      }
     data = Helper.CreateJson(worksheet);
                }
     return Ok(data);
  }
}

我创建了一个辅助类:

public static JArray CreateJson(ExcelWorksheet worksheet)
{
  JArray data = new JArray();
  JObject jobject = new JObject();

  int rowCount = worksheet.Dimension.End.Row;
  int colCount = worksheet.Dimension.End.Column;

    for (int row = 1; row <= rowCount; row++)
    {
       for (int col = 1; col <= colCount; col++)
        {
          var value = worksheet.Cells[row, col].Value;
          //Excel has 2 columns and I want to create a json from that.
          if (col == 1)              
          {
             jObject.Add("ID", rowValue.ToString());
          }
          else
          {
             jObject.Add("Name", rowValue.ToString());
          }
        }
         data.Add(jObject);
         jObject= new JObject();
     }
   return data;
}

这是我目前拥有的测试类。
public class TestClass
{
    private MyController _controller;
    public TestClass()
    {
      _controller = new MyController (); 
    }

      [Fact]
    public void Upload_WhenCalled()
    {
        //var file = new FileInfo(@"C:\myfile.xlsx");
        //...what next?

        var file = new Mock<IFormFile>();
        var content = File.OpenRead(@"C:\myfile.xlsx");

        var result = _controller.UploadFile(file.Object);
        //When I debug it throws error "Object reference not set to an instance of an object."
    }
}

在测试中模拟 IFormFile,以返回文件流并将其传递给要测试的操作。确保满足所有其他必要的依赖项。最后,根据实际行为断言预期行为。 - Nkosi
@Nkosi 我已经更新了我的代码,使用模拟 iform 文件。但是它抛出错误“对象引用未设置为对象的实例。” 错误出现在我的控制器上,具体是这一行:ExcelPackage package = new ExcelPackage(myFile.OpenReadStream())。 - Brenda
设置模拟以在调用时返回流。 file.Setup(_ => _.OpenReadStream()).Returns(content); - Nkosi
现在,虽然我的帮助可以帮你解决当前的问题,但你真的应该采纳@mason建议的抽象化ExcelPackage与控制器之间紧密耦合的方法,将其转化为自己的关注点。这样会使测试控制器变得更容易。你总是可以根据需要单独测试包装器。 - Nkosi
2个回答

3
在这种情况下,模拟IFormFile以在您的测试中返回文件流,并将其传递给正在测试的操作。确保满足所有其他必要的依赖项。
public class TestClass {
    private MyController _controller;
    public TestClass() {
      _controller = new MyController (); 
    }

    [Fact]
    public void Upload_WhenCalled() {
        //Arrange
        var content = File.OpenRead(@"C:\myfile.xlsx");
        var file = new Mock<IFormFile>();
        file.Setup(_ => _.OpenReadStream()).Returns(content);

        //Act
        var result = _controller.UploadFile(file.Object);

        //Assert
        //...
    }
}

现在虽然这应该可以帮助你解决当前的问题,但你真的应该采纳其他答案建议的把ExcelPackage的紧耦合从控制器中抽象出来。这样会使控制器的单元测试更加容易隔离。
如果需要,您始终可以单独对包装器进行集成测试。
以下是目前从控制器中抽象出来的接口的简化示例。
public interface IExcelService {
    Task<JArray> GetDataAsync(Stream stream);
}

这将具有与控制器中代码镜像的实现。

public class ExcelService: IExcelService {
    public async Task<JArray> GetDataAsync(Stream stream) {
        JArray data = new JArray();
        using (ExcelPackage package = new ExcelPackage(stream)) {
            ExcelWorksheet worksheet = package.Workbook.Worksheets[1];
            if (worksheet.Dimension != null) {
                data = await Task.Run(() => createJson(worksheet));
            }
        }
        return data;
    }

    private JArray createJson(ExcelWorksheet worksheet) {
        JArray data = new JArray();
        int colCount = worksheet.Dimension.End.Column;  //get Column Count
        int rowCount = worksheet.Dimension.End.Row;     //get row count
        for (int row = 1; row <= rowCount; row++) {
            JObject jobject = new JObject();
            for (int col = 1; col <= colCount; col++) {
                var value = worksheet.Cells[row, col].Value;
                //Excel has 2 columns and I want to create a json from that.
                if (col == 1) {
                    jObject.Add("ID", rowValue.ToString());
                } else {
                    jObject.Add("Name", rowValue.ToString());
                }
                data.Add(jObject);
            }
        }
        return data;
    }
}

现在可以将控制器简化为遵循显式依赖原则

public class MyController : Controller {
    private readonly IExcelService excel;
    public MyController(IExcelService excel) {
        this.excel = excel;
    }

    [HttpPost("upload")]
    public async Task<IActionResult> UploadFile(IFormFile file) {
        JArray data = await excel.GetDataAsync(myFile.OpenReadStream());
        if(data.Count == 0)
            return BadRequest("File is blank.");
        return Ok(data);
    }
}

您需要确保将接口和实现与依赖倒置框架在 Startup 中注册。

services.AddScoped<IExcelService, ExcelService>();

现在控制器只关心在运行时被调用时应该做什么。它没有理由处理实现方面的问题。

public class MyControllerTests {
    [Fact]
    public async Task Upload_WhenCalled() {
        //Arrange
        var content = new MemoryStream();
        var file = new Mock<IFormFile>();
        file.Setup(_ => _.OpenReadStream()).Returns(content);
        var expected = new JArray();
        var service = new Mock<IExcelService>();
        service
            .Setup(_ => _.GetDataAsync(It.IsAny<Stream>()))
            .ReturnsAsync(expected);

        var controller = new MyController(service.Object);

        //Act
        var result = await controller.UploadFile(file.Object);

        //Assert
        service.Verify(_ => _.GetDataAsync(content));
        //...other assertions like if result is OkContentResult...etc
    }
}

为了进行涉及实际文件的集成测试,您可以测试该服务。

public class ExcelServiceTests {
    [Fact]
    public async Task GetData_WhenCalled() {
        //Arrange
        var stream = File.OpenRead(@"C:\myfile.xlsx");
        var service = new ExcelService();

        //Act
        var actual = await service.GetDataAsync(stream);

        //Assert
        //...assert the contents of actual data.
    }
}

每个问题现在都可以单独测试。

是的,现在文件可以正常加载了。很抱歉我不是要求别人替我完成任务,但您能否给我一个例子,如何将Excel包提取到自己的接口和类中。因为在我的控制器中,我正在处理和读取上传的文件,然后返回数据。您的意思是我应该将所有内容移动到单独的Excel类中。如果您能给我一个小例子,我会非常感激。 - Brenda
谢谢,我会尝试使用这个重构,并告诉你结果。 - Brenda
@karen,请检查最新更新。根据代码结构,一切都应该是自解释的。 - Nkosi

1
你不需要模拟 EPPlus 进行测试。你的重点应该是测试你的代码,而不是 EPPlus 本身。就像你不会测试任何其他库一样。因此,让你的代码使用 EPPlus 在内存中生成一个 Excel 文件并返回它。然后在你的测试中使用 EPPlus 来验证对文件的断言。以下是一个可用的模式示例:
public class MyReportGenerator : IReportGenerator
{
    /* implementation here */
}

public interface IReportGenerator
{
    byte[] GenerateMyReport(ReportParameters parameters);
}

[TestMethod]
public void TestMyReportGenerate()
{
    //arrange
    var parameters = new ReportParameters(/* some values */);
    var reportGenerator = new MyReportGenerator(/* some dependencies */);

    //act
    byte[] resultFile = reportGenerator.GenerateMyReport(parameters);

    //assert
    using(var stream = new MemoryStream(resultFile))
    using(var package = new ExcelPackage(stream))
    {
        //now test that it generated properly, such as:
        package.Workbook.Worksheets["Sheet1"].Cells["C6"].GetValue<decimal>().Should().Be(3.14m);
        package.Workbook.Worksheets["Sheet1"].Column(5).Hidden.Should().BeTrue();
    }  
}

上面的示例使用了Fluent Assertions library,但显然这并非必需。

谢谢您的反馈。请查看我的帖子,因为我添加了更多代码。我正在我的API中进行这个操作。我还添加了测试类。如何在我的测试类中调用控制器上传方法并传递文件?您上面展示的方式是在测试方法中创建Excel。我相信我需要测试我的API方法。我该怎么做呢?感谢。 - Brenda
1
@karen,你的控制器不应直接修改文件。将所有EPPlus操作放到一个单独的类中(例如我的示例中的MyReportGenerator),然后你的控制器应该通过接口(例如我的示例中的IReportGenerator)依赖于该类。然后你可以单独测试控制器逻辑和报告生成/修改逻辑。在我的示例中,我创建了一个全新的Excel文件,但你也可以使用包含文件的MemoryStream。这样可以将报告修改逻辑与Web应用程序逻辑分开,使它们可以独立测试。 - mason

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接