xUnit ile kodumuzu test edelim.

xUnit ile kodumuzu test edelim.

Geçenlerde çok sevdiğim bir arkadaşım geldi ve beraber bir wallet uygulaması geliştirelim dedi. Tamam olur yapalım dedim ve uygulamayı geliştirebilmek için mobil tarafta flutter ve backend tarafta .net core kullanmayı tercih ettik. İş planını yaptık ve arkadaşım flutter ile uygulamayı geliştirirken ben de .net core ile rest api servislerini geliştirecektim. Uygulamamızın amacı bir kullanıcı borçlarını takip edebileceği bir ortam oluşturmaktı. Kredi borçlarını, kredi kartı taksit ödemelerini ya da düzenli ödediği ne kadar ödeme varsa hepsini bir yerde görebildiği, grafikler ile renklendirilmiş raporları takip edebildiği bir uygulama olacaktı.

Uygulamayı konuştukça bir yandan da kafamda ilgili entity’leri düşünüyordum. Kullanıcılar için bir User entity, borçlar için bir Loan entity olacaktı. İlk amacımız kullanıcıları email ve şifre ile giriş yapmalarını sağlamaktı. Bunun için kayıt ol, giriş yap, şifremi unuttum gibi temel servisleri yazdık. İkinci aşamamız ise kullanıcı bir borç (loan) oluşturabilmesiydi. Arkadaşımla bir borç oluşturulurken hangi parametreleri almalı ve kayıt gerçekleşirken iş kurallarımız ne olacak onları belirledik.

  • Loan Title: minimum 3 karakter ve maksimum 120 karakter olabilir.
  • Loan Description: boş olabilir ve maksimum 255 karakter olabilir.
  • Loan Type : boş olamaz, bizim sistem içerisinde belirlediğimiz loan type listesinin içerisinde olmalı.
  • Total Amount: boş olamaz ve negatif bir değer olamaz.
  • Total Currency: boş olamaz ve bizim sistem içerisinde belirlediğimiz currency type listesinin içerisinde olmalı.
  • Bir kullanıcı ücretsiz pakette maksimum 5 tane borç kaydı açabilir.

Bu kurallara göre bir loan entity yazmam gerekiyordu. Proje küçük bir proje olmasına rağmen ben bu projede birim testlerini yazmak istedim. Birim testler yani unit test, kodumuzu test etmek için kullandığımız bir yöntemdir. Unit testler kodumuzdaki en küçük birimi test eder olarak öğreniriz ve bu şekilde uygulamaya çalışırız. Birim testler aslında kodumuzdaki bir davranışı (use case) bağımlılıklar olmadan test etmelidir. Projemizdeki her en küçük birim için testler yazarsak, kodumuzun hareket kabiliyetini kısıtlarız, kodu kendi ellerimiz ile kitleriz. Bir yerden sonra refactor ederken bile karşımızda direnç gösteren testler ile karşı karşıya kalırız. Bizim amacımız iş kurallarımızın doğru şekilde çalıştığına emin olmak ve bağımlılıklardan sıyrılmış birim testler yazmak olmalıdır.

Bu uygulamada yeni borç oluşturmak bir davranıştır ve bunu bağımlılıklar olmadan birim testler ile test etmeliyiz. Borcu kapamak ya da bir taksidini ödemek de uygulamadaki diğer davranışlardır. Hepsi uygulamadaki kullanıcı ile etkileşime geçen davranışlardır. Bizim amacımız bu davranışları birim testler ile bağımlılıklar olmadan test edebilmektir.

Daha fazla uzatmadan kolları sıvayıp servislerimizi geliştirmeye başlayalım. Projemizin solution oluşturmak için terminalden aşağıdaki kodu yazdım.

dotnet new sln -o Wallet

Bu kod Wallet klasörü içerisine boş bir solution açar. Wallet klasörü içerisine girelim. Şimdi projemizin domain katmanını oluşturmak ve solution ile bağlamak için terminale aşağıdaki kodu yazılım.

dotnet new classlib -o Wallet.Domain
dotnet sln add .\Wallet.Domain\

Projemizin application katmanını oluşturmak ve solution ile bağlamak için için aşağıdaki kodu yazalım.

