I try to test Update method using xUnit and I don't know how to do it, below is my code:
Put method in controller:
[HttpPut]
[Route("{id}")]
public IActionResult Put([FromBody]BookDto book, [FromRoute]int id)
{
if (!ModelState.IsValid)
{
return BadRequest();
}
var isUpdated = _service.Update(book);
if (!isUpdated)
{
return NotFound();
}
return Ok();
}
BookService update method:
public bool Update(BookDto book)
{
var bookDb = _context.Books.FirstOrDefault(x => x.Id == book.Id);
if(bookDb == null)
{
return false;
}
bookDb.Title = book.Title;
_context.SaveChanges();
var existingAuthors = _context.Book_Authors.Where(x => x.BookId == book.Id).ToList();
_context.Book_Authors.RemoveRange(existingAuthors);
_context.SaveChanges();
foreach (var AuthorsId in book.AuthorsId)
{
var newBookAuthors = new Book_Author()
{
BookId = book.Id,
AuthorId = AuthorsId
};
_context.Book_Authors.Add(newBookAuthors);
}
_context.SaveChanges();
return true;
}
BookDto:
public class BookDto
{
public int Id { get; set; }
public string Title { get; set; }
public List<int> AuthorsId { get; set; }
}
Any suggestions how to write PutMethod test using Moq?
I would suggest to collect the test cases:
Given an invalid book When I call put Then It returns bad request
Given a non-existing book When I call put Then It returns not Found
Given an existing book When I call put Then It returns ok
I would suggest to AAA pattern for each test case, for example:
Arrange: Setup a book service mock which returns false whenever someone calls the update
Act: Call your controller's put method
Assert: Verify that the returned result is not found as expected
Try to codify. For example
public void GivenANonExistingBook_WhenICallPut_ThenItReturnsNotFound()
{
//Arrange
var book = ...;
var serviceMock = new Mock<IBookService>();
serviceMock.Setup(svc => svc.Update(It.IsAny<BookDto>()))
.Returns(false);
var sut = new YourController(serviceMock.Object);
//Act
var result = sut.Put(book, 0);
//Assert
var actionResult = result as NotFoundResult;
Assert.IsNotNull(actionResult);
}
It is also a good practice to verify that the Update method of the service has been called only once with the book variable: serviceMock.Verify(svc => svc.Update(book), Times.Once);.
Related
I am trying to run unit tests using Moq. I am fairly new to Moq and I have ran into a problem with this current unit test.
I have a controller that is fetching some items. The result is encapsulated within a generic interface using ICollection.
public interface IResult
{
}
public interface IListResult : ICollection<IResult>
{
}
My controller is simply calling a method that returns the result.
[HttpGet("get/{userId}/{pageSize}/{fetchNext}")]
public IActionResult GetConversations(int userId, int pageSize, bool fetchNext)
{
try
{
GetConversationsByUserIdQuery query = new GetConversationsByUserIdQuery()
{
UserId = userId,
PageSize = pageSize,
FetchNext = fetchNext
};
var result = _mediator.GetConversationsByUserId(query);
return Ok(result);
}
catch (Exception ex)
{
return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
}
}
}
When I am running my unit test, the result always comes back null and I am failing to understand what I am missing in my set up.
[Theory]
[InlineData(1, 2 , false)]
[InlineData(1, 2 , true)]
public async void When_AddMessageToNewConversation_ThenSuccessfullyAdd(int userId, int pageSize, bool fetchNext)
{
// Arrange
Mock<IMessageMediator> _mediator = new Mock<IMessageMediator>();
Mock<IListResult> _listResult = new Mock<IListResult>();
GetConversationsByUserIdQuery query = new GetConversationsByUserIdQuery()
{
UserId = userId,
PageSize = pageSize,
FetchNext = fetchNext
};
List<Conversation> expected = new List<Conversation>()
{
new Conversation()
{
Id = Guid.NewGuid(),
Created_At = DateTimeOffset.Now
}
};
_listResult.Setup(x =>
x.GetEnumerator())
.Returns(expected.GetEnumerator());
GetConversationsByUserIdController controller = new GetConversationsByUserIdController(_mediator.Object);
_mediator.Setup(x =>
x.GetConversationsByUserId(query)).Returns(_listResult.Object);
// Act
IActionResult result = controller.GetConversations(userId, pageSize, fetchNext);
// Assert
Assert.True(result is OkResult);
}
The IMessageMediator is simply a delegator that handles implementation of queriers and commands. The IListResult is returned from a querier handler.
public class GetConversationsByUserIdQueryHandler:
IQueryHandler<GetConversationsByUserIdQuery>
{
private IRepository<Conversations_By_UserId>
_conversationByUserIdRepository;
private IListResult _result;
public GetConversationsByUserIdQueryHandler(IRepository<Conversations_By_UserId> conversationByUserIdRepository,
IListResult result)
{
_conversationByUserIdRepository = conversationByUserIdRepository;
_result = result;
}
public IListResult Handle(GetConversationsByUserIdQuery query)
{
IEnumerable<Conversations_By_UserId> conversations = _conversationByUserIdRepository
.Get(query.PageSize,
query.FetchNext,
message => message.UserId == query.UserId).ToList();
if (conversations.Any())
{
foreach (Conversations_By_UserId m in conversations)
{
_result.Add(new Conversation()
{
Created_At = m.Created_At,
Id = m.Id,
});
}
}
return _result;
}
}
You don't need to mock both IMessageMediator and IListResult.
You should mock dependencies - it is IMessageMediator in your case - and your can setup it to return any result.
The other thing - you mock _mediator using query created in test, but in controller you create different query (references are different) and it is the reason of null result.
Change your test:
[Theory]
[InlineData(1, 2 , false)]
[InlineData(1, 2 , true)]
public async void When_AddMessageToNewConversation_ThenSuccessfullyAdd(int userId, int pageSize, bool fetchNext)
{
// Arrange
Mock<IMessageMediator> _mediator = new Mock<IMessageMediator>();
List<Conversation> expected = new List<Conversation>()
{
new Conversation()
{
Id = Guid.NewGuid(),
Created_At = DateTimeOffset.Now
}
};
GetConversationsByUserIdController controller = new GetConversationsByUserIdController(_mediator.Object);
// It.IsAny<GetConversationsByUserIdQuery>()) instead of query
_mediator.Setup(x => x.GetConversationsByUserId(It.IsAny<GetConversationsByUserIdQuery>()))
// create fake result to return from mediator (no need to mock IListResult
.Returns(new ListResult() ...); // just create instance of IListResult with needed values
// Act
IActionResult result = controller.GetConversations(userId, pageSize, fetchNext);
// Assert
Assert.True(result is OkResult);
}
Presentation layer call a method (CreateEvent) in my application layer. This method use generic parameters :
public async Task<string> CreateEvent<T, TDocument>(T #event)
where T : class
where TDocument : Document
{
using (var scope = _serviceProvider.CreateScope())
{
var myRepository = scope.ServiceProvider.GetRequiredService<ICarrierEventRepository<TDocument>>();
var eventMapped = _mapper.Map<TDocument>(#event);
await myRepository.InsertOneAsync(eventMapped);
return eventMapped.Id.ToString();
}
}
Parameter T is object define in presentation layer and TDocument is abstract class that my entities (Domain layer) inherit.
public abstract class Document : IDocument
{
public ObjectId Id { get ; set ; }
//some other properties....
}
Example of entity :
public class PaackCollection : Document
{
public string ExternalId { get; set; }
public DateTime At { get; set; }
//some other properties....
}
In presentation layer, I call my CreateEvent method like this :
[HttpPost]
public async Task<IActionResult> Post(PayLoadPaackModel payLoadPaackModel)
{
var idCreated = await _carrierEventService.CreateEvent<PayLoadPaackModel, Domain.Entities.MongoDb.PaackCollection>(payLoadPaackModel);
//some code here....
return Ok("OK");
}
It's possible to use type of Domain.Entities.MongoDb.PaackCollection as parameter knowing that it belongs to the domain layer ? Normally presentation layer communicate only with application layer.
Thanks for advices
UPDATE
This solution works :
Call CreateEvent :
await _carrierEventService.CreateEvent(paackEventMapped);
public async Task<string> CreateEvent<T>(T #event)
where T : class
{
using (var scope = _serviceProvider.CreateScope())
{
Type typeParameterType = typeof(T);
if (typeParameterType.Equals(typeof(PaackEventDto)))
{
var eventMapped = _mapper.Map<PaackEvent>(#event);
var carrierEventRepository = scope.ServiceProvider.GetRequiredService<ICarrierEventRepository<PaackEvent>>();
await carrierEventRepository.InsertOneAsync(eventMapped);
return eventMapped.Id.ToString();
}
else if (typeParameterType.Equals(typeof(LaPosteEventDto)))
{
var eventMapped = _mapper.Map<LaposteEvent>(#event);
var carrierEventRepository = scope.ServiceProvider.GetRequiredService<ICarrierEventRepository<LaposteEvent>>();
await carrierEventRepository.InsertOneAsync(eventMapped);
return eventMapped.Id.ToString();
}
else
return default;
}
}
Is there another solution to use generic for avoid to have lot of condition to compare object ? Because I can have 50 differents objects...
UPDATE
I found solution to get the DestinationType for mapper :
var destinationMap = _mapper.ConfigurationProvider.GetAllTypeMaps().First(t => t.SourceType == typeParameterType);
var destType = destinationMap.DestinationType;
var eventMapped = _mapper.Map(#event, typeParameterType, destType);
It's working, now how I can get type of carrierEventRepository with destType ?
I try this var repository = typeof(ICarrierEventRepository<>).MakeGenericType(destType); but I can use method of my repository...
Here is another example where I am passing a Dto to my Api base class.
public async Task<ServiceResponse<TServiceResponce>> CreateAsyncServiceWrapper<TServiceResponce, TModelToCreate>(string url, TModelToCreate ModelToCreate)
{ Removed Code}
I am calling it like this
_serviceResponce = await _compRepo.CreateAsyncServiceWrapper<ServiceResponse<CompanyDto>, CreateCompanyDto>(StaticDetails.CompanyAPIPath, model);
Here is an example from one of my blogs.
/// <summary>
/// Create a new company Record.
/// </summary>
/// <param name="createCompanyDto"></param>
/// <returns></returns>
[HttpPost]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(CompanyDto))]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<CompanyDto>> CreateCompany([FromBody] CreateCompanyDto createCompanyDto)
{
if (createCompanyDto == null)
{
return BadRequest(ModelState);
}
if (!ModelState.IsValid) { return BadRequest(ModelState); }
var _newCompany = await _companyService.AddCompanyAsync(createCompanyDto);
if (_newCompany.Success == false && _newCompany.Message == "Exist")
{
return Ok(_newCompany);
}
if (_newCompany.Success == false && _newCompany.Message == "RepoError")
{
ModelState.AddModelError("", $"Some thing went wrong in respository layer when adding company {createCompanyDto}");
return StatusCode(500, ModelState);
}
if (_newCompany.Success == false && _newCompany.Message == "Error")
{
ModelState.AddModelError("", $"Some thing went wrong in service layer when adding company {createCompanyDto}");
return StatusCode(500, ModelState);
}
//Return new company created
return CreatedAtRoute("GetCompanyByGUID", new { CompanyGUID = _newCompany.Data.GUID }, _newCompany);
}
I finally found solution :
To get destination type with Automapper, I use _mapper.ConfigurationProvider.GetAllTypeMaps(), MakeGenericType help me to have my ICarrierEventRepository<T> and with this Post help me to use dynamic keyword for call method InsertOneAsync.
public async Task<string> CreateEvent<T>(T #event)
where T : class
{
using (var scope = _serviceProvider.CreateScope())
{
//Get destination type
Type typeParameterType = typeof(T);
var destinationMap = _mapper.ConfigurationProvider.GetAllTypeMaps().First(t => t.SourceType == typeParameterType);
var destType = destinationMap.DestinationType;
//Map with destination type
var eventMapped = _mapper.Map(#event, typeParameterType, destType);
//Get repository register in services
var repository = typeof(ICarrierEventRepository<>).MakeGenericType(destType);
dynamic repo = scope.ServiceProvider.GetRequiredService(repository);
//Insert on database
await repo.InsertOneAsync((dynamic)eventMapped);
//Get generate id
return ((dynamic)eventMapped).Id.ToString();
}
}
I am new to Moq.
How do I assert a generic type?
The type error occurs on this line of the test
Assert.True(ServiceResponse<string>.Equals(result));
with error
Cannot access non-static method 'Equal' in static context
Full code is below
Controller
private readonly IAuthRepository _authRepo;
public AuthController(IAuthRepository authRepo)
{
_authRepo = authRepo;
}
[HttpPost("Register")]
public async Task<ActionResult<ServiceResponse<int>>> Register(AddUserDtos user)
{
var response = await _authRepo.Register(user);
if (!response.Success)
{
return BadRequest(response);
}
return Ok(response);
}
ServiceResponse
public class ServiceResponse<T>
{
public T Data { get; set; }
public bool Success { get; set; } = true;
public string Message { get; set; } = null;
}
Test
public class UnitTest1
{
public Mock<IAuthRepository> repositoryStub = new Mock<IAuthRepository>();
[Fact]
public async Task UnitOfWork_StateUnderTest_ExpectedBehaviour()
{
// Arrange
repositoryStub.Setup(repo =>
repo.Register(It.IsAny<AddUserDtos>()))
.ReturnsAsync((ServiceResponse<string>) null);
var controller = new AuthController(repositoryStub.Object);
AddUserDtos newUser = new AddUserDtosModel()
{Company = "Test", Email = "email", FirstName = "", LastName = "", Password = "344343", Phone = "test"};
// Act
var result = await controller.Register(newUser);
// Assert
Assert.True(ServiceResponse<string>.Equals(result));
}
}
First, let's amend the Arrange phase.
Rather than casting a null to ServiceResponse<int>, let's create a new instance:
// Arrange
ServiceResponse<int> response = new ServiceResponse<int>();
repositoryStub
.Setup(repo =>repo.Register(It.IsAny<AddUserDtos>()))
.ReturnsAsync(response);
Then you have several options to make sure that the returned object has a specific type.
as operator and null checks
You can simply try to cast the returned value to a specific type then check whether or it was successful:
var okResult = result as OkObjectResult;
Assert.NotNull(okResult);
var resultValue = okResult.Value as ServiceResponse<int>;
Assert.NotNull(resultValue);
IsAssignableFrom
Here all you need to do is call the IsAssignableFrom<T> with the proper T
var okResult = Assert.IsAssignableFrom<OkObjectResult>result;
_ = Assert.IsAssignableFrom<ServiceResponse<int>>(okResult.Value);
GetType and GetGenericTypeDefinition
If you need to check the container class and the generic type separately
then you can do that as well:
var okResult = ...;
var resultValue = okResult.Value;
Assert.NotNull(resultValue);
Assert.Equal(typeof(ServiceResponse<>), resultValue.GetType().GetGenericTypeDefinition());
Assert.IsType<int>(resultValue.Data);
I have the following setup:
DbContext:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public virtual DbSet<Album> Album { get; set; }
public ApplicationDbContext()
: base("DefaultConnection", throwIfV1Schema: false)
{
}
public static ApplicationDbContext Create()
{
return new ApplicationDbContext();
}
}
Model:
public class Album
{
public int AlbumID { get; set; }
[StringLength(150)]
public string Title { get; set; }
}
Controller:
public class AlbumController : Controller
{
ApplicationDbContext db = new ApplicationDbContext();
public AlbumController(ApplicationDbContext injectDb)
{
db = injectDb;
}
// POST: Albums/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
[Authorize(Roles = "Admin")]
public ActionResult DeleteConfirmed(int id)
{
Album album = db.Album.Find(id);
db.Album.Remove(album);
db.SaveChanges();
return RedirectToAction("Index");
}
}
I wrote the unit test using Moq and xUnit to check DeleteConfirmed functionality:
public class AlbumsControllerTests
{
public static Mock<DbSet<T>> MockDbSet<T>(List<T> inputDbSetContent) where T : class
{
var DbSetContent = inputDbSetContent.AsQueryable();
var dbSet = new Mock<DbSet<T>>();
dbSet.As<IQueryable<T>>().Setup(m => m.Provider).Returns(DbSetContent.Provider);
dbSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(DbSetContent.Expression);
dbSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(DbSetContent.ElementType);
dbSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(() => inputDbSetContent.GetEnumerator());
dbSet.Setup(m => m.Add(It.IsAny<T>())).Callback<T>((s) => inputDbSetContent.Add(s));
dbSet.Setup(m => m.Remove(It.IsAny<T>())).Callback<T>((s) => inputDbSetContent.Remove(s));
return dbSet;
}
[Fact]
public void DeleteConfirmedTest()
{
// Arrange
var mockAlbumSet = MockDbSet(new List<Album> { });
Mock<ApplicationDbContext> sutDbContext = new Mock<ApplicationDbContext>() { CallBase = true };
sutDbContext.Setup(m => m.Album).Returns(mockAlbumSet.Object);
// Check if Album.Remove works inside this test
var albumToBeDeleted = new Album() { AlbumID = 1, Title = "TestAlbumName" };
sutDbContext.Object.Album.Add(albumToBeDeleted);
Assert.Equal(1, (from a in sutDbContext.Object.Album select a).Count());
sutDbContext.Object.Album.Remove(albumToBeDeleted);
Assert.Equal(0, (from a in sutDbContext.Object.Album select a).Count());
// Actual Test
sutDbContext.Object.Album.Add(albumToBeDeleted);
sutDbContext.Setup(m => m.Album.Find(It.IsAny<int>()))
.Returns(albumToBeDeleted);
AlbumController sut = new AlbumController(sutDbContext.Object);
var output = sut.DeleteConfirmed(1); // Throws NotImplementedException
// Assert
Assert.Equal(0, (from a in sutDbContext.Object.Album select a).Count());
}
}
The test throws the following exception on db.Album.Remove(album) in DeleteConfirmed:
System.NotImplementedException : The member 'Remove' has not been
implemented on type 'DbSet1Proxy' which inherits from 'DbSet1'. Test
doubles for 'DbSet`1' must provide implementations of methods and
properties that are used.
As you can see in MockDbSet method body, I setup Remove method for my Mock and it works just fine inside the unit test. Can you explain me why it doesn't work inside the controller?
Your test will work fine if you change your line:
sutDbContext.Setup(m => m.Album.Find(It.IsAny<int>()))
.Returns(albumToBeDeleted);
To:
mockAlbumSet.Setup(x=>x.Find(It.IsAny<int>()))
.Returns(albumToBeDeleted);
You made a setup for your sutDbContext to return mockAlbumSet.Object when sutDbContext.Album is called, but that line overridden your setup to create a new mock object for sutDbContext.Album property and created a single setup for that mock:
m.Album.Find(It.IsAny<int>()))
.Returns(albumToBeDeleted);
Here is a simple test, that shows you that calling setup for nested property of the class, that was previously setup to return a Mock.Object, will override that property with a new Mock.Object:
public interface IParentService
{
IDependantService Dependant { get; }
}
public interface IDependantService
{
void Execute();
}
[Fact]
//This test passes
public void VerifyThatNestedMockSetupGeneratesNewMockObject()
{
var value = 0;
var parentServiceMock = new Mock<IParentService>();
var dependantServiceMock = new Mock<IDependantService>();
dependantServiceMock.Setup(x => x.Execute()).Callback(() => { value = 1; });
parentServiceMock.Setup(x => x.Dependant).Returns(dependantServiceMock.Object);
Assert.Same(parentServiceMock.Object.Dependant, dependantServiceMock.Object);
parentServiceMock.Setup(x => x.Dependant.Execute()).Callback(() => { value = 2; });
Assert.NotSame(parentServiceMock.Object.Dependant, dependantServiceMock.Object);
parentServiceMock.Object.Dependant.Execute();
Assert.Equal(2, value);
}
I'm currently writing integration tests using nunit for a previously untested server that was written in C# using ApiController and Entity Framework. Most of the tests run just fine, but I've ran into two that always cause the database to time out. The error messages look something like this:
System.Data.Entity.Infrastructure.DbUpdateException : An error occurred while updating the entries. See the inner exception for details.
System.Data.Entity.Core.UpdateException : An error occurred while updating the entries. See the inner exception for details.
System.Data.SqlClient.SqlException : Timeout expired. The timeout period elapsed prior to completion of the operation or the server is not responding.
System.ComponentModel.Win32Exception : The wait operation timed out
The first test that's timing out:
[TestCase, WithinTransaction]
public async Task Patch_EditJob_Success()
{
var testJob = Data.SealingJob;
var requestData = new Job()
{
ID = testJob.ID,
Name = "UPDATED"
};
var apiResponse = await _controller.EditJob(testJob.ID, requestData);
Assert.IsInstanceOf<StatusCodeResult>(apiResponse);
Assert.AreEqual("UPDATED", testJob.Name);
}
The other test that's timing out:
[TestCase, WithinTransaction]
public async Task Post_RejectJob_Success()
{
var rejectedJob = Data.SealingJob;
var apiResponse = await _controller.RejectJob(rejectedJob.ID);
Assert.IsInstanceOf<OkResult>(apiResponse);
Assert.IsNull(rejectedJob.Organizations);
Assert.AreEqual(rejectedJob.JobStatus, JobStatus.OnHold);
_fakeEmailSender.Verify(
emailSender => emailSender.SendEmail(rejectedJob.Creator.Email, It.Is<string>(emailBody => emailBody.Contains(rejectedJob.Name)), It.IsAny<string>()),
Times.Once());
}
These are the controller methods that these tests are using:
The timeout always happens on the first call to await db.SaveChangesAsync() within the controller. Other controller methods that are being tested also call SaveChangesAsync without any problem. I've also tried calling SaveChangesAsync from within the failing tests and it works fine there. Both of these methods they are calling work normally when called from within the controller, but time out when called from the tests.
[HttpPatch]
[Route("editjob/{id}")]
public async Task<IHttpActionResult> EditJob(int id, Job job)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (id != job.ID)
{
return BadRequest();
}
Job existingJob = await db.Jobs
.Include(databaseJob => databaseJob.Regions)
.FirstOrDefaultAsync(databaseJob => databaseJob.ID == id);
existingJob.Name = job.Name;
// For each Region find if it already exists in the database
// If it does, use that Region, if not one will be created
for (var i = 0; i < job.Regions.Count; i++)
{
var regionId = job.Regions[i].ID;
var foundRegion = db.Regions.FirstOrDefault(databaseRegion => databaseRegion.ID == regionId);
if (foundRegion != null)
{
existingJob.Regions[i] = foundRegion;
db.Entry(existingJob.Regions[i]).State = EntityState.Unchanged;
}
}
existingJob.JobType = job.JobType;
existingJob.DesignCode = job.DesignCode;
existingJob.DesignProgram = job.DesignProgram;
existingJob.JobStatus = job.JobStatus;
existingJob.JobPriority = job.JobPriority;
existingJob.LotNumber = job.LotNumber;
existingJob.Address = job.Address;
existingJob.City = job.City;
existingJob.Subdivision = job.Subdivision;
existingJob.Model = job.Model;
existingJob.BuildingDesignerName = job.BuildingDesignerName;
existingJob.BuildingDesignerAddress = job.BuildingDesignerAddress;
existingJob.BuildingDesignerCity = job.BuildingDesignerCity;
existingJob.BuildingDesignerState = job.BuildingDesignerState;
existingJob.BuildingDesignerLicenseNumber = job.BuildingDesignerLicenseNumber;
existingJob.WindCode = job.WindCode;
existingJob.WindSpeed = job.WindSpeed;
existingJob.WindExposureCategory = job.WindExposureCategory;
existingJob.MeanRoofHeight = job.MeanRoofHeight;
existingJob.RoofLoad = job.RoofLoad;
existingJob.FloorLoad = job.FloorLoad;
existingJob.CustomerName = job.CustomerName;
try
{
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!JobExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return StatusCode(HttpStatusCode.NoContent);
}
[HttpPost]
[Route("{id}/reject")]
public async Task<IHttpActionResult> RejectJob(int id)
{
var organizations = await db.Organizations
.Include(databaseOrganization => databaseOrganization.Jobs)
.ToListAsync();
// Remove job from being shared with organizations
foreach (var organization in organizations)
{
foreach (var organizationJob in organization.Jobs)
{
if (organizationJob.ID == id)
{
organization.Jobs.Remove(organizationJob);
}
}
}
var existingJob = await db.Jobs.FindAsync(id);
existingJob.JobStatus = JobStatus.OnHold;
await db.SaveChangesAsync();
await ResetJob(id);
var jobPdfs = await DatabaseUtility.GetPdfsForJobAsync(id, db);
var notes = "";
foreach (var jobPdf in jobPdfs)
{
if (jobPdf.Notes != null)
{
notes += jobPdf.Name + ": " + jobPdf.Notes + "\n";
}
}
// Rejection email
var job = await db.Jobs
.Include(databaseJob => databaseJob.Creator)
.SingleAsync(databaseJob => databaseJob.ID == id);
_emailSender.SendEmail(
job.Creator.Email,
job.Name + " Rejected",
notes);
return Ok();
}
Other code that might be relevant:
The model being used is just a normal code-first Entity Framework class:
public class Job
{
public Job()
{
this.Regions = new List<Region>();
this.ComponentDesigns = new List<ComponentDesign>();
this.MetaPdfs = new List<Pdf>();
this.OpenedBy = new List<User>();
}
public int ID { get; set; }
public string Name { get; set; }
public List<Region> Regions { get; set; }
// etc...
}
To keep the database clean between tests, I'm using this custom attribute to wrap each one in a transaction (from http://tech.trailmax.info/2014/03/how-we-do-database-integration-tests-with-entity-framework-migrations/):
public class WithinTransactionAttribute : Attribute, ITestAction
{
private TransactionScope _transaction;
public ActionTargets Targets => ActionTargets.Test;
public void BeforeTest(ITest test)
{
_transaction = new TransactionScope();
}
public void AfterTest(ITest test)
{
_transaction.Dispose();
}
}
The database connection and controller being tested is build in setup methods before each test:
[TestFixture]
public class JobsControllerTest : IntegrationTest
{
// ...
private JobsController _controller;
private Mock<EmailSender> _fakeEmailSender;
[SetUp]
public void SetupController()
{
this._fakeEmailSender = new Mock<EmailSender>();
this._controller = new JobsController(Database, _fakeEmailSender.Object);
}
// ...
}
public class IntegrationTest
{
protected SealingServerContext Database { get; set; }
protected TestData Data { get; set; }
[SetUp]
public void SetupDatabase()
{
this.Database = new SealingServerContext();
this.Data = new TestData(Database);
}
// ...
}
This bug was apparently caused by the use of await within a TransactionScope. Following the top answer to this question, I added the TransactionScopeAsyncFlowOption.Enabled parameter when constructing the TransactionScope and the timeout issue went away.