Jegyzet
Az oldalhoz való hozzáférés engedélyezést igényel. Próbálhatod be jelentkezni vagy könyvtárat váltani.
Az oldalhoz való hozzáférés engedélyezést igényel. Megpróbálhatod a könyvtár váltását.
Ezen a lapon az automatizált tesztek írásának technikáiról lesz szó, amelyek magukban foglalják azt az adatbázisrendszert, amelyen az alkalmazás éles környezetben fut. Léteznek alternatív tesztelési módszerek, amelyekben az éles adatbázisrendszert tesztkettőkkel cserélik fel; további információt a tesztelés áttekintési oldalán talál. Vegye figyelembe, hogy a tesztelés az éles környezetben használattól eltérő adatbázison (pl. Sqlite) nem tárgya ennek az ismertetésnek, mivel a különböző adatbázis teszthelyettesítőként működik; ez a megközelítés az éles adatbázisrendszer nélküli tesztelés részben található.
A valódi adatbázist magában foglaló tesztelés fő akadálya a megfelelő tesztelkülönítés biztosítása, hogy a párhuzamosan (vagy akár sorosan) futó tesztek ne zavarják egymást. Az alábbi teljes mintakód itt tekinthető meg.
Jótanács
Ezen az oldalon xUnit technikák láthatók, de hasonló fogalmak léteznek más tesztelési keretrendszerekben is, például NUnit.
Az adatbázis-rendszer beállítása
Manapság a legtöbb adatbázisrendszer könnyen telepíthető CI-környezetekben és fejlesztői gépeken is. Bár gyakran elég egyszerű telepíteni az adatbázist a rendszeres telepítési mechanizmussal, a használatra kész Docker-rendszerképek a legtöbb nagyobb adatbázishoz elérhetők, és különösen megkönnyítik a telepítést a CI-ben. Az olyan kódtárak, mint a Testcontainers , tovább egyszerűsíthetik ezt a tárolóalapú adatbázispéldányok tesztelés közbeni automatikus kezelésével. A fejlesztői környezethez, a GitHub-munkaterületekhez a Dev Container minden szükséges szolgáltatást és függőséget beállíthat , beleértve az adatbázist is. Bár ehhez kezdeti befektetésre van szükség a beállításba, ha ez megtörtént, egy működő tesztelési környezettel rendelkezik, és a fontosabb dolgokra összpontosíthat.
Bizonyos esetekben az adatbázisok speciális kiadással vagy verzióval rendelkeznek, amely hasznos lehet a teszteléshez. AZ SQL Server használatakor a LocalDB segítségével helyileg futtathat teszteket, gyakorlatilag egyáltalán nem lehet beállítani, igény szerint felpörgetheti az adatbázispéldányt, és erőforrásokat takaríthat meg kevésbé hatékony fejlesztői gépeken. A LocalDB-nek azonban nincsenek problémái:
- Nem támogat mindent, amit az SQL Server Developer Edition tesz.
- Csak Windows rendszeren érhető el.
- Ez késést okozhat az első tesztfuttatás során, mivel a szolgáltatás elindul.
Általában az SQL Server Developer Edition telepítését javasoljuk a LocalDB helyett, mivel a teljes SQL Server-funkciókészletet biztosítja, és általában nagyon könnyen elvégezhető.
Felhőalapú adatbázis használata esetén általában célszerű tesztelni az adatbázis helyi verzióját, mind a sebesség javítása, mind a költségek csökkentése érdekében. Ha például az SQL Azure-t éles környezetben használja, tesztelhet egy helyileg telepített SQL Serveren – a kettő rendkívül hasonló (bár még mindig bölcs dolog, ha maga az SQL Azure-on futtat teszteket, mielőtt éles üzembe lép). Az Azure Cosmos DB használatakor az Azure Cosmos DB emulátor hasznos eszköz mind a helyi fejlesztéshez, mind a tesztek futtatásához.
Tesztadatbázis létrehozása, üzembe helyezése és kezelése
Miután telepítette az adatbázist, készen áll arra, hogy megkezdje a használatát a tesztekben. A legegyszerűbb esetekben a tesztcsomag egyetlen adatbázissal rendelkezik, amely több tesztcsoporton belül több teszt között van megosztva, ezért némi logikára van szükségünk ahhoz, hogy az adatbázist a tesztfuttatás teljes időtartama alatt pontosan egyszer hozzuk létre és vizsgáljuk meg.
A Xunit használatakor ez egy osztályjavításon keresztül végezhető el, amely az adatbázist jelöli, és több tesztfuttatás között van megosztva:
public class TestDatabaseFixture
{
private const string ConnectionString = @"Server=(localdb)\mssqllocaldb;Database=EFTestSample;Trusted_Connection=True;ConnectRetryCount=0";
private static readonly object _lock = new();
private static bool _databaseInitialized;
public TestDatabaseFixture()
{
lock (_lock)
{
if (!_databaseInitialized)
{
using (var context = CreateContext())
{
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
context.AddRange(
new Blog { Name = "Blog1", Url = "http://blog1.com" },
new Blog { Name = "Blog2", Url = "http://blog2.com" });
context.SaveChanges();
}
_databaseInitialized = true;
}
}
}
public BloggingContext CreateContext()
=> new BloggingContext(
new DbContextOptionsBuilder<BloggingContext>()
.UseSqlServer(ConnectionString)
.Options);
}
A fenti fixture példányosításakor EnsureDeleted() szolgál az adatbázis elvetésére (ha egy korábbi futtatásból származik), majd EnsureCreated() hozza létre a legújabb modellkonfigurációval (lásd az API-k dokumentációját). Az adatbázis létrehozása után a tesztadatok feltöltése olyan adatokkal látja el, amelyeket a tesztek felhasználhatnak. Érdemes időt szánni a kezdeti adatok átgondolására, mert ha később módosítod őket egy új teszthez, az a meglévő tesztek sikertelenségéhez vezethet.
Ha egy tesztosztályban szeretné használni a szerelvényt, egyszerűen implementálja IClassFixture a rögzítési típust, és az xUnit a konstruktorba fogja injektálni:
public class BloggingControllerTest : IClassFixture<TestDatabaseFixture>
{
public BloggingControllerTest(TestDatabaseFixture fixture)
=> Fixture = fixture;
public TestDatabaseFixture Fixture { get; }
A tesztosztály most már rendelkezik egy Fixture tulajdonságtal, amelyet a tesztek egy teljesen működőképes környezeti példány létrehozásához használhatnak:
[Fact]
public async Task GetBlog()
{
using var context = Fixture.CreateContext();
var controller = new BloggingController(context);
var blog = (await controller.GetBlog("Blog2")).Value;
Assert.Equal("http://blog2.com", blog.Url);
}
Végül észrevehetted, hogy a fixture fentebbi létrehozási logikában valamilyen zárolás történt. Ha a rögzítést csak egyetlen tesztosztályban használják, az xUnit garantáltan pontosan egyszer példányosíthatja azt; de gyakori, hogy ugyanazt az adatbázis-szerelvényt több tesztosztályban is használják. Az xUnit gyűjtési szerelvényeket biztosít, de ez a mechanizmus megakadályozza, hogy a tesztosztályok párhuzamosan fussanak, ami fontos a tesztelési teljesítmény szempontjából. Ha ezt egy xUnit-osztály szerelvényével szeretné biztonságosan kezelni, egyszerűen zároljuk az adatbázis-létrehozást és -vetést, és statikus jelzővel biztosítjuk, hogy soha ne kelljen kétszer elvégezni.
Adatokat módosító tesztek
A fenti példa egy csak olvasható tesztet mutatott be, amely a tesztelkülönítés szempontjából egyszerű eset: mivel semmit nem módosítanak, a tesztelési interferencia nem lehetséges. Ezzel szemben az adatokat módosító tesztek problémásabbak, mivel zavarhatják egymást. Az írási tesztek elkülönítésének egyik gyakori módszere, hogy a tesztet egy tranzakcióba burkolják, és a tranzakciót a teszt végén visszaállítják. Mivel valójában semmi sincs elmentve az adatbázisba, más tesztek nem látnak módosításokat, így elkerülhető az interferencia.
Íme egy vezérlőmetódus, amely hozzáad egy blogot az adatbázishoz:
[HttpPost]
public async Task<ActionResult> AddBlog(string name, string url)
{
_context.Blogs.Add(new Blog { Name = name, Url = url });
await _context.SaveChangesAsync();
return Ok();
}
Ezt a módszert a következőkkel tesztelhetjük:
[Fact]
public async Task AddBlog()
{
using var context = Fixture.CreateContext();
context.Database.BeginTransaction();
var controller = new BloggingController(context);
await controller.AddBlog("Blog3", "http://blog3.com");
context.ChangeTracker.Clear();
var blog = await context.Blogs.SingleAsync(b => b.Name == "Blog3");
Assert.Equal("http://blog3.com", blog.Url);
}
Néhány megjegyzés a fenti tesztkódhoz:
- Elindítunk egy tranzakciót, hogy az alábbi módosítások ne legyenek lekötve az adatbázisra, és ne zavarjanak más teszteket. Mivel a tranzakció soha nem kerül véglegesítésre, implicit módon vissza lesz állítva a teszt végén, amikor a környezeti példányt megsemmisítik.
- A kívánt frissítések elvégzése után töröljük a környezeti példány változáskövetőjét ChangeTracker.Clear, hogy meggyőződjünk arról, hogy ténylegesen betöltjük a blogot az alábbi adatbázisból. Ehelyett két környezeti példányt használhatunk, de akkor meg kell győződnünk arról, hogy ugyanazt a tranzakciót mindkét példány használja.
- Érdemes lehet a tranzakciót a fixture-ben
CreateContextelindítani, hogy a tesztek egy olyan környezeti példányt kapjanak, amely már szerepel egy tranzakcióban, és készen álljon a frissítésekre. Ez segíthet megelőzni azokat az eseteket, amikor a tranzakció véletlenül feledésbe merül, és a tesztelés interferenciájához vezet, amely nehezen hibakereshető. Érdemes lehet a csak olvasható és írási teszteket is külön választani különböző tesztosztályokban.
Tranzakciókat explicit módon kezelő tesztek
A teszteknek van egy utolsó kategóriája, amely további nehézséget okoz: az adatokat módosító és a tranzakciókat explicit módon kezelő tesztek. Mivel az adatbázisok általában nem támogatják a beágyazott tranzakciókat, nem lehet tranzakciókat használni az elkülönítéshez a fentieknek megfelelően, mivel ezeket a tényleges termékkódnak kell használnia. Bár ezek a tesztek általában ritkábban fordulnak elő, speciális módon kell kezelni őket: az adatbázist az egyes tesztek után az eredeti állapotba kell törölni, a párhuzamosítást pedig le kell tiltani, hogy ezek a tesztek ne zavarják egymást.
Vizsgáljuk meg példaként a következő vezérlőmetódust:
[HttpPost]
public async Task<ActionResult> UpdateBlogUrl(string name, string url)
{
// Note: it isn't usually necessary to start a transaction for updating. This is done here for illustration purposes only.
await using var transaction = await _context.Database.BeginTransactionAsync(IsolationLevel.Serializable);
var blog = await _context.Blogs.FirstOrDefaultAsync(b => b.Name == name);
if (blog is null)
{
return NotFound();
}
blog.Url = url;
await _context.SaveChangesAsync();
await transaction.CommitAsync();
return Ok();
}
Tegyük fel, hogy valamilyen okból a metódushoz szerializálható tranzakciót kell használni (ez általában nem így van). Ennek eredményeképpen nem használhatunk tranzakciót a tesztelkülönítés biztosítására. Mivel a teszt ténylegesen elmenti az adatbázis módosításait, egy másik, saját, különálló adatbázissal rendelkező tesztelő környezetet határozunk meg, hogy ne zavarjuk a fent már látható teszteket.
public class TransactionalTestDatabaseFixture
{
private const string ConnectionString = @"Server=(localdb)\mssqllocaldb;Database=EFTransactionalTestSample;Trusted_Connection=True;ConnectRetryCount=0";
public BloggingContext CreateContext()
=> new BloggingContext(
new DbContextOptionsBuilder<BloggingContext>()
.UseSqlServer(ConnectionString)
.Options);
public TransactionalTestDatabaseFixture()
{
using var context = CreateContext();
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
Cleanup();
}
public void Cleanup()
{
using var context = CreateContext();
context.Blogs.RemoveRange(context.Blogs);
context.AddRange(
new Blog { Name = "Blog1", Url = "http://blog1.com" },
new Blog { Name = "Blog2", Url = "http://blog2.com" });
context.SaveChanges();
}
}
Ez a fixture hasonló a fent használthoz, de különösen tartalmaz egy metódust Cleanup . Ezt minden teszt után meghívjuk, hogy az adatbázis visszaállítsa a kiindulási állapotát.
Ha ezt a szerelvényt csak egyetlen tesztosztály fogja használni, a fenti módon hivatkozhatunk rá osztályszerelvényként – az xUnit nem párhuzamosítja a teszteket ugyanabban az osztályban (további információ a tesztgyűjteményekről és a párhuzamosításról az xUnit-dokumentumokban). Ha azonban ezt a szerelvényt több osztály között szeretnénk megosztani, meg kell győződnünk arról, hogy ezek az osztályok nem futnak párhuzamosan, hogy elkerüljük az interferenciát. Ehhez ezt xUnit gyűjtemény-szerelvényként fogjuk használni, nem pedig osztályszerelvényként.
Először meghatározunk egy tesztgyűjteményt, amely a fixtúránkra hivatkozik, és az összes tranzakciós tesztosztály használni fogja, amelyhez az szükséges:
[CollectionDefinition("TransactionalTests")]
public class TransactionalTestsCollection : ICollectionFixture<TransactionalTestDatabaseFixture>
{
}
Most a tesztosztályunkban hivatkozunk a tesztgyűjteményre, és a konstruktorban a korábbiakhoz hasonlóan elfogadjuk a tesztbeállítást.
[Collection("TransactionalTests")]
public class TransactionalBloggingControllerTest : IDisposable
{
public TransactionalBloggingControllerTest(TransactionalTestDatabaseFixture fixture)
=> Fixture = fixture;
public TransactionalTestDatabaseFixture Fixture { get; }
Végül eldobhatóvá tesszük a tesztosztályt, és minden egyes teszt után gondoskodunk az eszköz Cleanup metódusának meghívásáról.
public void Dispose()
=> Fixture.Cleanup();
Vegye figyelembe, hogy mivel az xUnit csak egyszer példányosítja a gyűjtemény tesztkörnyezetét, nincs szükség arra, hogy zároljuk az adatbázis létrehozását és inicializálását, ahogyan a fenti módon tettük.
A fentiek teljes mintakódja itt tekinthető meg.
Jótanács
Ha több tesztosztálya van, amelyek módosítják az adatbázist, akkor is futtathatja őket párhuzamosan, ha különböző szerelvényekkel rendelkezik, amelyek mindegyike a saját adatbázisára hivatkozik. Sok tesztadatbázis létrehozása és használata nem jelent problémát, és akkor érdemes elvégezni, amikor hasznos.
Hatékony adatbázis-létrehozás
A fenti mintákban teszteket használtunk EnsureDeleted() és EnsureCreated() futtattunk, hogy biztosan up-to-date tesztadatbázissal rendelkezzünk. Ezek a műveletek bizonyos adatbázisokban kissé lassúak lehetnek, ami problémát jelenthet a kódmódosítások iterálása és a tesztek újra- és újrafuttatása során. Ha ez a helyzet, előfordulhat, hogy ideiglenesen megjegyzést EnsureDeleted szeretne fűzni a berendezés konstruktorához: ez ugyanazt az adatbázist fogja újra felhasználni a tesztfuttatások során.
Ennek a módszernek az a hátránya, hogy ha módosítja az EF Core-modellt, az adatbázisséma nem lesz naprakész, és a tesztek sikertelenek lehetnek. Ennek eredményeképpen ezt csak ideiglenesen javasoljuk a fejlesztési ciklus során.
Hatékony adatbázis-törlés
A fentiekben láttuk, hogy amikor a módosítások ténylegesen le vannak kötelezve az adatbázisra, az interferencia elkerülése érdekében minden teszt között törölni kell az adatbázist. A fenti tranzakciós tesztmintában az EF Core API-k használatával töröltük a tábla tartalmát:
using var context = CreateContext();
context.Blogs.RemoveRange(context.Blogs);
context.AddRange(
new Blog { Name = "Blog1", Url = "http://blog1.com" },
new Blog { Name = "Blog2", Url = "http://blog2.com" });
context.SaveChanges();
Általában nem ez a leghatékonyabb módszer a tábla ürítésére. Ha a tesztelési sebesség aggodalomra ad okot, érdemes lehet nyers SQL-lel törölni a táblát:
DELETE FROM [Blogs];
Érdemes lehet megfontolni az újracsomagolt csomag használatát is, amely hatékonyan törli az adatbázist. Emellett nem kell megadnia a törölni kívánt táblákat, ezért a törlési kódot nem kell frissíteni, mivel a táblák hozzáadódnak a modellhez.
Összefoglalás
- Valós adatbázison végzett teszteléskor érdemes megkülönböztetni a következő tesztkategóriákat:
- A csak olvasható tesztek viszonylag egyszerűek, és mindig párhuzamosan hajthatók végre ugyanazon az adatbázison anélkül, hogy az elkülönítés miatt kellene aggódniuk.
- Az írási tesztek problémásabbak, de a tranzakciók segítségével biztosítható, hogy megfelelően elkülönítve legyenek.
- A tranzakciós tesztek a legproblémásabbak, amelyek megkövetelik, hogy a logika visszaállítsa az adatbázis eredeti állapotát, valamint letiltsa a párhuzamosítást.
- A tesztkategóriák külön osztályokba való elkülönítése elkerülheti a tesztek közötti zavart és véletlen interferenciát.
- Gondolkodjon el előre az előre meghatározott tesztadatokon, és próbálja meg úgy írni a teszteket, hogy azok ne törjenek meg túl gyakran, ha az előre meghatározott adatok módosulnak.
- Több adatbázissal párhuzamosíthatja azokat a teszteket, amelyek módosítják az adatbázist, és esetleg különböző alapadat-konfigurációkat is engedélyezhet.
- Ha a tesztelési sebesség aggodalomra ad okot, érdemes lehet áttekinteni a tesztadatbázis létrehozásának és az adatok futások közötti tisztításának hatékonyabb módszereit.
- Mindig tartsa szem előtt a párhuzamos tesztelést és az elkülönítést.