dotnet new classlib -o Wallet.Application
dotnet sln add .\Wallet.Application\

Application katmanındaki servislerimizi test etmek için application test katmanını oluşturalım ve solution ile bağlayalım.

dotnet new xunit -o Wallet.Application.Tests
dotnet sln add .\Wallet.Application.Tests\

Son olarak application katmanının domain katmanına erişebilmesi ve application test katmanının application katmanına erişebilmesi için referansları ekleyelim.

dotnet add .\Wallet.Application.Tests\ reference .\Wallet.Application\
dotnet add .\Wallet.Application\ reference .\Wallet.Domain\

Projemizin gerekli kurulumları yaptıktan sonra ilk servisimiz olan borç oluşturma servisini yazabiliriz.

Servisimizi yazmadan önce servisimizin bir borç oluşturabilmek için ihtiyaç duyduğu parametreleri saklayan bir veri yapısına ihtiyacımız var. Bunun için aşağıdaki CreateLoanInput recordunu oluşturdum.

using Wallet.Domain.Users;

namespace Wallet.Application.Loans.CreateLoan;

public sealed record CreateLoanInput( string Title,
string Description,
string LoadTypeCode,
decimal TotalAmount,
string CurrencyCode,
User User);

Daha sonra ICreateLoanService interfacesini oluşturdum.

using Wallet.Domain.Loans;

namespace Wallet.Application.Loans.CreateLoan;

public interface ICreateLoanService
{
Task Handle(CreateLoanInput input, CancellationToken cancellationToken);
}

Neden bir interface ihtiyacımız var sorusu akıllarda oluşmuş olabilir. Projelerde amacımız bağımlılıkları mümkün oldukça soyutlamaktır. Somut yapılar projenin parçalarını birbirleri ile sıkı sıkıya bağlar ve birbirinden ayrılmasını zorlaştırır. Soyut yapılar ile bu bağımlılıkları gevşek tutmaya çalışırız, böylelikle projenin değişime olan direncini azaltmış oluruz. Bir örnek vermek gerekirse, herkes sıklıkla e-ticaret platformlarından ya da bankalardan ürünler, kampanyalar hakkında sms ve email yoluyla bilgilendirme alıyordur. Zaman zaman bu platformlardan email, sms ile bilgilendirme ayarlarınızı değiştiriyor olabilirsiniz. Bu durumda sistem sizin talebinize göre sizi bilgilendirme yöntemlerini değiştiriyor. Peki bu değişimi nasıl yönetiyor olabilirler? Ben olsam şöyle bir çözüm üretiyor olabilirdim;

public sealed record NotifyUserInput(User User, string Title, string Description);

public interface INotifyUserService {
Task Notify(NotifyUserInput input);
}

public sealed class NotifySmsUserService : INotifyUserService
{
public Task Notify(NotifyUserInput input)
{
//notify with sms api
}
}

public sealed class NotifyEmailUserService : INotifyUserService
{
public Task Notify(NotifyUserInput input)
{
//notify with email api
}
}

public sealed class NotifyNothingUserService: INotifyUserService
{
public Task Notify(NotifyUserInput input)
{
//do not notify user
}
}

Burada bir INotifyUserService interface oluşturdum. Bu interface 3 farklı sınıfa uyguladım. Kullanıcı duruma göre sms ile bilgilendirilmek isterse NotifySmsUserService, email ile bilgilendirilmek isterse NotifyEmailUserService, herhangi bir şekilde bilgilendirmek istemiyorsa NotifyNothingUserService servisini çağırabilirim.

public sealed class SendNewProductToCustomerService
{
private readonly INotifyUserService _notifyUserService;
public User User { get; private set; }
public Product Product { get; private set; }

public SendNewProductToCustomerService( User user,
Product product,
INotifyUserService notifyUserService )
{
User = user;
Product = product;
_notifyUserService = notifyUserService;
}

public Task Handle()
{
// handle it
}
}

// notify with email
var sendNewProductToCustomerService = new SendNewProductToCustomerService(
user: User,
product: Product,
notifyUserService: new NotifyEmailUserService()
);

