.NET Uygulamalarında Code Coverage Metriklerini Hesaplama ve CI Süreçlerine Dahil Etmek

Code coverage metriği, projemizin sağlıklı ve güvenilir olduğu ve ayrıca değişimlere karşı ne derece hızlı ayak uydurabileceğimizin önemli bir göstergesidir.

Code coverage temelinde, yazmış olduğumuz kod satırlarının ne kadarının test yazılarak doğrulandığını belirlemektedir. Tabii ki burada sadece code coverage metriğini arttırabilmek için yazabileceğimiz happy-path test senaryolarından bahsetmiyoruz. Çünkü code coverage metriğinin %100 olması, tüm edge case’leri cover ettiğimiz anlamına gelmiyor maalesef. Kolay maintain edilebilir ve sürdürülebilir bir ürün için, olabildiğince tüm functional senaryoları her açıdan unit test’ler yazarak cover ediyor olmamız gerekmektedir.

https://geek-and-poke.com/geekandpoke/2017/2/18/50-shades-part-2

Ayrıca unit test’lere sahip olmak, otomatik olarak bizleri daha clean ve loosely coupled bir codebase’ine sahip olmaya doğru da itmektedir. Çünkü unit test’ini yazacağımız bir function/method her ne kadar büyük ve kompleks ise, test’ini yazabilmek de bi o kadar zorlayıcı ve kompleks olacaktır.

Code coverage metrikleri ile birlikte de unit test’ler yazarken gözümüzden kaçan önemli kod blok’larını belirleyebilir, onları da test yazarak cover edebiliriz.

Ayrıca ilgili ekipler içerisinde bazı code coverage kuralları da belirleyerek unit test yazma kültürünü de sağlayabilir, ilgili projeler üzerindeki güven oranlarımızı da arttırabiliriz.

Bu makale kapsamında ise kısaca .NET uygulamalarımız için code coverage’ı nasıl hesaplayabileceğimizi ve Azure DevOps üzerinde Continuous Integration süreçlerimize nasıl entegre edebileceğimizi göstermeye çalışacağım.

Code Coverage

Code coverage’ı hesaplayabilmek için cross-platform code coverage framework’ü olan coverlet’i kullanacağız. Coverlet, .NET Core ve .NET 5 xUnit Test projeleri için default VSTest entegre data collector olarak gelmektedir.

Data collector’ler test execution işlemleri sırasında code coverage metriklerinin toplanması gibi farklı monitoring işlemlerini gerçekleştirmektedirler.

Code coverage metriklerini toplayabilmek için tek yapmamız gereken, –collect=”XPlat Code Coverage” parametresi ile dotnet test işlemini çalıştırmak.

Örnek amaçlı oluşturduğum test projesi üzerinde (buradan ulaşabileceğiniz) code coverage metriklerini toplayabilmek için yukarıdaki komutu çalıştırdığımda, coverlet aşağıdaki gibi default formatı olan Cobertura formatında metrikleri toplamaktadır.

Ayrıca aşağıdaki gibi coverlet.runsettings isminde bir dosyaya sahip olarak, metrik toplama işlemini özelleştirebilmek de mümkündür.

<?xml version="1.0" encoding="utf-8" ?>
<RunSettings>
  <DataCollectionRunSettings>
    <DataCollectors>
      <DataCollector friendlyName="XPlat Code Coverage">
        <Configuration>
          <Format>cobertura</Format>
          <ExcludeByFile>**/MyTodoApp.API/Startup.cs</ExcludeByFile>
        </Configuration>
      </DataCollector>
    </DataCollectors>
  </DataCollectionRunSettings>
</RunSettings>
dotnet test --settings coverlet.runsettings

Örneğin yukarıdaki configuration ile “MyTodoApp.API” klasörü altındaki “Startup.cs” dosyasının code coverage metriği için dikkate alınmamasını ve metrik formatının “cobertura” formatında olmasını sağladık.

Azure Pipeline

Azure üzerinde CI süreçlerimize code coverage ve test sonuçlarını dahil edebilmek için, aşağıdaki gibi basit bir pipeline oluşturalım.

