Work in Progress
Diese Seite ist aktuell im Review! Die Seite wurde noch nicht qualitätsgesichert und kann Fehler enthalten.
Die verlinkten Seiten sind ggf. nur für Schleupen-Mitarbeiter sichtbar.

ConnectionTest für Objektpersistierung mit NHibernate

Die Implementierung von NHibernate-Mappings gemäß Objektpersistierung mit NHibernate ist fehlerträchtig und in vielen Fällen nichttrivial. Dabei ist es insbesondere wichtig, das Mapping und das Repository im Zusammenspiel mit einer echten Datenbank zu testen. Hierzu zählt auch die Überprüfung der ausgeführten SQL-Statements sowie deren Anzahl, wieviele Datensätze manipuliert werden und die Ausführungszeit.

Design

Um ein Repository für einen Test zu konfigurieren, wird ein eigener NHibernateConfigurator implementiert. Dies ist im Allgemeinen notwendig, um beispielsweise das Auslösen von Events abzuklemmen, wofür die CS 3.0-Infrastruktur notwendig ist. Diese ist im CI-Build nicht vorhanden.

Die Implementierung erfolgt dann durch Wiederverwendung vorhandenen Repository-Konfigurators aber unter anderer Zustellung der benutzten Komponenten. So wird beispielsweise für einen NotificationController (die Klasse die Create-Events etc. auslöst) im Normalfall als INHibernateExtender eingebunden. Hier wird stattdesse ein Mock eingehangen.

Zudem wird ein sogenannter TestConfigurationConnectionInfoProvider implementiert, der die Schnittstellen IConnectionInfoProvider implementiert. Dieser, im Test verwendete, ConnectionInfoProvider ermittelt die Datenbankverbnindung aus der app.config. Dies ist notwendig, um die DB-Verbindung unabhängig von der CS 3.0-Infrastruktur zu ermitteln.

<connectionStrings>
  <add name="Schleupen.AS.MT.BIB" providerName="System.Data.SqlClient" connectionString="Data Source=localhost;Initial Catalog=mss_cssy30;Integrated Security=False;User ID=usr;Password=pwd;MultipleActiveResultSets=False;MultipleActiveResultSets=False;Application Name=Schleupen.AS.MT.BIB;" />
</connectionStrings>

Da die Transaktionen in den Tests normalerweise zurückgerollt werden, sind die Daten während der Testausführung im SQL-Server nicht sichtbar.
Mittels SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED vor Ausführung einer Query im SQL-Server, können auch die noch nicht committeten Daten eingesehen werden. Dies vereinfacht die Analyse von Tests.

Implementierung

Das Konzept wird anhand eines einfachen Beispiels erläutert.

Zunächst wird gemäß Testklassen für Unittests erstellen ein Test implementiert. Dabei wird vor jedem Test mithilfe des TransactionScopeFixtures eine Transaktion aufgemacht und nach Abschluss ein Rollback durchgeführt.

BuchRepositoryTest.cs

public sealed partial class BuchRepositoryTest
{
    [Test]
    public void UpdateBuch_WithNewInhaltsangabeAndSeite_ShouldPersist()
    {
        Buch buch = fixture.CreatePersistedBuchAggregate();
        using (BuchRepository testObject = fixture.CreateBuchRepository())
        {
            Buch attachedBuch = testObject.QueryById(buch.Id);
            attachedBuch.ErweitereInhaltsangabeUm("meine eigene Supidupi-Seite");
            testObject.Flush();
        }

        fixture.AssertEqualExistenceOf(buch);
    }
    // ...
}

BuchRepositoryTest.Fixture.cs

public sealed partial class BuchRepositoryTest : IDisposable
{
    // Setup & TearDown etc.
    private sealed class Fixture : IDisposable
    {
        private readonly TransactionScopeFixture transactionScopeFixture = new TransactionScopeFixture();
        private readonly BuchFixture buchFixture = new BuchFixture();

        public Mocks Mocks { get; } = new Mocks();
        // ...
        public void AssertEqualExistenceOf(Buch expected)
        {
            Buch buchInDatabase = LoadBuchFromDatabase(expected);
            // ...
            Assert.That(buchInDatabase,
                Properties.Are.EqualTo(
                    expected,
                    Ignore.PropertyOf<IBuchData>(x => x.Erscheinungsdatum)
                        .IgnoreProperty(x => x.PreiseChronologie)
                        .IgnoreProperty(x => x.Inhaltsangabe)));
        }