// notify with sms
var sendNewProductToCustomerService = new SendNewProductToCustomerService(
user: User,
product: Product,
notifyUserService: new NotifySmsUserService()
);

// do not notify
var sendNewProductToCustomerService = new SendNewProductToCustomerService(
user: User,
product: Product,
notifyUserService: new NotifyNothingUserService()
);

Yukarıdaki örnekte gördüğünüz gibi SendNewProductToCustomerService servisi INotifyUserService interfacesine bağlı olduğu için INotifyUserService interfacesini uygulayan bir çok farklı sınıf ile birlikte çalışabilir hale geldi. Neyse konumuza geri dönelim. En son ICreateLoanService interfacemizi oluşturmuştuk. Sıra geldi CreateLoanService sınıfını oluşturalım ve ICreateLoanService interfacesini uygulayalım.

using Wallet.Domain.Loans;

namespace Wallet.Application.Loans.CreateLoan;

public sealed class CreateLoanService : ICreateLoanService
{
public Task Handle(CreateLoanInput input)
{
throw new NotImplementedException();
}
}

Şimdi iş kurallarımızı kontrol eden kodumuzu yazalım.

using Wallet.Domain.Abstractions;
using Wallet.Domain.Loans;

namespace Wallet.Application.Loans.CreateLoan;

public sealed class CreateLoanService : ICreateLoanService
{
private readonly IUnitOfWork _unitOfWork;
private readonly ILoanRepository _loanRepository;

public CreateLoanService(IUnitOfWork unitOfWork, ILoanRepository loanRepository)
{
_unitOfWork = unitOfWork;
_loanRepository = loanRepository;
}

public async Task Handle(CreateLoanInput input, CancellationToken cancellationToken)
{
var user = input.User;
var title = input.Title;
var description = input.Description;
var loadTypeCode = input.LoadTypeCode;
var currencyCode = input.CurrencyCode;
var totalAmount = input.TotalAmount;

var currency = Currency.GetByCode(currencyCode);
var amount = Money.Create(totalAmount, currency);
var loanType = LoanType.GetByCode(loadTypeCode);

if (!user.IsPremium)
{
var totalLoanCount = await _loanRepository.GetTotalLoanCountByUserId(user.Id);
if (totalLoanCount >= 5)
{
throw new MaxLoanCountException();
}
}

var loan = Loan.Create(user, title, description, loanType, amount);

await _loanRepository.Add(loan);
await _unitOfWork.SaveChangesAsync(cancellationToken);

return loan;
}
}

Burada CreateLoanService oluşturulurken 2 adet parametreye ihtiyaç duyuyor. Burada da gördüğünüz gibi CreateLoanService interfacelere bağlı. IUnitOfWork ve ILoanRepository implement eden sınıfların nesneleri bir kullanıcının toplam kayıtlı loan sayısını döndürmek ve yeni bir loan kaydını veritabanına kaydetmek için kullanacağız. Borç türü, borç tutarı ve borcun para birimini yönetebilmek için LoanType, Currency ve Money value objelerini oluşturdum. Ayrıca kullanıcıyı temsil edecek User entity sınıfını oluşturdum.

using Wallet.Domain.Abstractions;

namespace Wallet.Domain.Users;

public sealed class User : Entity
{
public string FirstName { get; private set; }
public string LastName { get; private set; }
public string Email { get; private set; }
public string Password { get; private set; }
public bool IsPremium { get; private set; }

private User(Guid id, string firstName, string lastName, string email, string password, bool isPremium) : base(id, DateTime.UtcNow)
{
FirstName = firstName;
LastName = lastName;
Email = email;
Password = password;
IsPremium = isPremium;
}

public static User Create(string firstName, string lastName, string email, string password, bool isPremium)
{
var user = new User(Guid.NewGuid(), firstName, lastName, email, password, isPremium);

return user;
}
}

namespace Wallet.Domain.Abstractions;

public interface IUnitOfWork
{
Task SaveChangesAsync(CancellationToken cancellationToken = default);
}

namespace Wallet.Domain.Loans;

public interface ILoanRepository
{
public Task GetTotalLoanCountByUserId(Guid userId);
public Task Add(Loan loan);
}

