6

Obecnie piszę testy integracyjne za pomocą nunit na poprzednio nieprzetestowany serwer napisany w języku C# przy użyciu ApiController i Entity Framework. Większość testów jest w porządku, ale mam dwa, które zawsze powodują, że baza danych przestaje działać. Komunikaty o błędach wyglądają mniej więcej tak:Test integracyjny powoduje przekroczenie limitu czasu Entity Framework

System.Data.Entity.Infrastructure.DbUpdateException: Wystąpił błąd podczas aktualizacji wpisów. Zobacz wewnętrzny wyjątek, aby poznać szczegóły.
System.Data.Entity.Core.UpdateException: Wystąpił błąd podczas aktualizowania wpisów. Zobacz wewnętrzny wyjątek, aby poznać szczegóły.
System.Data.SqlClient.SqlException: Limit czasu wygasł. Okres oczekiwania upłynął przed zakończeniem operacji lub serwer nie odpowiada.
System.ComponentModel.Win32Exception: Operacja upłynął limit czasu oczekiwania

Pierwszy test, który jest odmierzanie:

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

innego testu, który jest odmierzanie:

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

Są metody kontrolera używane przez te testy: Limit czasu zawsze ma miejsce podczas pierwszego połączenia z await db.SaveChangesAsync() w ramach kontrolki er. Inne testowane metody kontrolera również bez problemu wywołują numer SaveChangesAsync. Próbowałem również wywoływać SaveChangesAsync z testów niepowodzeń i działa dobrze tam. Obie te metody, które wywołują, działają normalnie, gdy są wywoływane z poziomu kontrolera, ale kończą się, gdy zostaną wywołane z testów.

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

Inne kody, które mogą być istotne:

model używany jest tylko kod pierwszego normalne klasy 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... 
} 

Aby zachować bazę danych czyste między testami, I” m użyciu tego zwyczaj atrybut zawijać w każdy z transakcji (od 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(); 
    } 
} 

Połączenie z bazą danych i kontroler testowany jest budować w metodach konfiguracji przed każdym badaniem:

[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

Instrukcja powodująca przekroczenie limitu czasu jest pierwszą 'oczekującą db.SaveChangesAsync()', która występuje. –

+0

Czy limity czasu również występują, jeśli testy są wykonywane oddzielnie? Wywołania asynchroniczne w teście integracji w zakresach transakcji mogą powodować zakleszczenia. Ale to dziwne, że zawsze te same testy zawodzą. Sprawdź instrukcje SQL, które wykonują 'SaveChangesAsync'. –

+1

To może pomóc http://stackoverflow.com/a/17527759/1236044 – jbl

Odpowiedz

4

Ten błąd został najwyraźniej spowodowane przez użycie czekają w TransactionScope. Po uzyskaniu najwyższej odpowiedzi na this question dodałem parametr TransactionScopeAsyncFlowOption.Enabled podczas konstruowania TransactionScope, a problem przekroczenia limitu czasu zniknął.

Powiązane problemy