Verwenden von Code Coverage für Komponententests

Wichtig

In diesem Abschnitt wird die Erstellung des Beispielprojekts erläutert. Wenn Sie bereits über ein Projekt verfügen, können Sie zum Abschnitt Code-Coverage-Tools springen.

Komponententests helfen beim Sicherstellen der Funktionalität und bieten eine Überprüfungsmethode für das Refactoring. Code Coverage ist eine Messung der Codemenge, die von Komponententests ausgeführt wird. Dabei kann es sich um Zeilen, Branches oder Methoden handeln. Wenn Sie beispielsweise über eine einfache Anwendung mit nur zwei bedingten Codebranches (Branch a und Branch b) verfügen, wird bei einem Komponententest, der den bedingten Branch a überprüft, eine Code Coverage des Branchs von 50 % gemeldet.

In diesem Artikel werden die Verwendung von Code Coverage für Komponententests mit Coverlet sowie die Berichtsgenerierung mithilfe von ReportGenerator erläutert. Der Fokus dieses Artikels liegt auf C# und xUnit als Testframework. Es funktioniert aber auch mit MSTest und NUnit. Coverlet ist ein Open-Source-Projekt auf GitHub, das ein plattformübergreifendes Code-Coverage-Framework für C# bereitstellt. „Coverlet“ ist Teil der .NET Foundation. Coverlet sammelt Code-Coverage-Testlaufdaten von Cobertura, die für die Berichtsgenerierung verwendet werden.

Außerdem wird in diesem Artikel erläutert, wie die Code-Coverage-Informationen, die von einem Coverlet-Testlauf gesammelt wurden, zum Generieren eines Berichts verwendet werden. Die Berichtsgenerierung ist mithilfe eines anderen Open-Source-Projekts auf GitHub, ReportGenerator, möglich. ReportGenerator wandelt von Cobertura und vielen anderen generierte Deckungsberichte in für Menschen lesbare Berichte in unterschiedlichen Formaten um.

Dieser Artikel basiert auf dem Beispiequellcodeprojekt, das im Beispielbrowser verfügbar ist.

Getestetes System

„Getestetes System“ bezieht sich auf den Code, für den Sie Komponententests schreiben. Dabei kann es sich um Objekte, Dienste oder alles andere handeln, das über testbare Funktionen verfügt. Im Rahmen dieses Abschnitts erstellen Sie eine Klassenbibliothek, die das getestete System darstellt, sowie zwei entsprechende Komponententestprojekte.

Erstellen einer Klassenbibliothek

Erstellen Sie über eine Eingabeaufforderung in einem neuen Verzeichnis mit dem Namen UnitTestingCodeCoverage mithilfe des Befehls dotnet new classlib eine neue .NET Standard-Klassenbibliothek:

dotnet new classlib -n Numbers

Der folgende Codeausschnitt definiert eine einfache PrimeService-Klasse, die Funktionen bereitstellt, um zu prüfen, ob eine Zahl eine Primzahl ist. Kopieren Sie den folgenden Codeausschnitt, und ersetzen Sie den Inhalt der Datei Class1.cs, die automatisch im Verzeichnis Numbers (Zahlen) erstellt wurde. Benennen Sie die Datei Class1.cs in PrimeService.cs um.

namespace System.Numbers
{
    public class PrimeService
    {
        public bool IsPrime(int candidate)
        {
            if (candidate < 2)
            {
                return false;
            }

            for (int divisor = 2; divisor <= Math.Sqrt(candidate); ++divisor)
            {
                if (candidate % divisor == 0)
                {
                    return false;
                }
            }
            return true;
        }
    }
}

Tipp

