6

現在、C#で書かれた以前にテストされていないサーバーに対してnunitを使用して統合テストを作成しています(ApiControllerとEntity Frameworkを使用)。ほとんどのテストはうまくいきますが、私は2つに分かれて常にデータベースのタイムアウトを引き起こしています。エラーメッセージは次のようになります。統合テストによりEntity Frameworkのタイムアウトが発生する

System.Data.Entity.Infrastructure.DbUpdateException:エントリの更新中にエラーが発生しました。詳細については、内部例外を参照してください。
System.Data.Entity.Core.UpdateException:エントリの更新中にエラーが発生しました。詳細については、内部例外を参照してください。
System.Data.SqlClient.SqlException:タイムアウトが切れています。操作が完了する前にタイムアウト時間が経過したか、サーバーが応答していません。
System.ComponentModel.Win32Exception:

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

他のテストタイムアウトです::待機操作がタイムアウトだ

最初のテストタイムアウトし

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

をこれらはこれらのテストで使用されているコントローラメソッド: タイムアウトは、コントロール内のawait db.SaveChangesAsync()への最初の呼び出しで常に発生しますエル。テスト中の他のコントローラーメソッドも、問題なくSaveChangesAsyncを呼び出します。私はまた、失敗したテストの中からSaveChangesAsyncを呼び出すことを試みました。これらのメソッドは、コントローラ内から呼び出されたときに正常に動作しますが、テストから呼び出されるとタイムアウトします。

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

関連するかもしれない他のコード:

使用されているモデルは、普通のコード最初の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... 
} 

テストの間にきれいなデータベースを維持するために、私は」 (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(); 
    } 
} 
[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

タイムアウトを引き起こすステートメントは、最初に発生する 'await db.SaveChangesAsync()'です。 –

+0

テストを単独で実行するとタイムアウトも発生しますか?トランザクションスコープでの統合テストでの非同期呼び出しは、デッドロックを引き起こす可能性があります。しかし、常に同じテストが失敗するのは変です。 'SaveChangesAsync'が実行するSQL文を確認してください。 –

+1

これは役立つかもしれませんhttp://stackoverflow.com/a/17527759/1236044 – jbl

答えて

4

このバグは明らかにTransactionScope内待つの使用によって引き起こされた:テストされているデータベース接続およびコントローラは、各テストの前に、セットアップ方法で構築しています。一番上の答えがthis questionに続いて、TransactionScopeを構築するときにTransactionScopeAsyncFlowOption.Enabledパラメータを追加し、タイムアウトの問題が解消されました。

関連する問題