I'm trying to carry out some unit tests on my API Controller however I'm having issues since it is using IHttpActionResult and my actual data is called from a database (Azure).
Here's a sample of a simple Get by id method
// GET: api/RoundOnesApi/5
[ResponseType(typeof(RoundOne))]
public IHttpActionResult GetRoundOne(int id)
{
RoundOne roundOne = db.RoundOnes.Find(id);
if (roundOne == null)
{
return NotFound();
}
return Ok(roundOne);
}
I then tried to make a unit test to test this.
public void TestMethod1()
{
//ApplicationDbContext db = new ApplicationDbContext();
//arrange
var controller = new RoundOnesApiController();
//act
var actionResult = controller.GetRoundOne(1); //1
//assert
var response = actionResult as OkNegotiatedContentResult<RoundOne>; //RoundOne is my model class
Assert.IsNotNull(response);
Assert.AreEqual(1, response.Content.Id);
GetRoundOne(1) contains a database entry of a football teams information. Since this is not null I assumed it would pass.
By the way I'm just looking to do a general Unit Test to see if GetRoundOne(1) can be tested against actually existing. Once it passes that's all I need.
Exception:
Microsoft.VisualStudio.TestTools.UnitTesting.AssertFailedException was unhandled by user code
HResult=-2146233088
Message=Assert.IsNotNull failed.
Source=Microsoft.VisualStudio.QualityTools.UnitTestFramework
StackTrace:
at Microsoft.VisualStudio.TestTools.UnitTesting.Assert.HandleFailure(String assertionName, String message)
at Microsoft.VisualStudio.TestTools.UnitTesting.Assert.IsNotNull(Object value)
at MyUnitTestProject.UnitTest1.TestMethod1() in c:\Users\Daniel\Documents\Visual Studio 2013\Projects\UefaServiceV9\MyUnitTestProject\UnitTest1.cs:line 46
InnerException:
Related
I'm trying to consume my asp.net web api in my asp.net core mvc web app which are on the same solution. I configured the solution for multi-project start and they start both.
next I tried to consume the API in the Web part but I'm getting the following error.
InvalidOperationException: A suitable constructor for type 'ProjectName.Web.Services.Interfaces.IAdminService' could not be located. Ensure the type is concrete and all parameters of a public constructor are either registered as services or passed as arguments. Also ensure no extraneous arguments are provided.
Microsoft.Extensions.DependencyInjection.ActivatorUtilities.FindApplicableConstructor(Type instanceType, Type[] argumentTypes, out ConstructorInfo matchingConstructor, out Nullable[] matchingParameterMap)
Here is the complete Stack trace
The Projects are structure like this
SolutionName:
Name.API
Name.Web
each with its own respective structure
This is my Helper Class
public static class HttpClientExtensions
{
public static async Task<T> ReadContentAsync<T>(this HttpResponseMessage response)
{
//if (response.IsSuccessStatusCode == false) return StatusCodes = 300;
//throw new ApplicationException($"Something went wrong calling the API: {response.ReasonPhrase}");
var dataAsString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
var result = JsonSerializer.Deserialize<T>(
dataAsString, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
return result;
}
}
The IAdmin Inter Face
Task<IEnumerable<Admins>> GetAllAdmins();
The AdminService(Implementation)
private readonly HttpClient _client;
public const string BasePath = "api/Admins";
public AdminService(HttpClient client)
{
_client = client; // ?? throw new ArgumentNullException(nameof(client));
}
public async Task<IEnumerable<Admins>> GetAllAdmins()
{
var response = await _client.GetAsync(BasePath);
return await response.ReadContentAsync<List<Admins>>();
}
Admins Controller
private readonly IAdminService _adminService;
public AdminController(IAdminService adminService)
{
_adminService = adminService;
}
public async Task<IActionResult> Index()
{
var adminsList = await _adminService.GetAllAdmins();
if(adminsList == null)
{
return new JsonResult("There are now Admins");
}
return View(adminsList);
}
Program.cs
builder.Services.AddControllersWithViews();
builder.Services.AddHttpClient<IAdminService, IAdminService>(c =>
c.BaseAddress = new Uri("https://localhost:<port-Num>/"));
var app = builder.Build();
What Could I be doing wrong???
I'm using .NET 6 adn both Projects are in the same solution
NB My end points are working fine, I test them using Postman.
It is failing because DI cannot instantiate your AdminService with parameterized constructor. This is possibly a duplicate of Combining DI with constructor parameters? .
Essentially, you should avoid parameterized constructor injection where possible. Either control it through configuration or have the configuration loaded through common infrastructure such as host configuration.
According to your codes, I found you put two interface inside the AddHttpClient method which caused the issue.
I suggest you could modify it like this and then it will work well.
builder.Services.AddHttpClient<IAdminService, AdminService>(c =>
c.BaseAddress = new Uri("https://localhost:3333/"));
I'm testing a MVC ASP.Net Web Application and using NUnit to test its controllers. I test the Create method in the controllers and there's a step that it save changes to a stored local database. But NUnit always skips the save changes step. Is there anyway can solve it?
This is the method I coded in the controllers
[HttpPost]
public ActionResult Create([Bind(Include = "CourseId,CourseName,CoursCategoryId,Credit")] COURSE course)
{
try
{
if (ModelState.IsValid)
{
db.COURSEs.Add(course);
db.SaveChanges();
return RedirectToAction("Index");
}
ViewBag.CourseCategagoryId = new SelectList(db.CourseCategorys, "CourseCategoryId", "CourseCategoryId", course.CourseCategoryId);
return View(course);
}
catch(Exception e)
{
return RedirectToAction("Create", "COURSEs", new { id = 1 });
}
}
And this is the test method I used to test that method
[Test]
[TestCase("IT01", "Computer Science", "PRA", 4, "Index")]
public void AddCourseTest(string CourseId,
string CourseName, string CourseCategoryId, int Credit, string expected)
{
COURSE course = new COURSE
{
CourseCategoryId= CourseCategoryId,
CourseName= CourseName,
CourseCategoryId= CourseCategoryId,
Credit= Credit
};
COURSEsController = new COURSEsController ();
RedirectToRouteResult result = COURSEsController .Create(course ) as RedirectToRouteResult;
Assert.AreEqual(expected, result.RouteValues["action"].ToString());
}
As expected that the course I'll add hasn't existed in our database, so the result will redirect to "Index" page. But when I debug the test method, It seems that when it runs to the db.SaveChanges(), it threw exceptions and always return to "Create" page.
I am trying to unit test a controller that returns an IActionResult but can also throw an exception in certain circumstances. The problem I am running into is I'm not sure how to call it as the Assert throws an error where you cannot convert from IActionResult to Action.
How would I go about testing below statement?
Assert.Throws<Exception>(await controller.SendEmail(email)); //how to test this
I looked through the Microsoft testing controller documentation and didn't find something relevant. Most examples I see testing exceptions are for things like accessing repositories or services.
I understand I can return a badrequest or redirect to the page with an error message. But is what I am trying to accomplish possible?
My HomeController Method
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> SendEmail(EmailModel emailModel)
{
if(!ModelState.IsValid)
{
return View("Index",emailModel);
}
var response = await _sendEmail.SendEmailMessage(emailModel);
if (response != System.Net.HttpStatusCode.Accepted)
{
throw new Exception("Email failed to send, please try again later");
}
else
{
TempData["message"] = $"Email has been sent";
}
return RedirectToAction("Index");
}
XUnit HomeControllerTest Constructor for arrange
private Mock<ISendEmail> mockSendEmail;
private HomeController controller;
public HomeControllerShould()
{
mockSendEmail = new Mock<ISendEmail>();
mockSendEmail.Setup(x => x.SendEmailMessage(It.IsAny<EmailModel>())).ReturnsAsync(HttpStatusCode.Accepted);
controller = new HomeController(mockSendEmail.Object);
}
XUnit Test for Sending Email
[Fact]
public async Task SendEmailActionThrowsExceptionOnEmailFailure()
{
mockSendEmail.Setup(x => x.SendEmailMessage(It.IsAny<EmailModel>())).ReturnsAsync(HttpStatusCode.InternalServerError);
var email = new EmailModel();
Assert.Throws<Exception>(await controller.SendEmail(email)); //how to test this
}
Assert.Throws requires a function. You could use ThrowsAsync.
[Fact]
public async Task SendEmailActionThrowsExceptionOnEmailFailure()
{
mockSendEmail.Setup(x => x.SendEmailMessage(It.IsAny<EmailModel>()))
.ReturnsAsync(HttpStatusCode.InternalServerError);
var email = new EmailModel();
await Assert.ThrowsAsync<Exception>(() => controller.SendEmail(email));
}
FYI: We don't normally return HttpStatusCode from service layer such as email service, but I'll let you decide.
I have a simple form to save and then use MailKit to provide email notification, with xUnit and Moq used for unit testing. I'm having difficulty setting up the unit test and associated services. I have a workaround ('if' statement in the action method) to only test the core repo saving functionality without also testing the email service. If I take out the if statement, the unit test does not have access to the appropriate methods, such as setting the web root path. The error is a null exception. If I default this value, there are other errors, such as "no database provider being configured for DbContext."
Is there a more appropriate way to set a unit test of this sort up? Or is it wrong to set up a unit test to test both the Create() and email functionality because it violates the one-function unit testing rule?
Unit test:
[Fact]
public void Can_Create_New_Lesson()
{
//Arrange
//create a mock repository
Mock<IHostingEnvironment> mockEnv = new Mock<IHostingEnvironment>();
Mock<ILessonRepository> mockRepo = new Mock<ILessonRepository>();
Mock<UserManager<AppUser>> mockUsrMgr = GetMockUserManager();
Mock<RoleManager<IdentityRole>> mockRoleMgr = GetMockRoleManager();
var opts = new DbContextOptions<AppIdentityDbContext>();
Mock <AppIdentityDbContext> mockCtx = new Mock<AppIdentityDbContext>(opts);
//create mock temporary data
Mock<ITempDataDictionary> tempData = new Mock<ITempDataDictionary>();
//create the controller
LessonController target = new LessonController(mockRepo.Object, mockEnv.Object, mockUsrMgr.Object, mockRoleMgr.Object, mockCtx.Object)
{
TempData = tempData.Object
};
//create a lesson
Lesson lesson = new Lesson { Title = "Unit Test", Domain= "Unit Test"};
//Act
//try to save the product using the Create method of the controller
IActionResult result = target.Create(lesson);
//Assert
//check that the repository was called
mockRepo.Verify(m => m.SaveLesson(lesson));
//check the result type is a redirection to the List action method of the controller
Assert.IsType<RedirectToActionResult>(result);
Assert.Equal("Success", (result as RedirectToActionResult).ActionName);
}
The Create() action method:
[HttpPost]
public IActionResult Create(Lesson lesson)
{
if (ModelState.IsValid)
{
repository.SaveLesson(lesson);
//This IF statement is a workaround for the unit test
//don't email users if the Title is "Unit Test"
if (lesson.Title != "Unit Test")
{
emailUsers(lesson);
}
TempData["message"] = $"{lesson.Title} has been saved";
//show the user that the update was made successfully
return RedirectToAction("Success");
}
else
{
//there is a problem with the data values
return View(lesson);
}
}
Email function:
public void emailUsers(Lesson lesson)
{
var webRoot = environment.WebRootPath;
var filePath = System.IO.Path.Combine(webRoot, "email\\NewLessonSubmitted.txt");
string message = System.IO.File.ReadAllText(filePath);
string domain = lesson.Domain;
IQueryable<AppUser> userList = GetUsersInRole(identityContext, domain);
//if there are users in that domain, send the email
if (userList != null)
{
foreach (AppUser user in userList)
{
sendEmail(domain, message, user.Email);
}
}
}
EDIT: I've instead implemented the email service as a class, as pointed out by MotoSV. However, I'm still getting an error for "No database provider has been configured for this DbContext" The stack trace for the exception points to the following method:
public static IQueryable<AppUser> GetUsersInRole(AppIdentityDbContext db, string roleName)
{
if (db != null && roleName != null)
{
var roles = db.Roles.Where(r => r.Name == roleName);
if (roles.Any())
{
var roleId = roles.First().Id;
return from user in db.Users
where user.Roles.Any(r => r.RoleId == roleId)
select user;
}
}
return null;
}
I have this constructor in my dbContext class:
public AppIdentityDbContext(DbContextOptions<AppIdentityDbContext> options)
: base(options) { }
EDIT: The solution (provided by MotoSV) was to:
1) Create an email service class with appropriate methods and
2) Install the appropriate Nuget package for Microsoft.EntityFrameworkCore.InMemory
3) mock the DbContext as:
var opts = new DbContextOptionsBuilder<AppIdentityDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
Mock<AppIdentityDbContext> mockCtx = new Mock<AppIdentityDbContext>(opts);
I would look at moving the code responsible for sending emails into it's own class. This class will implement an interface that can then be mocked in your test.
For example, create the interface and implementation:
public interface IEmailService
{
void SendEmail(string to, string from, string body);
}
public class EmailService : IEmailService
{
public void SendEmail(string to, string from string body)
{
...
}
}
The EmailService class will hold the functionality required to talk to MailKit. Then, register the IEmailService with .NET Core and add it to the constructor of your class:
public class LessonController : Controller
{
private readonly IEmailService _emailService;
public LessonController(IEmailService service, ...)
{
_emailService = emailService;
}
public void emailUsers(Lessong lesson)
{
...
if(userList != null)
{
foreach(...)
{
_emailService.Send(...);
}
}
...
}
}
In your test create a mock and pass that into your constructor.
First and foremost, you should never do stuff like putting in conditionals in your code for the purpose of unit testing. If for no other reason, you're violating the entire point of unit testing, as your test access different code paths than what your users actually experience; you learn nothing by doing this.
Testing that the repo actually saves is a job for a repo test not an action test. Likewise with your mail service: ensuring that an email is actually sent should be a test on your mail service, not your action method.
Long and short, your test here should simply ensure that the appropriate actions are taken (i.e. repo save is hit and email service send is hit). As such, you can drop in simple mocks that merely have those methods available to be hit. You don't need to (and shouldn't) be establishing full connections to the DB/SMTP server, as at that point you're integration testing, not unit testing.
Your applications send email class constructor should take an "email provider" object that is a generic email abstraction based on an IEmailProvider interface, and/or also take a IDataAccessProvider implementation.
Now you can mock both of these interfaces in the test and pass them to the send email class to test just your implementation.
From JavaScript client code I am creating the following data:
var employee = {
FirstName: "Rudolf",
Salary: 99
};
I then pass this through an Ajax call to an MVC Web API Controller Action:
using System.Web.Http.OData;
public async Task<IHttpActionResult> Patch([FromUri] int employeeId, [FromBody] Delta<Employee> employee)
{
await _employeeService.Patch(employeeId, employee);
return Ok();
}
This calls my service to update the database as follows:
public async Task Patch(int employeeId, Delta<Employee> employee)
{
using (var context = new DBEntities())
{
if (employee.TryGetPropertyValue("Salary", out object salary))
{
var ss = Convert.ToDouble(salary); // Always 0
}
if (employee.TryGetPropertyValue("FirstName", out object firstName))
{
var ss = Convert.ToString(firstName); // Expected value
}
var currentEmployee = await context.Employees
.FirstOrDefaultAsync(e => e.Id == employeeId);
if (currentEmployee == null)
return;
employee.Patch(currentEmployee);
await context.SaveChangesAsync();
}
}
Note: I missed out some of the details for brevity as the actual client-server call is working fine.
The code seems to work as expected, but the Salary property (the only none-string one) is always set to 0 (zero). So that field never get's updated.
Any ideas why the Salary is not being passed through?
Note: I use very similar client-server code for GET/POST/PUT/DELETE and they all work fine, so I believe it is related to the Delta<> part.
Yes, I encountered the same problem with int properties.
I solved the problem using SimplePatch (v1.0 is only 10KB).
Disclaimer: I'm the author of the project.
It is inspired to Microsoft.AspNet.WebApi.OData but SimplePatch has no dependencies.
How to use
Install the package using:
Install-Package SimplePatch
Your MVC Web API Controller Action becomes:
using SimplePatch;
public async Task<IHttpActionResult> Patch([FromUri] int employeeId, [FromBody] Delta<Employee> employee)
{
await _employeeService.Patch(employeeId, employee);
return Ok();
}
Then your Patch method becomes:
public async Task Patch(int employeeId, Delta<Employee> employee)
{
using (var context = new DBEntities())
{
if (employee.TryGetPropertyValue("Salary", out object salary))
{
var ss = Convert.ToDouble(salary);
}
if (employee.TryGetPropertyValue("FirstName", out object firstName))
{
var ss = Convert.ToString(firstName);
}
var currentEmployee = await context.Employees
.FirstOrDefaultAsync(e => e.Id == employeeId);
if (currentEmployee == null)
return;
employee.Patch(currentEmployee);
await context.SaveChangesAsync();
}
}
Also, SimplePatch gives you the ability to ignore some properties when calling the Patch method.
Global.asax or Startup.cs
DeltaConfig.Init((cfg) =>
{
cfg.ExcludeProperties<Employee>(x => x.YourPropertyName);
});
If Salary has type int then there is an issue with that type
The Json parser converts integers to 64 bit ones, which won't match the 32 bit int properties declared on the entity and thus get ignored by this method.
This problem is mentioned here
So, you can use long instead of int or using the OData media type formatters
See this