Search code examples
c#asp.net-coremoqepplus

Create Moq for Epplus Excel file


My first question here. I have looked my query but could not find a helpful answer.

My task is to write unit test case for my excel file. The issue I am facing is that we using Epplus for excel files and I am not sure how can we write unit test cases for this. I looked up and found that we can also use MOQ to mock up. But again I could not find any helpful links for mocking an excel file that uses Epplus. I found this link Unit testing classes that use EPPlus but I am not sure how I can implement this .

I would appreciate if someone can provide a sample of how to write a simple unit test for the excel file. The test can be to check if file uploaded is an excel file or not, checking if the excel is empty or not etc.

Sorry at this moment I dont have any sample. What I can share is the code where I am reading the excel file:

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

I had created a helper class as:

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

This is the Test Class i have so far.

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."
    }
}

Solution

  • In this case mock IFormFile to return the file stream in your test and pass that to the action under test. Make sure all other necessary dependencies are satisfied.

    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
            //...
        }
    }
    

    Now while this should help get you through the current problem, you should really take the advice suggested by other answers about abstracting that tight coupling of ExcelPackage out of the controller into its own concern. Would make unit testing the controller easier in isolation.

    You could always do an integration test of the wrapper separately as needed.

    A simplified example of an interface abstracted from what is currently in the controller

    public interface IExcelService {
        Task<JArray> GetDataAsync(Stream stream);
    }
    

    which would have an implementation that mirrors the code in the controller

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

    The controller can now be simplified to follow the Explicit Dependencies Principle

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

    You would make sure that the interface and implementation are registered with the Dependency Inversion framework in Startup

    services.AddScoped<IExcelService, ExcelService>();
    

    So now the controller is only concerned with what it is suppose to do when called at run time. I has no reason to be dealing with implementation concerns

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

    To do an integration test that involves an actual file you can test the service

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

    Each concern can now be tested on its own.