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

Dav*_*eed 6 c# nunit unit-testing entity-framework asp.net-apicontroller

我目前正在使用nunit为以前未经测试的服务器编写集成测试,该服务器是用C#using ApiController和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);
    }
Run Code Online (Sandbox Code Playgroud)

超时的另一个测试:

    [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());
    }
Run Code Online (Sandbox Code Playgroud)

这些是这些测试使用的控制器方法:超时始终发生在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();
    }
Run Code Online (Sandbox Code Playgroud)

其他可能相关的代码:

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

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...
}
Run Code Online (Sandbox Code Playgroud)

为了在测试之间保持数据库清洁,我使用这个自定义属性将每个属性包装在一个事务中(来自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();
    }
}
Run Code Online (Sandbox Code Playgroud)

正在测试的数据库连接和控制器是在每次测试之前构建的安装方法:

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

    // ...
}
Run Code Online (Sandbox Code Playgroud)

Dav*_*eed 5

这个错误显然是由于在TransactionScope. 按照这个问题的最佳答案,我TransactionScopeAsyncFlowOption.Enabled在构造时添加了参数TransactionScope,超时问题消失了。