Məzmuna keç
Educora
İrəli21 dəq12 / 16

xUnit ilə testləşdirmə

xUnit ilə unit testlər yaz: `[Fact]`, `[Theory]` və `[InlineData]`, yoxlamalar, FluentAssertions üslubu və Moq ilə asılılıqların əvəz edilməsi.

Özünü yoxla
Bu dərsdə öyrənəcəksən
  • [Fact] və Assert metodları ilə test sinfi yazmaq
  • [Theory] və [InlineData] ilə bir testi çoxlu verilənlərlə işə salmaq
  • Moq ilə interfeysi əvəz etmək və çağırışları yoxlamaq

Java kursunda JUnit ilə tanış olmusansa, burada çox şey tanış görünəcək: C#-da da testlər kodu çağıran və nəticəni gözlənilən qiymətlə müqayisə edən kiçik metodlardır. .NET dünyasında ən populyar test freymvorkları xUnit, NUnit və MSTest-dir; dotnet new xunit şablonu və ASP.NET Core-un öz mənbə kodu xUnit-dən istifadə edir. Əvvəlcə testin ideyasını heç bir kitabxana olmadan görək:

C#
(int Score, string Expected)[] cases = [(95, "A"), (80, "B"), (72, "C"), (49, "F")];
int passed = 0;

foreach (var (score, expected) in cases)
{
    string actual = Letter(score);
    bool ok = actual == expected;
    if (ok) passed++;
    Console.WriteLine($"{(ok ? "PASS" : "FAIL")} Letter({score}) = {actual}, expected {expected}");
}
Console.WriteLine($"{passed} of {cases.Length} passed");

// a bug hides here: > 80 instead of >= 80
static string Letter(int score) => score switch
{
    >= 90 => "A",
    > 80 => "B",
    >= 70 => "C",
    >= 50 => "D",
    _ => "F"
};
Gözlənilən nəticə
PASS Letter(95) = A, expected A
FAIL Letter(80) = C, expected B
PASS Letter(72) = C, expected C
PASS Letter(49) = F, expected F
3 of 4 passed
Testin mahiyyəti: bir yoxlama, çoxlu verilənlər — sərhəddəki səhv dərhal üzə çıxır

Test layihəsi və [Fact]

Əvvəlki dərsdə dotnet new xunit -n Gradebook.Tests ilə test layihəsi yaratmışdıq. .NET 10-un şablonu xunit, xunit.runner.visualstudio və Microsoft.NET.Test.Sdk paketlərini əlavə edir və Xunit ad fəzasını qlobal using edir. Test sinfi adi public sinifdir, heç bir atribut tələb etmir. Parametrsiz test metodu [Fact] ilə işarələnir — «həmişə doğru olan fakt». Adlar çox vaxt Method_Scenario_Expected qaydası ilə yazılır, testin özü isə AAA (Arrange, Act, Assert) quruluşuna malikdir.

C#
using Gradebook.Core;

namespace Gradebook.Tests;

public class GradeCalculatorTests
{
    [Fact]
    public void Average_OfThreeScores_ReturnsMean()
    {
        int[] scores = [85, 90, 95];                          // Arrange
        double result = GradeCalculator.Average(scores);      // Act
        Assert.Equal(90.0, result, precision: 3);             // Assert
    }

    [Fact]
    public void Average_OfEmptyList_Throws()
    {
        var ex = Assert.Throws<ArgumentException>(() => GradeCalculator.Average([]));
        Assert.Equal("No scores", ex.Message);
    }

    [Theory]
    [InlineData(95, "A")]
    [InlineData(80, "B")]
    [InlineData(72, "C")]
    [InlineData(49, "F")]
    public void Letter_ReturnsExpectedGrade(int score, string expected)
    {
        Assert.Equal(expected, GradeCalculator.Letter(score));
    }
}
Gradebook.Tests/GradeCalculatorTests.cs — GradeCalculator sinfi əvvəlki dərsdəndir

Assert.Equal(expected, actual) əsas yoxlamadır; həqiqi ədədlər üçün precision onluq işarələrin sayını göstərir. Assert.Throws<T> istisnanın tipini yoxlayır və onu qaytarır. Digər faydalı metodlar: Assert.True, Assert.Null, Assert.Contains, Assert.Empty, Assert.InRange. xUnit hər test üçün sinfin yeni nüsxəsini yaradır: hazırlıq kodu konstruktora, təmizlik isə IDisposable.Dispose() metoduna yazılır — ayrıca [SetUp] atributları yoxdur.

JUnit 5xUnitMənası
@Test[Fact]bir test
@ParameterizedTest + @CsvSource[Theory] + [InlineData]verilənlərlə test
@BeforeEach / @AfterEachkonstruktor / Dispose()hər testdən əvvəl / sonra
@BeforeAllIClassFixture<T>bütün sinif üçün ortaq hazırlıq
assertThrowsAssert.Throws<T>istisnanın yoxlanması

Testləri dotnet test işə salır (Visual Studio və Rider-də isə Test Explorer pəncərəsi). Tutaq ki, yuxarıdakı nümunədəki kimi Letter metodunda >= 80 səhvən > 80 olub:

Terminal
dotnet test
Gözlənilən nəticə
Failed Gradebook.Tests.GradeCalculatorTests.Letter_ReturnsExpectedGrade(score: 80, expected: "B") [2 ms]
  Error Message:
   Assert.Equal() Failure: Strings differ
