6

Attualmente sto scrivendo test di integrazione utilizzando nunit per un server precedentemente non testato che è stato scritto in C# utilizzando ApiController e Entity Framework. La maggior parte dei test funziona alla perfezione, ma ho avuto due tentativi che causano sempre il timeout del database. I messaggi di errore hanno un aspetto simile al seguente:Il test di integrazione causa il timeout di Entity Framework

System.Data.Entity.Infrastructure.DbUpdateException: si è verificato un errore durante l'aggiornamento delle voci. Vedi l'eccezione interna per i dettagli.
System.Data.Entity.Core.UpdateException: si è verificato un errore durante l'aggiornamento delle voci. Vedi l'eccezione interna per i dettagli.
System.Data.SqlClient.SqlException: Timeout scaduto. Il periodo di timeout è trascorso prima del completamento dell'operazione o il server non risponde.
System.ComponentModel.Win32Exception: L'operazione di attesa è scaduta

Il primo test che è timeout:

[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); 
    } 

L'altra prova che è timeout:

[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()); 
    } 

Questi sono i Metodi di controller utilizzati da questi test: Il timeout si verifica sempre alla prima chiamata a await db.SaveChangesAsync() all'interno del controller ER. Altri metodi di controllo che vengono testati chiamano anche SaveChangesAsync senza alcun problema. Ho anche provato a chiamare SaveChangesAsync all'interno dei test in errore e funziona bene lì. Entrambi i metodi che chiamano funzionano normalmente quando vengono richiamati dal controller, ma scadono quando vengono chiamati dai test.

[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(); 
    } 

Altro codice che potrebbero essere rilevanti:

Il modello utilizzato è solo un codice-prima Normale Entity Framework:

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... 
} 

per mantenere il database pulito tra i test, I' m utilizzando questo attributo personalizzato per racchiudere ognuno in una transazione (da 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(); 
    } 
} 

La connessione al database e di controllo in fase di sperimentazione è costruito in metodi di impostazione prima di ogni prova:

[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); 
    } 

    // ... 
} 
+0

L'istruzione che causa il timeout è la prima "attesa db.SaveChangesAsync()" che si verifica. –

+0

Si verificano anche i timeout se si eseguono i test in isolamento? Le chiamate asincrone nel test di integrazione negli ambiti di transazione possono causare deadlock. Ma poi è strano che sempre gli stessi test falliscano. Controlla le istruzioni SQL che esegue "SaveChangesAsync'. –

+1

Questo potrebbe aiutare http://stackoverflow.com/a/17527759/1236044 – jbl

risposta

4

Questo problema è stato a quanto pare causato dall'uso di attesa all'interno di un TransactionScope. Seguendo la risposta in alto a this question, ho aggiunto il parametro TransactionScopeAsyncFlowOption.Enabled durante la costruzione di TransactionScope e il problema di timeout è andato via.