如何创建一个文件来填充HttpContext.Current.Request.Files?

时间:2015-04-23 22:50:12

标签: c# unit-testing mocking asp.net-web-api

在我的Web API中,POST操作方法在服务器上上传文件。

对于单元测试此方法,我需要创建一个HttpContext并将一个文件放在其请求中:

HttpContext.Current.Request.Files

到目前为止,我正在使用此代码伪装HttpContext,该代码完美无缺:

  HttpRequest request = new HttpRequest("", "http://localhost/", "");
  HttpResponse response = new HttpResponse(new StringWriter());
  HttpContext.Current = new HttpContext(request, response);

请注意,我不想使用Moq或任何其他Mocking库。

我怎样才能做到这一点? (多部分内容可能?)

由于

2 个答案:

答案 0 :(得分:7)

鉴于大多数HttpContext基础设施隐藏在密封或内部,我最终能够通过大量使用reflection将伪文件添加到Request.Files进行WebApi单元测试类。

一旦您添加了以下代码,就可以相对轻松地将文件添加到HttpContext.Current

var request = new HttpRequest(null, "http://tempuri.org", null);
AddFileToRequest(request, "File", "img/jpg", new byte[] {1,2,3,4,5});

HttpContext.Current = new HttpContext(
    request,
    new HttpResponse(new StringWriter());

通过以下方式完成繁重的工作:

static void AddFileToRequest(
    HttpRequest request, string fileName, string contentType, byte[] bytes)
{
    var fileSize = bytes.Length;

    // Because these are internal classes, we can't even reference their types here
    var uploadedContent = ReflectionHelpers.Construct(typeof (HttpPostedFile).Assembly,
        "System.Web.HttpRawUploadedContent", fileSize, fileSize);
    uploadedContent.InvokeMethod("AddBytes", bytes, 0, fileSize);
    uploadedContent.InvokeMethod("DoneAddingBytes");

    var inputStream = Construct(typeof (HttpPostedFile).Assembly,
        "System.Web.HttpInputStream", uploadedContent, 0, fileSize);

    var postedFile = Construct<HttpPostedFile>(fileName, 
             contentType, inputStream);
    // Accessing request.Files creates an empty collection
    request.Files.InvokeMethod("AddFile", fileName, postedFile);
}

public static object Construct(Assembly assembly, string typeFqn, params object[] args)
{
    var theType = assembly.GetType(typeFqn);
    return theType
      .GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, 
             args.Select(a => a.GetType()).ToArray(), null)
      .Invoke(args);
}

public static T Construct<T>(params object[] args) where T : class
{
    return Activator.CreateInstance(
        typeof(T), 
        BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance,
        null, args, null) as T;
}

public static object InvokeMethod(this object o, string methodName, 
     params object[] args)
{
    var mi = o.GetType().GetMethod(methodName, 
             BindingFlags.NonPublic | BindingFlags.Instance);
    if (mi == null) throw new ArgumentOutOfRangeException("methodName",
        string.Format("Method {0} not found", methodName));
    return mi.Invoke(o, args);
}

答案 1 :(得分:3)

通常使用难以在控制器中模拟的对象(如HttpContext, HttpRequest, HttpResponse等对象)是一种不好的做法。例如,在MVC应用程序中,我们有ModelBinderHttpPostedFileBase对象,我们可以在控制器中使用它来避免使用HttpContext(对于Web Api应用程序,我们需要编写自己的逻辑) 。

public ActionResult SaveUser(RegisteredUser data, HttpPostedFileBase file)
{
   // some code here
}

因此您无需使用HttpContext.Current.Request.Files。这很难测试。这种类型的工作必须在应用程序的另一级别(而不是在控制器中)完成。在Web Api中,我们可以为此目的编写MediaTypeFormatter。

public class FileFormatter : MediaTypeFormatter
{
    public FileFormatter()
    {
        SupportedMediaTypes.Add(new MediaTypeHeaderValue("multipart/form-data"));
    }

    public override bool CanReadType(Type type)
    {
        return typeof(ImageContentList).IsAssignableFrom(type);
    }

    public override bool CanWriteType(Type type)
    {
        return false;
    }

    public async override Task<object> ReadFromStreamAsync(Type type, Stream stream, HttpContent content, IFormatterLogger logger)
    {
        if (!content.IsMimeMultipartContent())
        {
            throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
        }

        var provider = new MultipartMemoryStreamProvider();
        var formData = await content.ReadAsMultipartAsync(provider);

        var imageContent = formData.Contents
            .Where(c => SupportedMediaTypes.Contains(c.Headers.ContentType))
            .Select(i => ReadContent(i).Result)
            .ToList();

        var jsonContent = formData.Contents
            .Where(c => !SupportedMediaTypes.Contains(c.Headers.ContentType))
            .Select(j => ReadJson(j).Result)
            .ToDictionary(x => x.Key, x => x.Value);

        var json = JsonConvert.SerializeObject(jsonContent);
        var model = JsonConvert.DeserializeObject(json, type) as ImageContentList;

        if (model == null)
        {
            throw new HttpResponseException(HttpStatusCode.NoContent);
        }

        model.Images = imageContent;
        return model; 
    }

    private async Task<ImageContent> ReadContent(HttpContent content)
    {
        var data = await content.ReadAsByteArrayAsync();
        return new ImageContent
        {
            Content = data,
            ContentType = content.Headers.ContentType.MediaType,
            Name = content.Headers.ContentDisposition.FileName
        };
    }

    private async Task<KeyValuePair<string, object>> ReadJson(HttpContent content)
    {
        var name = content.Headers.ContentDisposition.Name.Replace("\"", string.Empty);
        var value = await content.ReadAsStringAsync();

        if (value.ToLower() == "null")
            value = null;

        return new KeyValuePair<string, object>(name, value);
    }
}

因此,任何将使用multipart/form-data内容类型发布的内容(以及必须使用该内容类型发布的文件)都将被解析为ImageContentList的子类(因此您可以发布文件)任何其他信息)。如果你想发布2或3个文件 - 它也会起作用。

public class ImageContent: IModel
{
    public byte[] Content { get; set; }
    public string ContentType { get; set; }
    public string Name { get; set; }
}

public class ImageContentList
{
    public ImageContentList()
    {
        Images = new List<ImageContent>();
    }
    public List<ImageContent> Images { get; set; } 
}

public class CategoryPostModel : ImageContentList
{
    public int? ParentId { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
}

然后您可以在应用程序的任何控制器中使用它。并且它很容易测试,因为控制器的代码不再依赖于HttpContext。

public ImagePostResultModel Post(CategoryPostModel model)
{
    // some code here
}

您还需要为MediaTypeFormatter配置

注册Web Api
configuration.Formatters.Add(new ImageFormatter());