Hello I need to test my APIcontroller which implements ApiController and I don't know how to do this, I know basics of UnitTesting but this is a little bit too complex for me. Also I don't know how to use automapper in unit tests, so maybe someone could help me with this
Here is my controller:
namespace Vidly.Controllers.Api
{
public class CustomersController : ApiController
{
private ApplicationDbContext _context;
public CustomersController(ApplicationDbContext _context)
{
_context = new ApplicationDbContext();
}
// GET /api/customers
public IHttpActionResult GetCustomers(string query = null)
{
var customersQuery = _context.Customers
.Include(c => c.MembershipType);
if (!String.IsNullOrWhiteSpace(query))
customersQuery = customersQuery.Where(c => c.Name.Contains(query));
var customerDtos = customersQuery
.ToList()
.Select(Mapper.Map<Customer, CustomerDto>);
return Ok(customerDtos);
}
// GET /api/customers/1
public IHttpActionResult GetCustomer(int id)
{
var customer = _context.Customers.SingleOrDefault(c => c.Id == id);
if (customer == null)
return NotFound();
return Ok(Mapper.Map<Customer, CustomerDto>(customer));
}
// POST /api/customers
[HttpPost]
public IHttpActionResult CreateCustomer(CustomerDto customerDto)
{
if (!ModelState.IsValid)
return BadRequest();
var customer = Mapper.Map<CustomerDto, Customer>(customerDto);
_context.Customers.Add(customer);
_context.SaveChanges();
customerDto.Id = customer.Id;
return Created(new Uri(Request.RequestUri + "/" + customer.Id), customerDto);
}
// PUT /api/customers/1
[HttpPut]
public IHttpActionResult UpdateCustomer(int id, CustomerDto customerDto)
{
if (!ModelState.IsValid)
return BadRequest();
var customerInDb = _context.Customers.SingleOrDefault(c => c.Id == id);
if (customerInDb == null)
return NotFound();
Mapper.Map(customerDto, customerInDb);
_context.SaveChanges();
return Ok();
}
// DELETE /api/customers/1
[HttpDelete]
public IHttpActionResult DeleteCustomer(int id)
{
var customerInDb = _context.Customers.SingleOrDefault(c => c.Id == id);
if (customerInDb == null)
return NotFound();
_context.Customers.Remove(customerInDb);
_context.SaveChanges();
return Ok();
}
}
}
So how do I need to start, do I need some kind of interface like this to mock dbcontext:
public interface IAPICustomerRepository
{
IHttpActionResult GetCustomers(string query = null);
IHttpActionResult GetCustomer(int id);
IHttpActionResult CreateCustomer(CustomerDto customerDto);
IHttpActionResult UpdateCustomer(int id, CustomerDto customerDto);
IHttpActionResult DeleteCustomer(int id);
}
Or maybe I can write Unit tests without mocking.
UPDATE After I edited my code with Nkosi's suggestion I am getting these errors
<Error>
<Message>An error has occurred.</Message>
<ExceptionMessage>
An error occurred when trying to create a controller of type 'CustomersController'. Make sure that the controller has a parameterless public constructor.
</ExceptionMessage>
<ExceptionType>System.InvalidOperationException</ExceptionType>
<StackTrace>
at System.Web.Http.Dispatcher.DefaultHttpControllerActivator.Create(HttpRequestMessage request, HttpControllerDescriptor controllerDescriptor, Type controllerType) at System.Web.Http.Controllers.HttpControllerDescriptor.CreateController(HttpRequestMessage request)
at System.Web.Http.Dispatcher.HttpControllerDispatcher.<SendAsync>d__1.MoveNext()
</StackTrace>
<InnerException>
<Message>An error has occurred.</Message>
<ExceptionMessage>
Type 'Vidly.Controllers.Api.CustomersController' does not have a default constructor
</ExceptionMessage>
<ExceptionType>System.ArgumentException</ExceptionType>
<StackTrace>
at System.Linq.Expressions.Expression.New(Type type)
at System.Web.Http.Internal.TypeActivator.Create[TBase](Type instanceType)
at System.Web.Http.Dispatcher.DefaultHttpControllerActivator.GetInstanceOrActivator(HttpRequestMessage request, Type controllerType, Func`1& activator)
at System.Web.Http.Dispatcher.DefaultHttpControllerActivator.Create(HttpRequestMessage request, HttpControllerDescriptor controllerDescriptor, Type controllerType)
</StackTrace>
</InnerException>
</Error>
Then I create a default constructor (without parameters as I understand) and then I get another error:
<Error>
<Message>An error has occurred.</Message>
<ExceptionMessage>
Object reference not set to an instance of an object.
</ExceptionMessage>
<ExceptionType>System.NullReferenceException</ExceptionType>
<StackTrace>
at Vidly.Controllers.Api.CustomersController.GetCustomers(String query) in C:\Users\Dovydas Petrutis\Desktop\vidly-mvc-5-master\Vidly\Controllers\Api\CustomersController.cs:line 26
at lambda_method(Closure , Object , Object[] )
at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.<>c__DisplayClass10.<GetExecutor>b__9(Object instance, Object[]
methodParameters) at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.Execute(Object instance, Object[] arguments)
at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ExecuteAsync(HttpControllerContext controllerContext, IDictionary`2 arguments, CancellationToken cancellationToken) --- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Web.Http.Controllers.ApiControllerActionInvoker.<InvokeActionAsyncCore>d__0.MoveNext() --- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Web.Http.Controllers.ActionFilterResult.<ExecuteAsync>d__2.MoveNext() --- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Web.Http.Dispatcher.HttpControllerDispatcher <SendAsync>d__1.MoveNext()
</StackTrace>
</Error>
Where could be the problem now?
First create a service that exposes the functionality you want so that it can be easily replaced/mocked for testing and improve maintainability.
You were close when you mentioned the repository. IHttpActionResult is a UI concern so you can refactor your interface like this.
public interface IAPICustomerRepository {
IEnumerable<CustomerDto> GetCustomers(string query = null);
CustomerDto GetCustomer(int id);
int CreateCustomer(CustomerDto customerDto);
CustomerDto UpdateCustomer(int id, CustomerDto customerDto);
bool? DeleteCustomer(int id);
}
The controller will now be slimmer in terms of functionality and no longer cares about EF/DbContext or Automapper.
public class CustomersController : ApiController {
private IAPICustomerRepository repository;
public CustomersController(IAPICustomerRepository repository) {
this.repository = repository;
}
// GET /api/customers
public IHttpActionResult GetCustomers(string query = null) {
var customerDtos = repository.GetCustomers(query);
return Ok(customerDtos);
}
// GET /api/customers/1
public IHttpActionResult GetCustomer(int id) {
var customer = repository.GetCustomer(id);
if (customer == null)
return NotFound();
return Ok(customer);
}
// POST /api/customers
[HttpPost]
public IHttpActionResult CreateCustomer([FromBody]CustomerDto customerDto) {
if (!ModelState.IsValid)
return BadRequest();
var id = repository.CreateCustomer(customerDto);
customerDto.Id = id;
return Created(new Uri(Request.RequestUri + "/" + id), customerDto);
}
// PUT /api/customers/1
[HttpPut]
public IHttpActionResult UpdateCustomer(int id, [FromBody]CustomerDto customerDto) {
if (!ModelState.IsValid)
return BadRequest();
var updated = repository.UpdateCustomer(id, customerDto);
if (updated == null)
return NotFound();
return Ok();
}
// DELETE /api/customers/1
[HttpDelete]
public IHttpActionResult DeleteCustomer(int id) {
var deleted = repository.DeleteCustomer(id);
if (deleted == null)
return NotFound();
return Ok();
}
}
With the controller now dependent on an abstraction you can mock the functionality when testing the controller in isolation.
Automapper is an implementation concern that can be encapsulated behind the abstraction so that it is a non issue when testing.
The following example uses Moq mocking framework. You can use your framework of choice is you so choose.
[TestClass]
public class CustomersController_Should {
[TestMethod]
public void GetCustomers() {
//Arrange
var fakeCustomers = new List<CustomerDto>{
new CustomerDto{ Id = 1 }
};
var repository = new Mock<IAPICustomerRepository>();
repository
.Setup(_ => _.GetCustomers(It.IsAny<string>()))
.Returns(fakeCustomers)
.Verifiable();
var controller = new CustomersController(repository.Object);
//Act
var result = controller.GetCustomers();
//Assert
repository.Verify();
//..other assertions
}
//...Other tests
}
The functionality that was originally in the controller would be encapsulated in the implementation of the repository in production.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With