namespace Wallet.Domain.Loans;

public sealed record Currency
{
public static readonly Currency Try = new("TRY");
public static readonly Currency Eur = new("EUR");
public static readonly Currency Usd = new("USD");
public static readonly Currency Gbp = new("GBP");
public static readonly IReadOnlyCollection All = new[] { Try, Eur, Usd, Gbp };

public string Code { get; init; }

private Currency(string code) => Code = code;

public static Currency GetByCode(string code)
{
return All.FirstOrDefault(p => p.Code == code) ?? throw new InvalidCurrencyException();
}
}

namespace Wallet.Domain.Loans;

public sealed record LoanType
{
public static readonly LoanType PersonalFinanceCredit = new("PersonalFinanceCredit");
public static readonly LoanType Mortgage= new ("Mortgage");
public static readonly LoanType CreditCardInstallment = new("CreditCardInstallment");
public static readonly IReadOnlyCollection All = new[] { PersonalFinanceCredit, Mortgage, CreditCardInstallment };

public string Code { get; init; }

private LoanType(string code)
{
Code = code;
}

public static LoanType GetByCode(string code)
{
return All.FirstOrDefault(p => p.Code == code) ?? throw new InvalidLoanTypeException();
}
}

namespace Wallet.Domain.Loans;

public sealed record Money
{
public decimal Amount { get; init; }
public Currency Currency { get; init; }

private Money(decimal amount, Currency currency)
{
Amount = amount;
Currency = currency;
}

public static Money Create(decimal amount, Currency currency)
{
if (amount < 0)
{
throw new AmountMustBePositiveException();
}

return new Money(amount, currency);
}

private static void Validate(Money first, Money second)
{
if (first.Currency != second.Currency)
{
throw new CurrenciesMustBeSameException();
}

if (first.Amount < 0)
{
throw new AmountMustBePositiveException();
}

if (second.Amount < 0)
{
throw new AmountMustBePositiveException();
}

if (first.Amount >= int.MaxValue)
{
throw new InvalidAmountException();
}

if (second.Amount >= int.MaxValue)
{
throw new InvalidAmountException();
}
}

public static Money operator +(Money first, Money second)
{
Validate(first, second);

var tempSum = first.Amount + second.Amount;

if (tempSum >= int.MaxValue)
{
throw new InvalidAmountException();
}

var tempDiff = tempSum - first.Amount;

if (tempDiff != second.Amount)
{
throw new InvalidAmountException();
}

return new Money(first.Amount + second.Amount, first.Currency);
}

public static Money operator -(Money first, Money second)
{
Validate(first, second);

var diff = first.Amount - second.Amount;

if (diff < 0)
{
throw new AmountMustBePositiveException();
}

return new Money(diff, first.Currency);
}
}

Aşağıda da Loan entity sınıfını görebilirsin. Yeni bir borç nesnesi oluşturmak için constructor kullanmıyorum. Bunun yerine Create methodunu çağırıyorum.

using Wallet.Domain.Abstractions;
using Wallet.Domain.Users;

namespace Wallet.Domain.Loans;

public sealed class Loan: Entity
{
public Guid UserId { get; private set; }
public string Title { get; private set; }
public string Description { get; private set; }
public LoanType LoanType { get; private set; }
public Money TotalAmount { get; private set; }

private Loan(Guid id, Guid userId, string title, string description, LoanType loanType, Money totalAmount) : base(id, DateTime.UtcNow)
{
UserId = userId;
SetTitle(title);
SetDescription(description);
LoanType = loanType;
TotalAmount = totalAmount;
}

public static Loan Create(User user,
string title, string description, LoanType loanType, Money totalAmount)
{
var userId = user.Id;

var loan = new Loan(Guid.NewGuid(), userId, title, description, loanType, totalAmount);
return loan;
}

internal void SetTitle(string title) {
var trimTitle = title.Trim();
if (string.IsNullOrEmpty(trimTitle))
{
throw new RequiredLoanTitleException();
}

if (trimTitle.Length < 3)
{
throw new MinLengthLoanTitleException();
}

if (trimTitle.Length > 120)
{
throw new MaxLengthLoanTitleException();
}
Title = title;
}

internal void SetDescription(string description) {
var trimDescription = description.Trim();
if (trimDescription.Length > 255)
{
throw new MaxLengthLoanDescriptionException();
}
Description = description;
}
}

