【问题标题】:Unit Testing a Web API end point that you upload files对您上传文件的 Web API 端点进行单元测试
【发布时间】:2018-07-11 11:50:12
【问题描述】:

我有一个要进行单元测试的 Web api 端点。我有一个自定义的SwaggerUploadFile 属性,它允许在招摇页面上使用文件上传按钮。但是对于单元测试,我不知道如何传入文件。

对于单元测试,我正在使用:XunitMoqFluent 断言

下面是我的带有端点的控制器:

public class MyAppController : ApiController
{
    private readonly IMyApp _myApp;

    public MyAppController(IMyApp myApp)
    {
         if (myApp == null) throw new ArgumentNullException(nameof(myApp));
         _myApp = myApp;
    }

    [HttpPost]
    [ResponseType(typeof(string))]
    [Route("api/myApp/UploadFile")]
    [SwaggerUploadFile("myFile", "Upload a .zip format file", Required = true, Type = "file")]
    public async Task<IHttpActionResult> UploadFile()
    {
        if (!Request.Content.IsMimeMultipartContent())
        {
            throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
        }

        var provider = await Request.Content.ReadAsMultipartAsync();
        var bytes = await provider.Contents.First().ReadAsByteArrayAsync();
        try
        {
           var retVal = _myApp.CheckAndSaveByteStreamAsync(bytes).Result;
           if(retVal)
            {
                return
                    ResponseMessage(
                        new HttpResponseMessage(HttpStatusCode.OK)
                        {
                            Content = new StringContent(JsonConvert.SerializeObject(
                                new WebApiResponse
                                {
                                    Message = "File has been saved"
                                }), Encoding.UTF8, "application/json")
                        });
            }             
            return ResponseMessage(
                new HttpResponseMessage(HttpStatusCode.BadRequest)
                {
                    Content = new StringContent(JsonConvert.SerializeObject(
                        new WebApiResponse
                        {
                            Message = "The file could not be saved"
                        }), Encoding.UTF8, "application/json")
                });
        }
        catch (Exception e)
        {
            //log error
            return BadRequest("Oops...something went wrong");
        }
    }    
}

到目前为止我进行的单元测试:

    [Fact]
    [Trait("Category", "MyAppController")]
    public void UploadFileTestWorks()
    {
        //Arrange

        _myApp.Setup(x => x.CheckAndSaveByteStreamAsync(It.IsAny<byte[]>())).ReturnsAsync(() => true);
        var expected = JsonConvert.SerializeObject(
            new WebApiResponse
            {
                Message = "The file has been saved"
            });

        var _sut = new MyAppController(_myApp.Object);


        //Act
        var retVal = _sut.UploadFile();
        var content = (ResponseMessageResult)retVal.Result;
        var contentResult = content.Response.Content.ReadAsStringAsync().Result;
        //Assert
        contentResult.Should().Be(expected); 
    }

上面的失败,因为当它到达if(!Request.Content.IsMimeMultipartContent())这一行时,我们得到一个NullReferenceException > "{"Object reference not set to an instance of an object."}"

已实施的最佳答案:

创建了一个接口:

 public interface IApiRequestProvider
    {
        Task<MultipartMemoryStreamProvider> ReadAsMultiPartAsync();

        bool IsMimeMultiPartContent();
    }

然后是一个实现:

public class ApiRequestProvider : ApiController, IApiRequestProvider
    {       
        public Task<MultipartMemoryStreamProvider> ReadAsMultiPartAsync()
        {
            return Request.Content.ReadAsMultipartAsync();
        }
        public bool IsMimeMultiPartContent()
        {
            return Request.Content.IsMimeMultipartContent();
        }
    }

现在我的控制器使用构造函数注入来获取 RequestProvider:

 private readonly IMyApp _myApp;
 private readonly IApiRequestProvider _apiRequestProvider;

 public MyAppController(IMyApp myApp, IApiRequestProvider apiRequestProvider)
        {
             if (myApp == null) throw new ArgumentNullException(nameof(myApp));
             _myApp = myApp;

             if (apiRequestProvider== null) throw new ArgumentNullException(nameof(apiRequestProvider));
             _apiRequestProvider= apiRequestProvider;
        }

方法的新实现:

[HttpPost]
        [ResponseType(typeof(string))]
        [Route("api/myApp/UploadFile")]
        [SwaggerUploadFile("myFile", "Upload a .zip format file", Required = true, Type = "file")]
        public async Task<IHttpActionResult> UploadFile()
        {
            if (!_apiRequestProvider.IsMimeMultiPartContent())
            {
                throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
            }

            var provider = await _apiRequestProvider.ReadAsMultiPartAsync();
            var bytes = await provider.Contents.First().ReadAsByteArrayAsync();
            try
            {
               var retVal = _myApp.CheckAndSaveByteStreamAsync(bytes).Result;
               if(retVal)
                {
                    return
                        ResponseMessage(
                            new HttpResponseMessage(HttpStatusCode.OK)


             {
                            Content = new StringContent(JsonConvert.SerializeObject(
                                new WebApiResponse
                                {
                                    Message = "File has been saved"
                                }), Encoding.UTF8, "application/json")
                        });
            }             
            return ResponseMessage(
                new HttpResponseMessage(HttpStatusCode.BadRequest)
                {
                    Content = new StringContent(JsonConvert.SerializeObject(
                        new WebApiResponse
                        {
                            Message = "The file could not be saved"
                        }), Encoding.UTF8, "application/json")
                });
        }
        catch (Exception e)
        {
            //log error
            return BadRequest("Oops...something went wrong");
        }
    }    
}