Es sollte erwähnt werden, dass die Numbers-Klassenbibliothek dem System-Namespace absichtlich hinzugefügt wurde. Dies ermöglicht, dass System.Math ohne eine using System;-Namespacedeklaration zugänglich ist. Weitere Informationen finden Sie unter Namespace (C#-Referenz).

Erstellen von Testprojekten

Erstellen Sie über dieselbe Eingabeaufforderung mit dem Befehl dotnet new xunit zwei neue xUnit-Testprojekt (.NET Core) -Vorlagen:

dotnet new xunit -n XUnit.Coverlet.Collector
dotnet new xunit -n XUnit.Coverlet.MSBuild

Beide neu erstellten xUnit-Testprojekte müssen einen Projektverweis der Numbers-Klassenbibliothek hinzufügen. Dies ist erforderlich, damit die Testprojekte zum Testen auf PrimeService zugreifen können. Verwenden Sie in der Eingabeaufforderung den Befehl dotnet add:

dotnet add XUnit.Coverlet.Collector\XUnit.Coverlet.Collector.csproj reference Numbers\Numbers.csproj
dotnet add XUnit.Coverlet.MSBuild\XUnit.Coverlet.MSBuild.csproj reference Numbers\Numbers.csproj

Das MSBuild-Projekt wird entsprechend benannt, da es vom NuGet-Paket coverlet.msbuild abhängen wird. Fügen Sie diese Paketabhängigkeit hinzu, indem Sie den Befehl dotnet add package ausführen:

cd XUnit.Coverlet.MSBuild && dotnet add package coverlet.msbuild && cd ..

Mit dem vorherigen Befehl wurden Verzeichnisse geändert, wobei der Bereich effektiv auf das MSBuild-Testprojekt beschränkt und dann das NuGet-Paket hinzugefügt wurde. Anschließend wurden die Verzeichnisse geändert und um eine Ebene erhöht.

Öffnen Sie beide UnitTest1.cs-Dateien, und ersetzen Sie deren Inhalt durch den folgenden Codeausschnitt. Benennen Sie die UnitTest1.cs-Dateien in PrimeServiceTests.cs um.

using System.Numbers;
using Xunit;

namespace XUnit.Coverlet
{
    public class PrimeServiceTests
    {
        readonly PrimeService _primeService;

        public PrimeServiceTests() => _primeService = new PrimeService();

        [Theory]
        [InlineData(-1), InlineData(0), InlineData(1)]
        public void IsPrime_ValuesLessThan2_ReturnFalse(int value) =>
            Assert.False(_primeService.IsPrime(value), $"{value} should not be prime");

        [Theory]
        [InlineData(2), InlineData(3), InlineData(5), InlineData(7)]
        public void IsPrime_PrimesLessThan10_ReturnTrue(int value) =>
            Assert.True(_primeService.IsPrime(value), $"{value} should be prime");

        [Theory]
        [InlineData(4), InlineData(6), InlineData(8), InlineData(9)]
        public void IsPrime_NonPrimesLessThan10_ReturnFalse(int value) =>
            Assert.False(_primeService.IsPrime(value), $"{value} should not be prime");
    }
}

Erstellen einer Projektmappe

Erstellen Sie über die Eingabeaufforderung eine neue Projektmappe, um die Klassenbibliothek und die beiden Testprojekte zu kapseln. Verwenden Sie hierzu den dotnet sln-Befehl:

dotnet new sln -n XUnit.Coverage

Dadurch wird ein neuer Projektmappen-Dateiname XUnit.Coverage im Verzeichnis UnitTestingCodeCoverage erstellt. Fügen Sie die Projekte dem Stamm der Projektmappe hinzu.

dotnet sln XUnit.Coverage.sln add **/*.csproj --in-root

Erstellen Sie die Projektmappe mithilfe des dotnet build-Befehls:

dotnet build

Wenn der Build erfolgreich ist, haben Sie die drei Projekte erstellt, ordnungsgemäß auf die Projekte und Pakete verwiesen und den Quellcode richtig aktualisiert. Und das war‘s schon.

Code-Coverage-Tools

Es gibt zwei Arten von Code-Coverage-Tools:

  • DataCollectors: DataCollectors (Datensammler) überwachen die Testausführung und sammeln Informationen zu Testläufen. Sie melden die gesammelten Informationen in verschiedenen Ausgabeformaten, z. B. XML und JSON. Weitere Informationen finden Sie unter Ihr erster DataCollector.
  • Berichtsgeneratoren: Verwenden Sie die bei Testläufen gesammelten Daten zum Generieren von Berichten, die oft in Form von formatierten HTML-Dateien vorliegen.

In diesem Abschnitt liegt der Schwerpunkt auf den Datensammlertools.

.NET enthält einen integrierten Code-Coverage-Datencollector, der auch in Visual Studio verfügbar ist. Dieser Datencollector generiert eine binäre COVERAGE-Datei, die zum Generieren von Berichten in Visual Studio verwendet werden kann. Die Binärdatei ist nicht lesbar und muss in ein lesbares Format konvertiert werden, bevor sie zum Generieren von Berichten außerhalb von Visual Studio verwendet werden kann.

Tipp

Das dotnet-coverage-Tool kann plattformübergreifend verwendet werden, um die Testergebnisse der binären COVERAGE-Datei in ein lesbares Format zu konvertieren. Weitere Informationen finden Sie unter dotnet-coverage.

Coverlet ist eine Open-Source-Alternative zum integrierten Collector. Sie generiert Testergebnisse als lesbare Cobertura-XML-Dateien, die dann zum Generieren von HTML-Berichten verwendet werden können. Wenn Sie Coverlet für die Code Coverage verwenden möchten, muss ein vorhandenes Komponententestprojekt die entsprechenden Paketabhängigkeiten aufweisen. Alternativ muss es auf globalen .NET-Tools und dem entsprechenden coverlet.console-NuGet-Paket basieren.

Integration mit .NET-Test

Die Vorlage des xUnit-Testprojekts ist standardmäßig bereits mit coverlet.collector integriert. Wechseln Sie über die Eingabeaufforderung in das Verzeichnis des XUnit.Coverlet.Collector-Projekts, und führen Sie den dotnet test-Befehl aus:

cd XUnit.Coverlet.Collector && dotnet test --collect:"XPlat Code Coverage"

Hinweis

Das "XPlat Code Coverage"-Argument ist ein Anzeigename, der den Datensammlern von Coverlet entspricht. Dieser Name ist erforderlich, die Groß-/Kleinschreibung wird jedoch nicht beachtet. Verwenden Sie "Code Coverage", um den integrierten Code-Coverage-Datencollector zu verwenden.

Bei der Ausführung von dotnet test wird eine resultierende coverage.cobertura.xml-Datei im Verzeichnis TestResults ausgegeben. Die XML-Datei enthält die Ergebnisse. Diese plattformübergreifende Option gilt für die .NET-CLI und eignet sich sehr gut für Build-Systeme, in denen MSBuild nicht verfügbar ist.

Nachfolgend finden Sie die coverage.cobertura.xml-Beispieldatei.

<?xml version="1.0" encoding="utf-8"?>
<coverage line-rate="1" branch-rate="1" version="1.9" timestamp="1592248008"
          lines-covered="12" lines-valid="12" branches-covered="6" branches-valid="6">
  <sources>
    <source>C:\</source>
  </sources>
  <packages>
    <package name="Numbers" line-rate="1" branch-rate="1" complexity="6">
      <classes>
        <class name="Numbers.PrimeService" line-rate="1" branch-rate="1" complexity="6"
               filename="Numbers\PrimeService.cs">
          <methods>
            <method name="IsPrime" signature="(System.Int32)" line-rate="1"
                    branch-rate="1" complexity="6">
              <lines>
                <line number="8" hits="11" branch="False" />
                <line number="9" hits="11" branch="True" condition-coverage="100% (2/2)">
                  <conditions>
                    <condition number="7" type="jump" coverage="100%" />
                  </conditions>
                </line>
                <line number="10" hits="3" branch="False" />
                <line number="11" hits="3" branch="False" />
                <line number="14" hits="22" branch="True" condition-coverage="100% (2/2)">
                  <conditions>
                    <condition number="57" type="jump" coverage="100%" />
                  </conditions>
                </line>
                <line number="15" hits="7" branch="False" />
                <line number="16" hits="7" branch="True" condition-coverage="100% (2/2)">
                  <conditions>
                    <condition number="27" type="jump" coverage="100%" />
                  </conditions>
                </line>
                <line number="17" hits="4" branch="False" />
                <line number="18" hits="4" branch="False" />
                <line number="20" hits="3" branch="False" />
                <line number="21" hits="4" branch="False" />
                <line number="23" hits="11" branch="False" />
              </lines>
            </method>
          </methods>
          <lines>
            <line number="8" hits="11" branch="False" />
            <line number="9" hits="11" branch="True" condition-coverage="100% (2/2)">
              <conditions>
                <condition number="7" type="jump" coverage="100%" />
              </conditions>
            </line>
            <line number="10" hits="3" branch="False" />
            <line number="11" hits="3" branch="False" />
            <line number="14" hits="22" branch="True" condition-coverage="100% (2/2)">
              <conditions>
                <condition number="57" type="jump" coverage="100%" />
              </conditions>
            </line>
            <line number="15" hits="7" branch="False" />
            <line number="16" hits="7" branch="True" condition-coverage="100% (2/2)">
              <conditions>
                <condition number="27" type="jump" coverage="100%" />
              </conditions>
            </line>
            <line number="17" hits="4" branch="False" />
            <line number="18" hits="4" branch="False" />
            <line number="20" hits="3" branch="False" />
            <line number="21" hits="4" branch="False" />
            <line number="23" hits="11" branch="False" />
          </lines>
        </class>
      </classes>
    </package>
  </packages>
</coverage>

Tipp

Alternativ könnten Sie das MSBuild-Paket verwenden, wenn Ihr Buildsystem bereits MSBuild verwendet. Wechseln Sie über die Eingabeaufforderung in das Verzeichnis des XUnit.Coverlet.MSBuild-Projekts, und führen Sie den dotnet test-Befehl aus:

dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura

Die resultierende coverage.cobertura.xml-Datei wird ausgegeben. Einen Leitfaden zur MSBuild-Integration finden Sie hier.

Generieren von Berichten

Da das Sammeln von Daten aus Komponententestläufen nun möglich ist, können Sie mithilfe von ReportGenerator Berichte generieren. Verwenden Sie den dotnet tool install-Befehl, um das NuGet-Paket ReportGenerator als globales .NET-Tool zu installieren:

dotnet tool install -g dotnet-reportgenerator-globaltool

Führen Sie das Tool aus, und geben Sie die gewünschten Optionen mit der coverage.cobertura.xml-Ausgabedatei aus dem vorherigen Testlauf an.

reportgenerator
-reports:"Path\To\TestProject\TestResults\{guid}\coverage.cobertura.xml"
-targetdir:"coveragereport"
-reporttypes:Html

Nachdem Sie diesen Befehl ausgeführt haben, stellt eine HTML-Datei den generierten Bericht dar.

Unit test-generated report

Siehe auch

Nächste Schritte