集成测试导致实体框架超时

时间:2016-05-25 21:54:08

标签: c# entity-framework unit-testing nunit asp.net-apicontroller

我目前正在使用nunit为使用ApiController和实体框架的C#编写的以前未经测试的服务器编写集成测试。大多数测试运行得很好,但我遇到了两个总是导致数据库超时的问题。错误消息如下所示:

  

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

其他可能相关的代码:

正在使用的模型只是一个普通的代码优先实体框架类:

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

    // ...
}

1 个答案:

答案 0 :(得分:4)

这个错误显然是由TransactionScope内的await使用引起的。在this question的最佳答案之后,我在构建TransactionScopeAsyncFlowOption.Enabled时添加了TransactionScope参数,并且超时问题消失了。