Search code examples
c#asp.net-mvcunit-testingmemorystream

How to UnitTest this method properly?


I have a method (below) that is called from a controller in a MVC application which then outputs an excel file.

Method:

public static void ExportToExcel(IEnumerable<dynamic> data, string bookName, string sheetName)
{
    XLWorkbook workbook = new XLWorkbook();
    var worksheet = workbook.Worksheets.Add(sheetName);
    worksheet.Cell(1, 1).InsertTable(data);

    HttpContext.Current.Response.Clear();
    HttpContext.Current.Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
    HttpContext.Current.Response.AddHeader("content-disposition", String.Format(@"attachment;filename={0}.xlsx", bookName.Replace(" ", "_")));

    using (MemoryStream memStream = new MemoryStream())
    {
        workbook.SaveAs(memStream);
        memStream.WriteTo(HttpContext.Current.Response.OutputStream);
        memStream.Close();
    }
    HttpContext.Current.Response.End();
}

Controller:

[ActionName("ExportData")]
public ActionResult ExportData()
{
    ExcelExport.ExportToExcel(_dbaccess.GetAllData()), "Workbook", "Worksheet");

    return RedirectToAction("Index");
}

How can I test this method?


Solution

  • First you need to refactor that method to make it a little more testable. There are just too many concerns mixed up in there to make testing it easy. (My opinion).

    Strip out workbook generation, which can belong to the same class or to some dependency

    public interface IGetWorkBook {
        XLWorkbook GetWorkBook(IEnumerable<dynamic> data, string sheetName);
    }
    

    where the implementation can look like exactly what you have in your original method.

    public XLWorkbook GetWorkBook(IEnumerable<dynamic> data, string sheetName) {    
        XLWorkbook workbook = new XLWorkbook();
        var worksheet = workbook.Worksheets.Add(sheetName);
        worksheet.Cell(1, 1).InsertTable(data);    
    }
    

    Next you need to abstract away the tight coupling to HttpContext

    there are some good articles about getting around testing with HttpContext

    Don't mock HttpContext He doesn't like to be mocked! :)

    The question you have to ask is what am I trying to achieve?. In this case we want to save the workbook to something. Yes in this case its the HttpContext response, but it could be something else. For that we need an abstraction

    public interface IWriteWorkbook {
        void Write(XLWorkbook workbook, string bookName); 
    }
    

    which you can later include in a derived class the HttpContext you so love to use.

    void Write(XLWorkbook workbook, string bookName) {
        HttpContext.Current.Response.Clear();
        HttpContext.Current.Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
        HttpContext.Current.Response.AddHeader("content-disposition", String.Format(@"attachment;filename={0}.xlsx", bookName.Replace(" ", "_")));
    
        using (MemoryStream memStream = new MemoryStream())
        {
            workbook.SaveAs(memStream);
            memStream.WriteTo(HttpContext.Current.Response.OutputStream);
            memStream.Close();
        }
        HttpContext.Current.Response.End();
    }
    

    This too can be improved upon but that's out side of this post.

    Your refactored method could look something like this after all the changes.

    public interface IExcelExporter {
        void ExportToExcel(IEnumerable<dynamic> data, string bookName, string sheetName);
    }
    
    public class ExcelExport : IExcelExporter {
        IGetWorkBook workbookgGetter;
        IWriteWorkbook workbookWriter;
    
        public ExcelExport (IGetWorkBook workbookgGetter,IWriteWorkbook workbookWriter) {
            this.workbookgGetter = workbookgGetter;
            this.workbookWriter = workbookWriter;
        }
    
        public void ExportToExcel(IEnumerable<dynamic> data, string bookName, string sheetName)
        {
            XLWorkbook workbook = workbookgGetter.GetWorkBook(data, sheetName);
            void workbookWriter.Write(workbook,bookName);        
        }    
    }
    

    Ok, phew...that's a lot. Didn't think it would be that much did you? But it's worth it in the end. I think? :)

    Now we need target the controller.

    public class MyExcelController: Controller {
    
        public MyExcelController(IExcelExporter exporter){
            ExcelExport = exporter;
        }
    
        IExcelExporter ExcelExport{get; private set;}
    
        [ActionName("ExportData")]
        public ActionResult ExportData()
        {
            ExcelExport.ExportToExcel(_dbaccess.GetAllData(), "Workbook", "Worksheet");
    
            return RedirectToAction("Index");
        }
    }
    

    Note how after all this that the original controller action has not changed while everything around it has :) sweet!!.

    Now we are ready to mock and test to our hearts content.

    You can test IGetWorkBook.GetWorkBook

    You can test IWriteWorkbook.Write

    You can test ExcelExport.ExportToExcel

    to make sure they all operate as expected.