在我的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库。
我怎样才能做到这一点? (多部分内容可能?)
由于
答案 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应用程序中,我们有ModelBinder
和HttpPostedFileBase
对象,我们可以在控制器中使用它来避免使用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());