Bir loan nesnesi oluşturmak için gerekli olan tüm geliştirmeyi yaptık. Şimdi testlerini yazalım.

using Wallet.Application.Loans.CreateLoan;
using Wallet.Domain.Loans;
using Wallet.Domain.Users;

namespace Wallet.Application.Tests.Loans.CreateLoan;

public class CreateLoanServiceTests
{
[Fact]
public async Task Given_Empty_Title_Throw_RequiredLoanTitleException()
{
var user \= User.Create("Serhat", "Yavaş", "serhatleventyavas@gmail.com", "test123", true);
var input \= new CreateLoanInput(
User: user,
Title: "",
Description: "",
LoadTypeCode: LoanType.Mortgage.Code,
TotalAmount: 10_000,
CurrencyCode: Currency.Try.Code
);
var service \= new CreateLoanService(new FakeUnitOfWork(), new FakeLoanRepository());

await Assert.ThrowsAsync(() => service.Handle(input, new CancellationToken()));

}

[Fact]
public async Task Given_2_Chars_Title_Throw_MinLengthLoanTitleException()
{
var user \= User.Create("Serhat", "Yavaş", "serhatleventyavas@gmail.com", "test123", true);
var input \= new CreateLoanInput(
User: user,
Title: new string('*',2),
Description: "",
LoadTypeCode: LoanType.Mortgage.Code,
TotalAmount: 10_000,
CurrencyCode: Currency.Try.Code
);
var service \= new CreateLoanService(new FakeUnitOfWork(), new FakeLoanRepository());

await Assert.ThrowsAsync(() => service.Handle(input, new CancellationToken()));

}

[Fact]
public async Task Given_121_Chars_Title_Throw_MaxLengthLoanTitleException()
{
var user \= User.Create("Serhat", "Yavaş", "serhatleventyavas@gmail.com", "test123", true);
var input \= new CreateLoanInput(
User: user,
Title: new string('*', 121),
Description: "",
LoadTypeCode: LoanType.Mortgage.Code,
TotalAmount: 10_000,
CurrencyCode: Currency.Try.Code
);
var service \= new CreateLoanService(new FakeUnitOfWork(), new FakeLoanRepository());

await Assert.ThrowsAsync(() => service.Handle(input, new CancellationToken()));

}

[Fact]
public async Task Given_256_Chars_Description_Throw_MaxLengthLoanDescriptionException()
{
var user \= User.Create("Serhat", "Yavaş", "serhatleventyavas@gmail.com", "test123", true);
var input \= new CreateLoanInput(
User: user,
Title: "Title",
Description: new string('*', 256),
LoadTypeCode: LoanType.Mortgage.Code,
TotalAmount: 10_000,
CurrencyCode: Currency.Try.Code
);
var service \= new CreateLoanService(new FakeUnitOfWork(), new FakeLoanRepository());

await Assert.ThrowsAsync(() => service.Handle(input, new CancellationToken()));
}

[Fact]
public async Task Given_Empty_Loan_Type_Code_Throw_InvalidLoanTypeException()
{
var user \= User.Create("Serhat", "Yavaş", "serhatleventyavas@gmail.com", "test123", true);
var input \= new CreateLoanInput(
User: user,
Title: "Title",
Description: "Description",
LoadTypeCode: "",
TotalAmount: 10_000,
CurrencyCode: Currency.Try.Code
);
var service \= new CreateLoanService(new FakeUnitOfWork(), new FakeLoanRepository());

await Assert.ThrowsAsync(() => service.Handle(input, new CancellationToken()));
}

[Fact]
public async Task Given_Not_Exists_Code_Loan_Type_Code_Throw_InvalidLoanTypeException()
{
var user \= User.Create("Serhat", "Yavaş", "serhatleventyavas@gmail.com", "test123", true);
var input \= new CreateLoanInput(
User: user,
Title: "Title",
Description: "Description",
LoadTypeCode: "invalidCode",
TotalAmount: 10_000,
CurrencyCode: Currency.Try.Code
);
var service \= new CreateLoanService(new FakeUnitOfWork(), new FakeLoanRepository());

await Assert.ThrowsAsync(() => service.Handle(input, new CancellationToken()));
}

[Fact]
public async Task Given_Empty_Currency_Code_Throw_InvalidCurrencyException()
{
var user \= User.Create("Serhat", "Yavaş", "serhatleventyavas@gmail.com", "test123", true);
var input \= new CreateLoanInput(
User: user,
Title: "Title",
Description: "Description",
LoadTypeCode: LoanType.Mortgage.Code,
TotalAmount: 10_000,
CurrencyCode: ""
);
var service \= new CreateLoanService(new FakeUnitOfWork(), new FakeLoanRepository());

await Assert.ThrowsAsync(() => service.Handle(input, new CancellationToken()));
}

[Fact]
public async Task Given_Not_Exists_Currency_Code_Throw_InvalidCurrencyException()
{
var user \= User.Create("Serhat", "Yavaş", "serhatleventyavas@gmail.com", "test123", true);
var input \= new CreateLoanInput(
User: user,
Title: "Title",
Description: "Description",
LoadTypeCode: LoanType.Mortgage.Code,
TotalAmount: 10_000,
CurrencyCode: "invalid_code"
);
var service \= new CreateLoanService(new FakeUnitOfWork(), new FakeLoanRepository());

await Assert.ThrowsAsync(() => service.Handle(input, new CancellationToken()));
}

[Fact]
public async Task Given_Negative_Amount_Throw_AmountMustBePositiveException()
{
var user \= User.Create("Serhat", "Yavaş", "serhatleventyavas@gmail.com", "test123", true);
var input \= new CreateLoanInput(
User: user,
Title: "Title",
Description: "Description",
LoadTypeCode: LoanType.Mortgage.Code,
TotalAmount: -10_000,
CurrencyCode: Currency.Try.Code
);
var service \= new CreateLoanService(new FakeUnitOfWork(), new FakeLoanRepository());

await Assert.ThrowsAsync(() => service.Handle(input, new CancellationToken()));
}

[Fact]
public async Task Given_Freemium_User_With_5_Loans_Total_Throw_MaxLoanCountException()
{
var user \= User.Create("Serhat", "Yavaş", "serhatleventyavas@gmail.com", "test123", false);
var input \= new CreateLoanInput(
User: user,
Title: "Title",
Description: "Description",
LoadTypeCode: LoanType.Mortgage.Code,
TotalAmount: 10_000,
CurrencyCode: Currency.Try.Code
);
var service \= new CreateLoanService(new FakeUnitOfWork(), new FakeLoanRepository(5));

await Assert.ThrowsAsync(() => service.Handle(input, new CancellationToken()));
}

[Fact]
public async Task Given_Correct_Input_And_Freemium_User_Create_Loan_Successfully()
{
var user \= User.Create("Serhat", "Yavaş", "serhatleventyavas@gmail.com", "test123", false);
var input \= new CreateLoanInput(
User: user,
Title: "Title",
Description: "Description",
LoadTypeCode: LoanType.Mortgage.Code,
TotalAmount: 10_000,
CurrencyCode: Currency.Try.Code
);

var fakeUnitOfWork \= new FakeUnitOfWork();
var fakeLoanRepository \= new FakeLoanRepository(4);
var service \= new CreateLoanService(fakeUnitOfWork, fakeLoanRepository);

var loan \= await service.Handle(input, new CancellationToken());

Assert.Equal(1, fakeLoanRepository.TotalCalledGetTotalLoanCountByUserId);
Assert.Equal(1, fakeUnitOfWork.Count);
Assert.Contains(loan, fakeLoanRepository.AddedLoanList);
Assert.NotNull(loan);
Assert.Equal(loan.UserId, user.Id);
Assert.Equal(loan.LoanType.Code, input.LoadTypeCode);
Assert.Equal(loan.TotalAmount.Currency.Code, input.CurrencyCode);
Assert.Equal(loan.TotalAmount.Amount, input.TotalAmount);
Assert.Equal(loan.Title, input.Title);
Assert.Equal(loan.Description, input.Description);
}

[Fact]
public async Task Given_Correct_Input_And_Premium_User_Create_Loan_Successfully()
{
var user \= User.Create("Serhat", "Yavaş", "serhatleventyavas@gmail.com", "test123", true);
var input \= new CreateLoanInput(
User: user,
Title: "Title",
Description: "Description",
LoadTypeCode: LoanType.Mortgage.Code,
TotalAmount: 10_000,
CurrencyCode: Currency.Try.Code
);

var fakeUnitOfWork \= new FakeUnitOfWork();
var fakeLoanRepository \= new FakeLoanRepository(5);
var service \= new CreateLoanService(fakeUnitOfWork, fakeLoanRepository);

var loan \= await service.Handle(input, new CancellationToken());

Assert.Equal(0, fakeLoanRepository.TotalCalledGetTotalLoanCountByUserId);
Assert.Equal(1, fakeUnitOfWork.Count);
Assert.Contains(loan, fakeLoanRepository.AddedLoanList);
Assert.NotNull(loan);
Assert.Equal(loan.UserId, user.Id);
Assert.Equal(loan.LoanType.Code, input.LoadTypeCode);
Assert.Equal(loan.TotalAmount.Currency.Code, input.CurrencyCode);
Assert.Equal(loan.TotalAmount.Amount, input.TotalAmount);
Assert.Equal(loan.Title, input.Title);
Assert.Equal(loan.Description, input.Description);
}
}

