I am currently writing integration tests using nunit for a previously untested server that was written in C # using ApiController and Entity Framework. Most tests run just fine, but I came across two that always force the database to time out. The error messages look something like this:
System.Data.Entity.Infrastructure.DbUpdateException: An error occurred while updating records. See Internal Exception for more details.
System.Data.Entity.Core.UpdateException: An error occurred while updating records. See Internal Exception for more details.
System.Data.SqlClient.SqlException: Timed out. The wait period expires before the operation is completed or the server does not respond.
System.ComponentModel.Win32Exception: timeout
The first test that gives time:
[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); }
Another test that gives time:
[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 used by these tests: The timeout always occurs the first time await db.SaveChangesAsync() called inside the controller. Other validated controller methods also call SaveChangesAsync without any problems. I also tried calling SaveChangesAsync from the error tests, and it works fine. Both of these methods that they call usually work when called from the controller, but the wait time 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(); }
Another code that may make a difference:
The model used is just a regular Entity Framework class with code:
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; }
To keep the database clean between tests, I use this custom attribute to wrap each 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 connection to the database and the controller under test are built in the configuration methods before each test:
[TestFixture] public class JobsControllerTest : IntegrationTest {