I am trying to write unit tests for DocumentDBRepository paging code. Since there is continuation token involved in the FeedResponse, I need to mock the FeedResponse in order to put some value for FeedResponse.ContinuationToken. But the problem is that I got an error saying:
Message: System.ArgumentException : Constructor arguments cannot be
passed for interface mocks.
Does it mean I am not able to mock FeedResponse? Or maybe the way I use FeedResponse is wrong?
Here's my code:
var response = new Mock<IFeedResponse<T>>(expected);
response.Setup(_ => _.ResponseContinuation).Returns(It.IsAny<string>());
var mockDocumentQuery = new Mock<IFakeDocumentQuery<T>>();
mockDocumentQuery
.SetupSequence(_ => _.HasMoreResults)
.Returns(true)
.Returns(false);
mockDocumentQuery
.Setup(_ => _.ExecuteNextAsync<T>(It.IsAny<CancellationToken>()))
.Returns((Task<FeedResponse<T>>)response.Object);
When I debugged, the break point stops at var response = new Mock<IFeedResponse<T>>(expected); and then the error happened.
The error is because you were mocking the interface and trying to pass a constructor argument. That wont work as stated by the error message.
You can however use an actual instance of FeedResponse.
Given that the desired member is not virtual and is also read-only, you could consider stubbing the class and overriding the default behavior since FeedResponse<T> is not sealed.
For example
public class FeedResponseStub<T> : FeedResponse<T> {
private string token;
public FeedResponseStub(IEnumerable<T> result, string token)
: base(result) {
this.token = token;
}
public new string ResponseContinuation {
get {
return token;
}
}
}
and using the stub in the test
//...
var token = ".....";
var response = new FeedResponseStub<T>(expected, token);
//...
mockDocumentQuery
.Setup(_ => _.ExecuteNextAsync<T>(It.IsAny<CancellationToken>()))
.ReturnsAsync(response);
//...
Here is the way I work around it in Cosmonaut.
public static FeedResponse<T> ToFeedResponse<T>(this IQueryable<T> resource, IDictionary<string, string> responseHeaders = null)
{
var feedResponseType = Type.GetType("Microsoft.Azure.Documents.Client.FeedResponse`1, Microsoft.Azure.DocumentDB.Core, Version=1.9.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");
var flags = BindingFlags.NonPublic | BindingFlags.Instance;
var headers = new NameValueCollection
{
{ "x-ms-request-charge", "0" },
{ "x-ms-activity-id", Guid.NewGuid().ToString() }
};
if (responseHeaders != null)
{
foreach (var responseHeader in responseHeaders)
{
headers[responseHeader.Key] = responseHeader.Value;
}
}
var arguments = new object[] { resource, resource.Count(), headers, false, null };
if (feedResponseType != null)
{
var t = feedResponseType.MakeGenericType(typeof(T));
var feedResponse = Activator.CreateInstance(t, flags, null, arguments, null);
return (FeedResponse<T>)feedResponse;
}
return new FeedResponse<T>();
}
}
You can pass your continuation token as a header key-value in the dictionary to set the FeedResponse value.
You can do that by setting the x-ms-continuation value to a token.
Keep in mind that the ResponseContinuation property of the FeedResponse also takes the useETagAsContinuation value into account. I default it to false in the reflection invoked constructor.
For any further reference check the project's code and how unit tests are written.
Related
I am writing some tests where the class I'm testing depends on HttpClient. To mock that I am mocking a HttpMessageHandler and pass that to the HttpClient constructor.
To accomplish this I have a base class:
public class HttpTestBase
{
protected static readonly string BaseAddress = "https://test.com";
protected readonly HttpClient _httpClient;
protected readonly Mock<HttpMessageHandler> _httpMessageHandlerMock;
public HttpTestBase()
{
_httpMessageHandlerMock = new Mock<HttpMessageHandler>();
_httpClient = new HttpClient(_httpMessageHandlerMock.Object);
_httpClient.BaseAddress = new Uri(BaseAddress);
}
protected void MockHttpResponse(HttpResponseMessage message, string expectedPath, HttpMethod expectedMethod)
{
_httpMessageHandlerMock
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.Is<HttpRequestMessage>(request => AssertRequestParameters(expectedPath, expectedMethod, request)),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(message);
}
private bool AssertRequestParameters(string expectedPath, HttpMethod expectedMethod, HttpRequestMessage request)
{
// Throw an exception if the method or path does not match what is expected.
}
}
And then a test looks as follows, in a test class inheriting this test base:
[Fact]
public async Task GetAvailableLicenseCount()
{
// Arrange
var licenses = new JsonObject
{
["total_seats_consumed"] = 4500,
["total_seats_purchased"] = 5000
};
MockHttpResponse(
new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent(licenses.ToJsonString()) },
expectedPath: "/enterprises/enterprise/consumed-licenses",
expectedMethod: HttpMethod.Get
);
// Act
var result = await sut.GetAvailableLicenseCount();
// Assert
result.Should().Be(500);
}
This works well when a method only performs a single call. Some methods perform multiple calls in a sequence. I have solved that partially, but I would still like to have the assertion in the setup (e.g. when I call AssertRequestParameters).
To support multiple calls in a chain, I instead did this in the base class:
protected void AddHttpMockResponse(HttpResponseMessage message, string expectedPath, HttpMethod expectedMethod)
{
_responseMocks.Enqueue(new HttpMock
{
Response = message,
Assertion = new Assertion { ExpectedPath = expectedPath, ExpectedMethod = expectedMethod }
});
}
protected void MockHttpResponses()
{
_httpMessageHandlerMock
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.IsAny<HttpRequestMessage>(),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(() => _responseMocks.Dequeue().Response);
}
As you can see I have removed the assertion in the setup, because it seems that the assertions collide and I get the wrong return value from the mock.
To use this, I refactored the test as follows:
[Fact]
public async Task GetAvailableLicenseCount()
{
// Arrange
var licenses = new JsonObject
{
["total_seats_consumed"] = 4500,
["total_seats_purchased"] = 5000
};
AddHttpMockResponse(
new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent(licenses.ToJsonString()) },
expectedPath: "/enterprises/enterprise/consumed-licenses",
expectedMethod: HttpMethod.Get
);
MockHttpResponses();
// Act
var result = await sut.GetAvailableLicenseCount();
// Assert
result.Should().Be(500);
}
This also works well for multiple responses from the same mock.
However, I would like to be able to mock multiple responses where the response is determined by what I use in the Setup of the mock. The reason I want to do this is that I cannot use Verify on HttpMessageHandler as the method is not accessible to me.
Is it possible to achieve a dynamic return value based on what is done in the setup of the mock?
If I understand your question correctly you'll want something like this:
I recently had to do a similar thing and found the below article very helpful. Check the 'Mock HttpMessageHandler Using Moq' section. It shows you how to return a HttpResponseMessage object you create, which is what I think you might want.
https://code-maze.com/csharp-mock-httpclient-with-unit-tests/
Field on your class
private readonly Mock<HttpMessageHandler> _httpMessageHandlerStub;
In your setup (constructor or a separate method)
_httpMessageHandlerStub = new Mock<HttpMessageHandler>();
var httpClient = new HttpClient(_httpMessageHandlerStub.Object);
_sut = new SomeService(
httpClient,
anotherDependency
)
And then you can configure the below in each test
[Fact]
public async Task GetListAsync_Successfully_Uses_Api_Key()
{
var unauthorisedResponseMessage = new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized)
{
Content = new StringContent($"'{_apiKeyHeaderName}' header not found or API key is incorrect")
};
var successResponseMessage = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent(JsonConvert.SerializeObject(new List<SomeDto>()))
};
// Return 200 if api key header is present and the value is correct.
_httpMessageHandlerStub.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.Is<HttpRequestMessage>(x => x.Method == HttpMethod.Get && x.Headers.Any(h => h.Key == _apiKeyHeaderName && h.Value.FirstOrDefault() == _apiKeyHeaderValue)),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(successResponseMessage);
// Return 401 is api key header is not present or api key value is incorrect.
_httpMessageHandlerStub.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.Is<HttpRequestMessage>(x => x.Method == HttpMethod.Get && !x.Headers.Any(h => h.Key == _apiKeyHeaderName && h.Value.FirstOrDefault() == _apiKeyHeaderValue)),
ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(unauthorisedResponseMessage);
var result = _sut.GetListAsync();
// Assert stuff...
}
Hope that's helpful.
Posting this question after trying a lot. Doing normal for is not an option because we need to do a large amount of processing in very less time.
I have GetDataFor() inside which HttpContext.Current is used.
The code looks like this:
public void SomeMethod()
{
var context = HttpContext.Current;
Parallel.For(0, 100, i =>
{
var data = GetDataFor(i, context);
});
}
public data GetDataFor(int i, HttpContext context)
{
Uri requestUri = null;
if (HttpContext.Current != null)
{
requestUri = HttpContext.Current.Request.Url;
sCookie = string.Format("{0}", HttpContext.Current.Request.Headers["cookie"]);
}
else
{
requestUri = context.Request.Url;
}
//do something
return data;
}
Everything works fine inside normal for loop. However, when I call it inside Parallel.For and pass HttpContext.Current, HttpContext.Current.Request, HttpContext.Current.Request.Url as method parameters:
HttpContext.Current cannot be serialized because it does not have a parameterless constructor
Passing HttpContextBase httpContext = null as parameter throws:
To be XML serializable, types which inherit from ICollection must have an implementation of Add(System.Object) at all levels of their inheritance hierarchy. System.Web.HttpApplicationStateBase does not implement Add(System.Object).
Tried making a property:
public string[] httpContextData
{
get
{
string requestUrl = HttpContext.Current.Request.Url.ToString();
string sCookie = string.Format("{0}", HttpContext.Current.Request.Headers["cookie"]);
return new string[] { requestUrl, sCookie };
}
}
and using in method:
var contextData = httpContextData;
which throws:
System.Uri cannot be serialized because it does not have a parameterless constructor
I did all this to send it's reference and state but unable to understand why the problem is not solving.
How do I use HttpContext.Current inside Parallel.For? What am I doing wrong here?
Btw, the needed stuff are:
HttpContext.Current.Request.Url and HttpContext.Current.Request.Headers["cookie"]
HttpContext.Current is only available (not null) inside request-handling threads. Parallel.For creates multiple threads, none of which is has access to HttpContext.Current.
You have to pass all data that code in Parallel.For threads needs either through
local variables assigned before the loop or
TLocal instance used in Parallel.For<TLocal>.
In any event, code such as HttpContext.Current.... is out.
The code is proprietary hence I'm only posting the relevant parts:
Since passing the following objects:
HttpContext.Current
HttpContext.Current.Request
HttpContext.Current.Request.Url
as params to GetDataFor was throwing so many errors.
Also, my needs were only
request url which can be re-generated by passing url as string to it's constructor
and a request header's value which is essentially a string
I only passed string to GetDataFor() method:
public void SomeMethod()
{
string requestUrl = HttpContext.Current.Request.Url.ToString();
string sCookie = string.Format("{0}", HttpContext.Current.Request.Headers["cookie"]);
Parallel.For(0, 100, i =>
{
var data = GetDataFor(i,
requestUrl: requestUrl,
sCookie: sCookie);
});
}
public data GetDataFor(int i, string requestUrl = null, string sCookie = null)
{
Uri requestUri = null;
if (HttpContext.Current != null)
{
requestUri = HttpContext.Current.Request.Url;
sCookie = string.Format("{0}", HttpContext.Current.Request.Headers["cookie"]);
}
else
{
requestUri = new Uri(requestUrl);
}
//do something
return data;
}
In our MVC4 application with Entity Framework 4.0 based on the Music Store Tutorial we are using Moq to mock the DbContext and unit test are logic. One of our methods proves difficult to test though since it makes use of HttpContext or HttpContextBase. One example method looks like this:
public static ShoppingCart GetCart(HttpContextBase context)
{
var cart = new ShoppingCart();
cart.ShoppingCartId = cart.GetCartId(context);
return cart;
}
The only property collected from HttpContextBase is the [CartSessionKey] as can be seen here:
public string GetCartId(HttpContextBase context)
{
if (context.Session[CartSessionKey] == null)
{
if (!string.IsNullOrWhiteSpace(context.User.Identity.Name))
{
context.Session[CartSessionKey] =
context.User.Identity.Name;
}
else
{
// Generate a new random GUID using System.Guid class
Guid tempCartId = Guid.NewGuid();
// Send tempCartId back to client as a cookie
context.Session[CartSessionKey] = tempCartId.ToString();
}
}
return context.Session[CartSessionKey].ToString();
}
We have heard horror stories that HttpContext is a very complex class and that if you print it you have enough paper to circle the earth eight times.
Nevertheless we want to mock it. The question is how. The properties that we want to mock are the [CartSessionKey], and the property that come from the context as contest.User.Identity.Name.
We suspect we need to use something like this:
var mockData = new Mock<FakeContext>();
mockData.Setup(m => m.Orders).Returns(memoryOrderItems);
mockData.Setup(m => m.Carts).Returns(memoryCartItems);
Mock<HttpContextBase> mockHttpContext = new Mock<HttpContextBase>();
Mock<HttpRequestBase> mockHttpRequest = new Mock<HttpRequestBase>();
mockHttpRequest.Setup(x => x.CartSessionKey).Returns(1);
mockHttpContext.Setup(x => x.Request).Returns(mockHttpRequest.Object);
but we cannot find how to specifically implement this so we do not get any errors on methods that use context.Session[CartSessionKey] or context.User.Identity.Name.
We hope someone can help us out.
/edit
When we do this:
var memoryUserItems = new FakeDbSet<User>()
{
new User { Email = "test#test.de",
FullName = "Test Person",
isAvailable = true,
Name = "WHat"
},
new User { Email = "test2#test.de",
FullName = "Test Person 2",
isAvailable = true,
Name = "WHat 2"
}
};
(...) Other memory...Items
And then this:
// Create mock units of work
var mockData = new Mock<FakeContext>();
mockData.Setup(m => m.Orders).Returns(memoryOrderItems);
mockData.Setup(m => m.Carts).Returns(memoryCartItems);
mockData.Setup(m => m.Users).Returns(memoryUserItems);
var principalMock = new Mock<IPrincipal>();
var identityMock = new Mock<IIdentity>();
var userMock =
identityMock.Setup(x => x.Name).Returns("Test!");
identityMock.Setup(x => x.IsAuthenticated).Returns(true); // optional ;)
mockData.Setup(x => x.Identity).Returns(identityMock.Object);
var httpReqBase = new Mock<HttpRequestBase>(); // this is useful if you want to test Ajax request checks or cookies in the controller.
var httpContextBase = new Mock<HttpContextBase>();
httpContextBase.Setup(x => x.User).Returns(principalMock.Object);
httpContextBase.Setup(x => x.Session[It.IsAny<string>()]).Returns(1); //Here is the session indexer. You can swap 'any' string for specific string.
httpContextBase.Setup(x => x.Request).Returns(httpReqBase.Object);
We get the error that:
Error 3 'project.Models.FakeContext' does
not contain a definition for 'Identity' and no extension method
'Identity' accepting a first argument of type
'project.Models.FakeContext' could be found
(are you missing a using directive or an assembly
reference?)
/ edit2
To make it more clear. The actual method I am testing is the following:
public ActionResult Complete(int id)
{
// Make sure that user is currentuser and otherwise bring user to our Thief page
if (id != db.GetCurrentUserId())
{
return View("Thief");
}
var cart = ShoppingCart.GetCart(this.HttpContext);
var currentDate = DateTime.Today;
var viewModel = new ShoppingCartViewModel
{
CartItems = cart.GetCartItems(),
CartTotal = cart.GetTotal(),
ProductItems = db.Products.ToList()
};
if (viewModel.CartItems.Count() == 0)
{
return View("Empty");
}
// Try to write cart to order table
try
{
foreach (var item in viewModel.CartItems)
{
ProcessOrder(item, id, currentDate);
}
// after this we empty the shopping cart
cart.EmptyCart();
return View();
}
catch
{
// Invalid - display error page
return View("Error");
}
}
As can be seen the var cart = ShoppingCart.GetCart(this.HttpContext); uses this.HttpContext. In the test I just do controller.Complete(1). I cannot pass a new HttpContext to the controller I guess?
/ edit 3
While using the code below with the mocks I get the following message:
Test Name: TestCheckoutCompleteShouldWithEmptyCart
Test FullName: Controllers.CheckoutControllerTest.TestCheckoutCompleteShouldWithEmptyCart
Test Source: Controllers\CheckoutControllerTest.cs : line 141
Test Outcome: Failed
Test Duration: 0:00:00.0158591
Result Message:
Test method Controllers.CheckoutControllerTest.TestCheckoutCompleteShouldWithEmptyCart threw exception:
System.NullReferenceException: Object reference not set to an instance of an object.
Result StackTrace:
at Models\ShoppingCart.cs:line 170
at \Models\ShoppingCart.cs:line 20
at \Controllers\CheckoutController.cs:line 48
at Controllers\CheckoutControllerTest.cs:line 143
OK, here it goes. The following works in MVC5 with AD, I'm not sure if it's fully backwards compatible, you'll have to check.
var principalMock = new Mock<IPrincipal>();
var identityMock = new Mock<IIdentity>();
identityMock.Setup(x => x.Name).Returns("Test!");
identityMock.Setup(x => x.IsAuthenticated).Returns(true); // optional ;)
userMock.Setup(x => x.Identity).Returns(identityMock.Object);
var httpReqBase = new Mock<HttpRequestBase>(); // this is useful if you want to test Ajax request checks or cookies in the controller.
var httpContextBase = new Mock<HttpContextBase>();
httpContextBase.Setup(x => x.User).Returns(principalMock.Object);
httpContextBase.Setup(x => x.Session[It.IsAny<string>()]).Returns(1); //Here is the session indexer. You can swap 'any' string for specific string.
httpContextBase.Setup(x => x.Request).Returns(httpReqBase.Object);
This would help you to write a proper Unit Test using Moq.
[TestClass]
public class SutTest
{
[TestMethod]
public void GetCartId_WhenUserNameIsNotNull_SessionContainsUserName()
{
var httpContextStub = new Mock<HttpContextBase>();
var httpSessionStub = new Mock<ISessionSettings>();
httpSessionStub.Setup(x => x.Get<string>(It.IsAny<string>())).Returns(() => null);
httpSessionStub.SetupSequence(x => x.Get<string>(It.IsAny<string>()))
.Returns(null)
.Returns("FakeName");
var httpUserStub = new Mock<IPrincipal>();
var httpIdenttyStub = new Mock<IIdentity>();
httpUserStub.SetupGet(x => x.Identity).Returns(httpIdenttyStub.Object);
httpIdenttyStub.SetupGet(x => x.Name).Returns("FakeName");
httpContextStub.Setup(x => x.User).Returns(httpUserStub.Object);
var sut = new Sut(httpSessionStub.Object);
var result = sut.GetCartId(httpContextStub.Object);
Assert.AreEqual("FakeName",result );
}
}
Check the SetupSequence method which gives you find Control over different values being return on he same stubbed call.
Also important to decouple your session from HttpContext as you can always run into issues.
public class SessionSettings : ISessionSettings
{
private readonly HttpSessionStateBase _session;
public SessionSettings(HttpSessionStateBase session)
{
_session = session;
}
public T Get<T>(string key)
{
return (T)_session[key];
}
public void Set<T>(string key, T value)
{
_session[key] = value;
}
}
public interface ISessionSettings
{
T Get<T>(string key);
void Set<T>(string key, T value);
}
public class Sut
{
private ISessionSettings _sessionSettings;
public Sut(ISessionSettings sessionSettings)
{
_sessionSettings = sessionSettings;
}
public string GetCartId(HttpContextBase context)
{
if (_sessionSettings.Get<string>(CartSessionKey) == null)
{
if (!string.IsNullOrWhiteSpace(context.User.Identity.Name))
{
_sessionSettings.Set<string>(CartSessionKey, context.User.Identity.Name);
}
else
{
// Generate a new random GUID using System.Guid class
Guid tempCartId = Guid.NewGuid();
// Send tempCartId back to client as a cookie
_sessionSettings.Set<string>(CartSessionKey, tempCartId.ToString());
}
}
return _sessionSettings.Get<string>(CartSessionKey);
}
private string CartSessionKey = "key";
}
This way the code is more readable and easier to understand.
Using ASP.NET Web API. Is there a way to automatically return a status code 400 if a parameter is null? I found this question but that is a global solution that is applied to all methods, I want to do this on a per method per parameter basis.
So, for example, this is what I am currently doing:
public HttpResponseMessage SomeMethod(SomeNullableParameter parameter)
{
if (parameter == null)
throw new HttpResponseException(HttpStatusCode.BadRequest);
// Otherwise do more stuff.
}
I would really just like to do something like this (notice the required attribute):
public HttpResponseMessage SomeMethod([Required] SomeNullableParameter parameter)
{
// Do stuff.
}
The approach I ended up using was to create a custom filter that I registered globally. The filter checks all request parameters for the RequiredAttribute. If the attribute is found then it checks if the parameter was passed with the request (not null) and returns status code 400 if it was null. I also added a cache to the filter to store the required parameters for each request to avoid the reflection hit on future calls. I was pleasantly surprised to find that this works for value types as well since the action context stores the parameters as objects.
EDIT - Updated solution based on tecfield's comment
public class RequiredParametersFilter : ActionFilterAttribute
{
// Cache used to store the required parameters for each request based on the
// request's http method and local path.
private readonly ConcurrentDictionary<Tuple<HttpMethod, string>, List<string>> _Cache =
new ConcurrentDictionary<Tuple<HttpMethod, string>, List<string>>();
public override void OnActionExecuting(HttpActionContext actionContext)
{
// Get the request's required parameters.
List<string> requiredParameters = this.GetRequiredParameters(actionContext);
// If the required parameters are valid then continue with the request.
// Otherwise, return status code 400.
if(this.ValidateParameters(actionContext, requiredParameters))
{
base.OnActionExecuting(actionContext);
}
else
{
throw new HttpResponseException(HttpStatusCode.BadRequest);
}
}
private bool ValidateParameters(HttpActionContext actionContext, List<string> requiredParameters)
{
// If the list of required parameters is null or containst no parameters
// then there is nothing to validate.
// Return true.
if (requiredParameters == null || requiredParameters.Count == 0)
{
return true;
}
// Attempt to find at least one required parameter that is null.
bool hasNullParameter =
actionContext
.ActionArguments
.Any(a => requiredParameters.Contains(a.Key) && a.Value == null);
// If a null required paramter was found then return false.
// Otherwise, return true.
return !hasNullParameter;
}
private List<string> GetRequiredParameters(HttpActionContext actionContext)
{
// Instantiate a list of strings to store the required parameters.
List<string> result = null;
// Instantiate a tuple using the request's http method and the local path.
// This will be used to add/lookup the required parameters in the cache.
Tuple<HttpMethod, string> request =
new Tuple<HttpMethod, string>(
actionContext.Request.Method,
actionContext.Request.RequestUri.LocalPath);
// Attempt to find the required parameters in the cache.
if (!this._Cache.TryGetValue(request, out result))
{
// If the required parameters were not found in the cache then get all
// parameters decorated with the 'RequiredAttribute' from the action context.
result =
actionContext
.ActionDescriptor
.GetParameters()
.Where(p => p.GetCustomAttributes<RequiredAttribute>().Any())
.Select(p => p.ParameterName)
.ToList();
// Add the required parameters to the cache.
this._Cache.TryAdd(request, result);
}
// Return the required parameters.
return result;
}
}
Set [Required] on a property in your model and then check the ModelState to see if it IsValid.
This will allow all the required properties to be tested at the same time.
See the "Under-Posting" section # Model validation in WebAPI
we can use the BindRequired, which is from Microsoft.AspNetCore.Mvc.ModelBinding namespace.
public async Task<ActionResult<IEnumerable<Numbers>>> GetAll([BindRequired, FromQuery]string[] numbers)
{
var result = await _service.GetAllDetails(numbers);
return Ok(result);
}
after that your swagger will look like below.
A solution for asp.net core...
[AttributeUsage(AttributeTargets.Method)]
public sealed class CheckRequiredModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
var requiredParameters = context.ActionDescriptor.Parameters.Where(
p => ((ControllerParameterDescriptor)p).ParameterInfo.GetCustomAttribute<RequiredModelAttribute>() != null).Select(p => p.Name);
foreach (var argument in context.ActionArguments.Where(a => requiredParameters.Contains(a.Key, StringComparer.Ordinal)))
{
if (argument.Value == null)
{
context.ModelState.AddModelError(argument.Key, $"The argument '{argument.Key}' cannot be null.");
}
}
if (!context.ModelState.IsValid)
{
var errors = context.ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage);
context.Result = new BadRequestObjectResult(errors);
return;
}
base.OnActionExecuting(context);
}
}
[AttributeUsage(AttributeTargets.Parameter)]
public sealed class RequiredModelAttribute : Attribute
{
}
services.AddMvc(options =>
{
options.Filters.Add(typeof(CheckRequiredModelAttribute));
});
public async Task<IActionResult> CreateAsync([FromBody][RequiredModel]RequestModel request, CancellationToken cancellationToken)
{
//...
}
The accepted solution takes it upon itself to report back any errors. A more appropriate approach for MVC5 is to let the controller handle (via model validation) the reporting of any errors, aka something like this:
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using System.Web.Http.ModelBinding;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public sealed class ValidateParametersAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext context)
{
var descriptor = context.ActionDescriptor;
if (descriptor != null)
{
var modelState = context.ModelState;
foreach (var parameterDescriptor in descriptor.GetParameters())
{
EvaluateValidationAttributes(
suppliedValue: context.ActionArguments[parameterDescriptor.ParameterName],
modelState: modelState,
parameterDescriptor: parameterDescriptor
);
}
}
base.OnActionExecuting(context);
}
static private void EvaluateValidationAttributes(HttpParameterDescriptor parameterDescriptor, object suppliedValue, ModelStateDictionary modelState)
{
var parameterName = parameterDescriptor.ParameterName;
parameterDescriptor
.GetCustomAttributes<object>()
.OfType<ValidationAttribute>()
.Where(x => !x.IsValid(suppliedValue))
.ForEach(x => modelState.AddModelError(parameterName, x.FormatErrorMessage(parameterName)));
}
}
You may then plug it in universally via WebApiConfig.cs:
config.Filters.Add(new ValidateParametersAttribute());
Scenario:
Validate that my class issues a RestRequest with the provided IRestclient where certain Parameters are set on the request.
public class MyClass {
private readonly IRestClient _client;
public MyClass(IRestClient client) {
_client = client;
}
void DoIt() {
var req = new RestRequest { Method = WebMethod.Get, Path = "/DoIt" };
req.AddParameter("key1", "value1");
req.AddParameter("key2", "value2");
var resp = _client.Request(req);
}
}
[TestFixture]
public class MyClassTests {
[Test]
public void VerifyDoIt() {
var client = MockRepository.GenerateStrictMock<IRestClient>();
var resp = new RestResponse { StatusCode = HttpStatusCode.OK };
client.Expect(x => x.Request(null)).IgnoreArguments().WhenCalled(inv => {
var req = inv.Arguments[0] as RestRequest;
//NO WAY TO VALIDATE THAT PARAMETERS HAS BEEN SET!
inv.ReturnValue = resp;
});
}
}
Is Hammock simply not testable in this fashion or am I missing something crucial?
Edit: this is not a question about on how to access/validate method parameters with Rhino.Mocks, but rather how/if Hammock supports testing/validating that request parameters are set in unit-test scenarios.
If you need to validate the parameters sent to a mocked/stubbed method, you can use Rhino.Mocks' "GetArgumentsForCallsMadeOn" method. Pass it a lambda representing the call that was made and it will return you a jagged array. The first element ([0]) will be an array of arguments passed the first time the method was called. The second element ([1]) will be an array of arguments passed the second time the method was called, etc...
You could re-write your test like this to validate the request data:
var client = MockRepository.GenerateStub<IRestClient>();
var mc = new MyClass(client);
mc.DoIt();
client.AssertWasCalled(c => c.Request(null), o => o.IgnoreArguments());
var req = (RestRequest)client.GetArgumentsForCallsMadeOn(c => c.Request(null), o => o.IgnoreArguments())[0][0];
Assert.AreEqual("/DoIt", req.Path);
Assert.AreEqual(WebMethod.Get, req.Method);