Yukarıdaki testlerimiz bir borç oluşturmak için tanımladığımız tüm iş kurallarını test ediyor. Ayrıca testlerimizde gördüğünüz gibi iş kurallarına uymayan durumlarda özel hata fırlatıyoruz. Bu hatalarıda aşağıda bulabilirsin.

namespace Wallet.Domain.Loans;

public sealed class RequiredLoanTitleException: Exception;
public sealed class MinLengthLoanTitleException : Exception;
public sealed class MaxLengthLoanTitleException : Exception;
public sealed class MaxLengthLoanDescriptionException : Exception;
public sealed class MaxLoanCountException: Exception;
public sealed class InvalidLoanTypeException: Exception;
public sealed class InvalidCurrencyException: Exception;
public sealed class AmountMustBePositiveException: Exception;
public sealed class CurrenciesMustBeSameException: Exception;
public sealed class InvalidAmountException: Exception;

Test Results

Yukarıdaki ekran alıntısında yazdığımız testlerin hepsinin doğru şekilde çalıştığını görebiliyoruz.

Tests Coverage

Yukarıdaki ekran alıntısında ise Unit Tests Coverage sonucunu görebiliriz. Unit Test Coverage, testler kodumuzun ne kadar ile etkileşime geçti onu yüzdesel olarak bize sunar. CreateLoanService ve Loan sınıflarına %100 olarak dokunmuşuz, her satırı ile etkileşime geçmişiz. Yani elimizdeki iş kurallarının her maddesini test etmişiz. Buradaki amacımız %100 oranına ulaşmak değil. Amacımız bir borç oluşturulurken elimizdeki iş kuralları doğru şekilde çalıştığından emin olmak. Bu şekilde oluşabilecek hataların önüne geçebiliriz. Tabi farkedemediğimiz iş kuralları yüzünden hatalar oluşabilir, ancak elimizdeki iş kurallarının doğru çalıştığına eminiz. Bu sayede kodun hata ve bakım maliyetlerini azaltabiliriz.

Uzun bir yazı oldu. Bu yazımızda xUnit ile nasıl test yazabiliriz, unit test nedir, soyutlama ve bağımlılıklardan küçük küçük örnekler ile bahsetmeye çalıştım. Umarım sana dokunmuş olabilirim. Bu örnek projenin kaynak kodunu aşağıya bırakıyorum. Merak ettiğin bir şey olursa benimle iletişime geçebilirsin.

[GitHub - serhatleventyavas/Wallet
Contribute to serhatleventyavas/Wallet development by creating an account on GitHub.github.com](https://github.com/serhatleventyavas/Wallet "https://github.com/serhatleventyavas/Wallet")