Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unit Testing a Web API end point that you upload files

I have a web api end point that i want to unit test. I have a custom SwaggerUploadFile attribute that allows a file upload button on the swagger page. But for unit testing I cant figure out how to pass in a file.

For unit testing I am using: Xunit, Moq and Fluent Assertions

Below is my controller with the endpoint:

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

Unit test I have so far:

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

The above fails as when it hits this line if(!Request.Content.IsMimeMultipartContent()) we get a NullReferenceException > "{"Object reference not set to an instance of an object."}"

Best Answer Implemented:

Created an interface:

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

        bool IsMimeMultiPartContent();
    }

Then an implementation:

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

Now my controller uses constructor injection to get the 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;
        }

New implementation on method:

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

And my unit test that mocks the ApiController Request:

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

Thanks to @Badulake for the idea

like image 517
JohnChris Avatar asked Jul 11 '18 11:07

JohnChris


People also ask

Is it possible to unit test Web API?

We have an option to create a Unit test project when we create a Web API project. When we create a unit test project with a web API project, Visual Studio IDE automatically adds Web API project reference to the unit test project.


2 Answers

You should do a better separation in the logic of the method. Refactor your method so it does not depends on any class related to your web framework , in this case the Request class. Your upload code does not need to know anything about it. As a hint:

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

could be transformed to:

var provider = IProviderExtracter.Extract();

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

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

In your tests you can easely mock IProviderExtracter and focus in which work is doing each part of your code.

The idea is you get the most decoupled code so your worries are focused only in mocking the classes you have developed, no the ones the framework forces you to use.

like image 172
X.Otano Avatar answered Nov 14 '22 23:11

X.Otano


The below was how I initially solved it but after Badulake's answer i implemented that where i abstracted the api request to an interface/class and Mocked it out with Moq. I edited my question and put the best implementation there, but i left this answer here for people who dont want to go to the trouble of mocking it

I used part of this guide but I made a simpler solution:

New unit test:

    [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 support method:

    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;
    }
like image 41
JohnChris Avatar answered Nov 15 '22 00:11

JohnChris