# Starter pipeline
# Start with a minimal pipeline that you can customize to build and deploy your code.
# Add steps that build, run tests, deploy, and more:
# https://aka.ms/yaml

trigger:
- master

pool:
  vmImage: ubuntu-latest
  
variables:
  testProjectName: 'MyTodoApp.Tests'

steps:
- task: DotNetCoreCLI@2
  displayName: "Run dotnet restore"
  inputs:
    command: 'restore'
    projects: '**/$(testProjectName).csproj'
- task: DotNetCoreCLI@2
  displayName: "Run dotnet test"
  inputs:
    command: 'test'
    projects: '**/$(testProjectName).csproj'
    publishTestResults: false
    arguments: '--settings $(Build.Repository.LocalPath)/$(testProjectName)/coverlet.runsettings --logger trx'
- task: PublishTestResults@2
  displayName: "Publish test results"
  inputs:
    testResultsFormat: 'VSTest'
    testResultsFiles: '$(System.DefaultWorkingDirectory)/$(testProjectName)/TestResults/**/*.trx'
- task: PublishCodeCoverageResults@1
  displayName: "Publish code coverage results"
  inputs:
    codeCoverageTool: 'Cobertura'
    summaryFileLocation: '$(System.DefaultWorkingDirectory)/$(testProjectName)/TestResults/*/coverage.cobertura.xml'

Bu pipeline tanımlamasında,

- task: DotNetCoreCLI@2
  displayName: "Run dotnet test"
  inputs:
    command: 'test'
    projects: '**/$(testProjectName).csproj'
    publishTestResults: false
    arguments: '--settings $(Build.Repository.LocalPath)/$(testProjectName)/coverlet.runsettings --logger trx'

ilk olarak NuGet paketlerini “restore” ediyoruz ve sonra “dotnet test” komutunu, projemize göre tanımlamış olduğumuz “coverlet.runsettings” dosyası ile çalıştırıyoruz. Bu noktada ben ilgili sonuçların agent içerisinde bir temp klasörde oluşturulmasını istemediğim için, “publishTestResults” parametresini “false” olarak set ettim. Böylece ilgili sonuçlar, proje path’i altında “TestResults” klasörü içerisinde oluşturuluyor olacak.

- task: PublishTestResults@2
  displayName: "Publish test results"
  inputs:
    testResultsFormat: 'VSTest'
    testResultsFiles: '$(System.DefaultWorkingDirectory)/$(testProjectName)/TestResults/**/*.trx'

Bir önceki aşamada test sonuçlarını otomatik olarak publish ettirmediğimiz için, bu aşamada proje path’i altında oluşturulan test sonuçlarını “VSTest” formatında Azure Pipeline’a publish ediyoruz. Böylece detaylı test sonuçlarını da pipeline üzerinden görebileceğiz.

- task: PublishCodeCoverageResults@1
  displayName: "Publish code coverage results"
  inputs:
    codeCoverageTool: 'Cobertura'
    summaryFileLocation: '$(System.DefaultWorkingDirectory)/$(testProjectName)/TestResults/*/coverage.cobertura.xml'

Son aşama olarak ise cobertura formatında oluşturttuğumuz code coverage sonuçlarını, “PublishCodeCoverageResults” task’ını kullanarak Azure Pipeline’a publish ediyoruz.

Hepsi bu kadar!

Pipeline’ı çalıştırdığımızda, aşağıdaki gibi Summary kısmında projenin test ve coverage bilgilerine hızlı bir şekilde göz atabileceğiz.

Dilersek ilgili tab’lara tıklayarak, daha detaylı bilgiler de edinebiliriz.

 

Yukarıdaki sonuçlardan da görebildiğimiz gibi uygulamanın hangi kısımları içerisinde ne kadar bir code coverage oranına sahip olduğumuzu veya olabileceğimizi buradan detaylı bir şekilde görebiliyoruz.