还有我模拟 ApiController 请求的单元测试:

    [Fact]
    [Trait("Category", "MyAppController")]
    public void UploadFileTestWorks()
    {
        //Arrange
        _apiRequestProvider = new Mock<IApiRequestProvider>();
        _myApp = new Mock<IMyApp>();
         MultipartMemoryStreamProvider fakeStream = new MultipartMemoryStreamProvider();
        fakeStream.Contents.Add(CreateFakeMultiPartFormData());
        _apiRequestProvider.Setup(x => x.IsMimeMultiPartContent()).Returns(true);
        _apiRequestProvider.Setup(x => x.ReadAsMultiPartAsync()).ReturnsAsync(()=>fakeStream);
        _myApp.Setup(x => x.CheckAndSaveByteStreamAsync(It.IsAny<byte[]>())).ReturnsAsync(() => true);
        var expected = JsonConvert.SerializeObject(
            new WebApiResponse
            {
                Message = "The file has been saved"
            });

        var _sut = new MyAppController(_myApp.Object, _apiRequestProvider.Object);

        //Act
        var retVal = _sut.UploadFile();
        var content = (ResponseMessageResult)retVal.Result;
        var contentResult = content.Response.Content.ReadAsStringAsync().Result;
        //Assert
        contentResult.Should().Be(expected); 
    }

感谢@Badulake 的创意

【问题讨论】:

  • 这对你真的有用吗?我使用相同的设置return Request.Content.IsMimeMultipartContent();ApiRequestProvider 中抛出空引用
  • 是的,这对我有用,你成功了吗?
  • 对我也不起作用,也有一个空引用异常。我假设是因为界面中的“请求”为空。

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


【解决方案1】:

你应该在方法的逻辑上做一个更好的分离。 重构您的方法,使其不依赖于与您的 Web 框架相关的任何类,在本例中为 Request 类。您的上传代码不需要知道任何相关信息。 提示:

var provider = await Request.Content.ReadAsMultipartAsync();

可以转化为:

var provider = IProviderExtracter.Extract();

public interface IProviderExtracter
{
    Task<provider> Extract();
}

public class RequestProviderExtracter:IProviderExtracter
{
    public Task<provider> Extract()
    { 
      return Request.Content.ReadAsMultipartAsync();
    }
}

在您的测试中,您可以轻松地模拟 IProviderExtracter 并专注于执行代码的每个部分的工作。

我们的想法是您获得最解耦的代码,因此您的担忧只集中在模拟您开发的类上,而不是框架强迫您使用的那些。

【讨论】:

  • 感谢您的回答,我设法解决了我的单元测试问题,但我会尝试实现您的抽象
  • @JohnChris 你在单元测试上花费的时间越多,你就会发现付出的努力是值得的。我的回答只是一个例子,您必须选择要抽象代码的部分
  • 是的,我同意,我现在试试,我认为如果我可以将它抽象为一个接口并模拟它,它会比我目前的解决方案更好
  • @JohnChris 过几天回来告诉我们你的感受;)我会把啤酒冷藏
  • 工作就像一个魅力感谢您的帮助,我在我的问题下方发布了我的实现
【解决方案2】:

以下是我最初解决它的方法,但在 Badulake 的回答之后,我实现了我将 api 请求抽象为接口/类并用 Moq 模拟出来的方法。我编辑了我的问题并将最好的实现放在那里,但是我把这个答案留给了那些不想去嘲笑它的人

我使用了this guide 的一部分,但我做了一个更简单的解决方案:

新的单元测试:

    [Fact]
    [Trait("Category", "MyAppController")]
    public void UploadFileTestWorks()
    {
        //Arrange
       var multiPartContent = CreateFakeMultiPartFormData();
        _myApp.Setup(x => x.CheckAndSaveByteStreamAsync(It.IsAny<byte[]>())).ReturnsAsync(() => true);
        var expected = JsonConvert.SerializeObject(
        new WebApiResponse
        {
            Message = "The file has been saved"
        });

        
        _sut = new MyAppController(_myApp.Object);

        //Sets a controller request message content to 
        _sut.Request = new HttpRequestMessage()
        {
            Method = HttpMethod.Post,
            Content = multiPartContent
        };
        //Act
        var retVal = _sut.UploadFile();
        var content = (ResponseMessageResult)retVal.Result;
        var contentResult = content.Response.Content.ReadAsStringAsync().Result;
        //Assert
        contentResult.Should().Be(expected); 
    }
    

私人支持方式:

    private static MultipartFormDataContent CreateFakeMultiPartFormData()
    {
        byte[] data = { 1, 2, 3, 4, 5 };
        ByteArrayContent byteContent = new ByteArrayContent(data);
        StringContent stringContent = new StringContent(
            "blah blah",
            System.Text.Encoding.UTF8);

        MultipartFormDataContent multipartContent = new MultipartFormDataContent { byteContent, stringContent };
        return multipartContent;
    }

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-06-02
    • 1970-01-01
    • 2010-09-18
    • 2021-09-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多