        public BuchRepository CreateBuchRepository()
        {
            return new BuchRepository(
                new TestConfigurationConnectionInfoProvider(), // ConnectionInfoProvider, der aus app.config liest
                new BuchRepositoryConfigurator(
                    new BuchRepositoryNotificationController(
                        Mocks.BuchCreatedEventServiceGatewayMock.Object, ...), ...);  // Registrierung der Mocks
        }

        public Buch CreatePersistedBuchAggregate()
        {
            return Persist(CreateBuchAggregate());
        }

        public Buch CreateBuchAggregate(string titelprefix = null)
        {
            return buchFixture.NewBuch(titelprefix).WithSeite()....Build();
        }

        public Buch Persist(Buch buch)
        {
            using (BuchRepository buchRepository = CreateBuchRepository())
            {
                buchRepository.Add(buch);
                buchRepository.Flush();
                Buch detachedBuch = buchRepository.Detach(buch);
                return detachedBuch;
            }
        }

        public void Dispose()
        {
            transactionScopeFixture.Dispose();
        }
    }

    private sealed class Mocks
    {
        public Mock<IBuchUpdatedEventServiceGateway> BuchUpdatedEventServiceGatewayMock { get; } = new Mock<IBuchCreatedEventServiceGateway>();
        //...
    }
}

BuchRepositoryConfigurator

public class BuchRepositoryConfigurator : NHibernateConfigurator, IBuchRepositoryConfigurator
{
    private readonly IBuchRepositoryNotificationController notificationController;
    // ...
    public BuchRepositoryConfigurator(IBuchRepositoryNotificationController notificationController, ...)
    {
        this.notificationController = notificationController ?? throw new ArgumentNullException(nameof(notificationController));
        // ...
    }

    protected override IEnumerable<Type> MappingTypes
    {
        get
        {
            //...
            return mappingTypes;
        }
    }

    protected override IEnumerable<INHibernateExtender> Extenders
    {
        get
        {
            List<INHibernateExtender> nHibernateExtenders = new List<INHibernateExtender>();
            nHibernateExtenders.AddRange(base.Extenders);
            nHibernateExtenders.Add(notificationController);  // Hier wird der Mock registriert
            // ...
            return nHibernateExtenders;
        }
    }
}

TestConfigurationConnectionInfoProvider

public class TestConfigurationConnectionInfoProvider : ConfigurationConnectionInfoProvider
{
    public TestConfigurationConnectionInfoProvider()
     : base("Schleupen.AS.MT.BIB") // Eintrag in app.config, <connectionStrings><add name="Schleupen.CS.MT.BIB" ...
    {
    }
}
Testen von SQL-Abfragen

Wie die Erfahrung gezeigt hat, ist es wichtig, die ausgeführten SQL-Statements zu prüfen. Dies kann durch die Klasse StatementExecutionEngine implementiert werden:

// ...
[Test]
public void QueryByTitel_WithMaxResults_WhereTitelExists_ShouldExecuteOneStament()
{
    using (new TransactionScopeFixture())
    {
        Buch buch = fixture.CreatePersistedBuchAggregate();

        using (BuchRepository testObject = fixture.CreateBuchRepository())
        using (StatementExecutionScope statementExecutionScope = new StatementExecutionScope())
        {
            BuchAggregateRestriction buchRestriction = ...
            buchRestriction.MaxResults = 1;

            BuchListe buecher = testObject.QueryBy(buchRestriction);

            Assert.That(buecher.Items.Count, Is.EqualTo(1));
            Assert.That(statementExecutionScope.StatementCount, Is.EqualTo(1));
            Assert.That(statementExecutionScope.SqlTraces[0].Sql, Is.StringContaining(...)); // Verfügbar ab HV20
        }
    }
}

Abgrenzung

Vollintegrative Tests sind nach wie vor notwendig.

Stärken und Schwächen

Stärken
  • Möglichkeit des Testens im CI-Build, d.h. unabhängig von der CS 3.0-Infrastruktur.
  • Flexibilität bzgl. des Testens
Cookie Consent mit Real Cookie Banner