Expected: "B"
Actual:   "C"

Failed!  - Failed:     1, Passed:     5, Skipped:     0, Total:     6, Duration: 41 ms - Gradebook.Tests.dll (net10.0)
Nümunə nəticə (qısaldılıb): [InlineData(80, "B")] halı səhvi tutdu

[Theory] və verilənlər

[Theory] bir testi müxtəlif verilənlərlə işə salır: hər [InlineData] atributu ayrıca test halıdır və hesabatda öz parametrləri ilə görünür — yuxarıda altı test halı var idi. Verilənlər atributa sığmırsa (məsələn, obyektlər və ya siyahılar), [MemberData(nameof(Cases))] onları statik xassədən götürür. Asinxron kodu test edəndə isə metod mütləq async Task qaytarmalıdır:

Səhv: `async void`
[Fact]
public async void LoadsScores()          // async void: the runner cannot await it
{
    var scores = await repository.LoadAsync("Aysel");
    Assert.NotEmpty(scores);
}
Düzgün: `async Task`
[Fact]
public async Task LoadsScores()          // async Task: xUnit awaits the result
{
    var scores = await repository.LoadAsync("Aysel");
    Assert.NotEmpty(scores);
}
Test icraçısı async void metodunu gözləyə bilmir, ona görə onun xətası itə və ya başqa yerdə üzə çıxa bilər

Moq ilə asılılıqları əvəz etmək

ReportService balları IScoreRepository interfeysindən alır; real proqramda onun arxasında verilənlər bazası dayanır. Unit testdə bazaya ehtiyac yoxdur: Moq kitabxanası (dotnet add package Moq) interfeysin saxta obyektini — mock-u yaradır. Asılılıq sinfə konstruktordan gəlir; burada o, C# 12-nin əsas konstruktoru (primary constructor) ilə yazılıb.

C#
public interface IScoreRepository
{
    IReadOnlyList<int> GetScores(string student);   // e.g. reads a database
}

public class ReportService(IScoreRepository repository)
{
    public string Summary(string student)
    {
        double average = GradeCalculator.Average(repository.GetScores(student));
        return $"{student}: {GradeCalculator.Letter((int)average)}";
    }
}
Servis konkret bazanı yox, interfeysi tanıyır
C#
using Moq;

public class ReportServiceTests
{
    [Fact]
    public void Summary_UsesScoresFromRepository()
    {
        var repo = new Mock<IScoreRepository>();
        repo.Setup(r => r.GetScores("Aysel")).Returns(new List<int> { 90, 80 });

        var service = new ReportService(repo.Object);
        string summary = service.Summary("Aysel");

        Assert.Equal("Aysel: B", summary);
        repo.Verify(r => r.GetScores("Aysel"), Times.Once);
    }
}
Moq ilə test: saxta repozitori 90 və 80 qaytarır, orta 85 — «B»

new Mock<IScoreRepository>() saxta obyekt yaradır, Setup(...).Returns(...) ona cavabı öyrədir, repo.Object isə servisə ötürülən hazır nüsxədir. Verify(..., Times.Once) metodun məhz bir dəfə çağırıldığını yoxlayır. Moq yalnız interfeysləri və virtual metodları əvəz edə bilər — asılılıqları interfeys kimi elan etməyin bir səbəbi də budur. Populyar alternativ kitabxana NSubstitute-dur.

FluentAssertions ideyası yoxlamaları cümlə kimi oxunan zəncirlərə çevirməkdir: summary.Should().Be(...). Səhv mesajları da daha ətraflı olur. Diqqət: FluentAssertions 8-ci versiyadan (2025) başlayaraq kommersiya layihələri üçün ödənişli lisenziya tələb edir; pulsuz alternativ kimi, məsələn, Shouldly kitabxanası var.

Klassik `Assert`
Assert.Equal("Aysel: B", summary);
Assert.Equal(3, scores.Count);
Assert.Contains(95, scores);
FluentAssertions üslubu
summary.Should().Be("Aysel: B");
scores.Should().HaveCount(3).And.Contain(95);

Nəyi test etməli? Test piramidası qaydası deyir: ən çox sürətli unit testlər (bir sinif, saxta asılılıqlar), daha az inteqrasiya testləri (həqiqi baza və ya veb-server ilə) və ən az bütöv sistemi yoxlayan UI testləri. Kodun nə qədərinin testlərlə əhatə olunduğunu şablondakı coverlet.collector paketi ölçür: dotnet test --collect:"XPlat Code Coverage" hesabat faylı yaradır. Amma 100% əhatə məqsəd deyil — sərhəd halları və səhvə meylli yerlər daha vacibdir.

Əsas fikirlər

  • xUnit: [Fact] — parametrsiz test, [Theory] + [InlineData] — verilənlərlə test.
  • Assert.Equal(expected, actual), Assert.Throws<T>, Assert.Contains — əsas yoxlamalar.
  • Hər test yeni obyektdə işləyir: hazırlıq konstruktorda, təmizlik Dispose()-dadır.
  • Asinxron testlər async Task qaytarır, heç vaxt async void.
  • Moq: Setup(...).Returns(...) cavabı öyrədir, .Object saxta nüsxəni verir, Verify çağırışı yoxlayır.

Özünü yoxla

10 sual. Hər düzgün cavab XP qazandırır.

1 / 10
Parametrləri [InlineData]-dan gələn test hansı atributla işarələnir?