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);
}
// ...
}
Instrukcja powodująca przekroczenie limitu czasu jest pierwszą 'oczekującą db.SaveChangesAsync()', która występuje. –
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'. –
To może pomóc http://stackoverflow.com/a/17527759/1236044 – jbl