Böylece iyi test edilmemiş veya gözümüzden kaçan kısımları kolay bir şekilde adresleyebilir, onları da cover edebiliriz. Ayrıca bir takım olarak belirli bir code coverage oranını kendimize kalite hedefi olarak da belirleyebiliriz. Böylece proje codebase’ini daha temiz bir noktaya getirirken, bizlerinde proje üzerindeki güven oranlarını yükseltebiliriz.

Code coverage kalite hedefini CI sürecine dahil edebilmek için ise, Build Quality Checks task’ından yararlanabiliriz.

Örneğin %95 lik bir kalite oranı belirleyebilmek için, yaml pipeline’a aşağıdaki gibi bir task ekleyebiliriz.

- task: BuildQualityChecks@8
  displayName: "Code coverage quality check"
  inputs:
    checkCoverage: true
    coverageFailOption: 'fixed'
    coverageType: 'lines'
    coverageThreshold: '95'

Bu task kısaca hesaplanan “line coverage” ve “threshold” oranlarına bakarak, build’in başarılı veya başarısız olmasını sağlamaktadır.

Örneğin,

makale için kullandığım test projesindeki code coverage değeri %95’in altında kaldığı için, build başarısız bir şekilde sonuçlandı. Böylece ekip içerisindeki test yazma kültürünü arttırabilir veya sahip olmaya zorlayabilir, codebase’in daima iyi bir şekilde test edilmiş ve temiz olmasını sağlayabiliriz.

Toparlayalım

Code coverage metriğine sahip olmanın önemi, projenin sağlığı ve ekip olarak projeye olan güven oranının arttırılması konusunda oldukça yüksektir. Bu güven oranının daha gerçekçi bir oran olabilmesi için de, test case’lerine sahip olurken olabildiğince edge case’leri de dikkate alarak hareket ediyor olmamız gerekmektedir.

Bu makale kapsamında ise code coverage metriklerini Coverlet ile nasıl toplayabileceğimizi ve Azure üzerinde CI süreçlerimize nasıl dahil edebileceğimizi göstermeye çalıştım.

Github: https://github.com/GokGokalp/dotnet5codecoverage

Referanslar

https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/test/publish-code-coverage-results?view=azure-devops
https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/test/publish-test-results?view=azure-devops&tabs=trx%2Cyaml
https://github.com/coverlet-coverage/coverlet/blob/master/Documentation/VSTestIntegration.md

Gökhan Gökalp

Recent Posts

Containerized Uygulamaların Supply Chain’ini Güvence Altına Alarak Güvenlik Risklerini Azaltma (Güvenlik Taraması, SBOM’lar, Artifact’lerin İmzalanması ve Doğrulanması) – Bölüm 1

{:tr}Bildiğimiz gibi modern yazılım geliştirme ortamında containerization'ın benimsenmesi, uygulamaların oluşturulma ve dağıtılma şekillerini oldukça değiştirdi.…

9 ay ago

Identity & Access Management İşlemlerini Azure AD B2C ile .NET Ortamında Gerçekleştirmek

{:tr}Bildiğimiz gibi bir ürün geliştirirken olabildiğince farklı cloud çözümlerinden faydalanmak, harcanacak zaman ve karmaşıklığın yanı…

1 yıl ago

Azure Service Bus Kullanarak Microservice’lerde Event’ler Nasıl Sıralanır (FIFO Consumers)

{:tr}Bazen bazı senaryolar vardır karmaşıklığını veya eksi yanlarını bildiğimiz halde implemente etmekten kaçamadığımız veya implemente…

2 yıl ago

.NET Microservice’lerinde Outbox Pattern’ı ile Eventual Consistency için Atomicity Sağlama

{:tr}Bildiğimiz gibi microservice architecture'ına adapte olmanın bir çok artı noktası olduğu gibi, maalesef getirdiği bazı…

2 yıl ago

Dapr ve .NET Kullanarak Minimum Efor ile Microservice’ler Geliştirmek – 02 (Azure Container Apps)

{:tr}Bir önceki makale serisinde Dapr projesinden ve faydalarından bahsedip, local ortamda self-hosted mode olarak .NET…

2 yıl ago