[Fact]vəAssertmetodları 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:
(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"
};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
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.
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əndirAssert.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 5 | xUnit | Mənası |
|---|---|---|
@Test | [Fact] | bir test |
@ParameterizedTest + @CsvSource | [Theory] + [InlineData] | verilənlərlə test |
@BeforeEach / @AfterEach | konstruktor / Dispose() | hər testdən əvvəl / sonra |
@BeforeAll | IClassFixture<T> | bütün sinif üçün ortaq hazırlıq |
assertThrows | Assert.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:
dotnet testFailed 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)
[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:
[Fact]
public async void LoadsScores() // async void: the runner cannot await it
{
var scores = await repository.LoadAsync("Aysel");
Assert.NotEmpty(scores);
}[Fact]
public async Task LoadsScores() // async Task: xUnit awaits the result
{
var scores = await repository.LoadAsync("Aysel");
Assert.NotEmpty(scores);
}async void metodunu gözləyə bilmir, ona görə onun xətası itə və ya başqa yerdə üzə çıxa bilərMoq 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.
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)}";
}
}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);
}
}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.
Assert.Equal("Aysel: B", summary);
Assert.Equal(3, scores.Count);
Assert.Contains(95, scores);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 Taskqaytarır, heç vaxtasync void. - Moq:
Setup(...).Returns(...)cavabı öyrədir,.Objectsaxta nüsxəni verir,Verifyçağırışı yoxlayır.
Özünü yoxla
10 sual. Hər düzgün cavab XP qazandırır.
[InlineData]-dan gələn test hansı atributla işarələnir?