Add unit tests

- TransferService tests for linked transfer entries
- YearlyStatement calculation tests for balance verification
This commit is contained in:
2026-03-31 17:13:30 +02:00
parent d5164d34aa
commit eb53af2cd9
3 changed files with 245 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="*" />
<PackageReference Include="xunit" Version="*" />
<PackageReference Include="xunit.runner.visualstudio" Version="*">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="*" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.*" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Duempelkas.Domain\Duempelkas.Domain.csproj" />
<ProjectReference Include="..\..\src\Duempelkas.App\Duempelkas.App.csproj" />
<ProjectReference Include="..\..\src\Duempelkas.Infrastructure\Duempelkas.Infrastructure.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,110 @@
using Duempelkas.Domain.Entities;
using Duempelkas.Domain.Enums;
using Duempelkas.Infrastructure.Persistence;
using Duempelkas.Infrastructure.Services;
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Xunit;
namespace Duempelkas.Tests;
public class TransferServiceTests : IDisposable
{
private readonly FinanceDbContext _db;
private readonly EntryService _entryService;
private readonly BalanceQueryService _balanceQueryService;
public TransferServiceTests()
{
var options = new DbContextOptionsBuilder<FinanceDbContext>()
.UseSqlite("Data Source=:memory:")
.Options;
_db = new FinanceDbContext(options);
_db.Database.OpenConnection();
_db.Database.EnsureCreated();
_entryService = new EntryService(_db);
_balanceQueryService = new BalanceQueryService(_db);
}
[Fact]
public async Task CreateTransfer_CreatesLinkedExpenseAndIncome()
{
var accountA = new Account { Name = "Account A" };
var accountB = new Account { Name = "Account B" };
_db.Accounts.AddRange(accountA, accountB);
await _db.SaveChangesAsync();
await _entryService.CreateTransferAsync(accountA.Id, accountB.Id, new DateTime(2026, 3, 15), "Test Transfer", 100.00m);
var entries = await _db.Entries.ToListAsync();
entries.Should().HaveCount(2);
var sourceEntry = entries.Single(e => e.AccountId == accountA.Id);
sourceEntry.Type.Should().Be(EntryType.Expense);
sourceEntry.Amount.Should().Be(100.00m);
sourceEntry.Title.Should().Be("Test Transfer");
sourceEntry.DisplayId.Should().Be("2026-001");
var targetEntry = entries.Single(e => e.AccountId == accountB.Id);
targetEntry.Type.Should().Be(EntryType.Income);
targetEntry.Amount.Should().Be(100.00m);
targetEntry.DisplayId.Should().Be("2026-002");
var links = await _db.TransferLinks.ToListAsync();
links.Should().HaveCount(1);
links[0].SourceEntryId.Should().Be(sourceEntry.Id);
links[0].TargetEntryId.Should().Be(targetEntry.Id);
var balanceA = await _balanceQueryService.GetAccountBalanceAsync(accountA.Id);
balanceA.TotalBalance.Should().Be(-100.00m);
var balanceB = await _balanceQueryService.GetAccountBalanceAsync(accountB.Id);
balanceB.TotalBalance.Should().Be(100.00m);
}
[Fact]
public async Task DeleteTransfer_SoftDeletesBothSides()
{
var accountA = new Account { Name = "Account A" };
var accountB = new Account { Name = "Account B" };
_db.Accounts.AddRange(accountA, accountB);
await _db.SaveChangesAsync();
await _entryService.CreateTransferAsync(accountA.Id, accountB.Id, new DateTime(2026, 3, 15), "Test Transfer", 100.00m);
var sourceEntry = await _db.Entries.FirstAsync(e => e.AccountId == accountA.Id);
await _entryService.DeleteEntryAsync(sourceEntry.Id);
var entries = await _db.Entries.ToListAsync();
entries.Should().HaveCount(2);
entries.Should().AllSatisfy(e => e.IsDeleted.Should().BeTrue());
var links = await _db.TransferLinks.ToListAsync();
links.Should().HaveCount(1);
var balanceA = await _balanceQueryService.GetAccountBalanceAsync(accountA.Id);
balanceA.TotalBalance.Should().Be(0m);
var balanceB = await _balanceQueryService.GetAccountBalanceAsync(accountB.Id);
balanceB.TotalBalance.Should().Be(0m);
}
[Fact]
public async Task CreateTransfer_SameAccount_Throws()
{
var account = new Account { Name = "Account A" };
_db.Accounts.Add(account);
await _db.SaveChangesAsync();
var act = () => _entryService.CreateTransferAsync(account.Id, account.Id, DateTime.Today, "Bad Transfer", 50m);
await act.Should().ThrowAsync<InvalidOperationException>();
}
public void Dispose()
{
_db.Database.CloseConnection();
_db.Dispose();
}
}

View File

@@ -0,0 +1,109 @@
using Duempelkas.Domain.Entities;
using Duempelkas.Domain.Enums;
using Duempelkas.Infrastructure.Persistence;
using Duempelkas.Infrastructure.Services;
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Xunit;
namespace Duempelkas.Tests;
public class BalanceCalculationTests : IDisposable
{
private readonly FinanceDbContext _db;
private readonly BalanceQueryService _balanceQueryService;
private readonly EntryService _entryService;
public BalanceCalculationTests()
{
var options = new DbContextOptionsBuilder<FinanceDbContext>()
.UseSqlite("Data Source=:memory:")
.Options;
_db = new FinanceDbContext(options);
_db.Database.OpenConnection();
_db.Database.EnsureCreated();
_balanceQueryService = new BalanceQueryService(_db);
_entryService = new EntryService(_db);
}
[Fact]
public async Task AccountBalance_CalculatesCorrectTotals()
{
var account = new Account { Name = "Test Account", CarryoverBalance = 500.00m };
_db.Accounts.Add(account);
await _db.SaveChangesAsync();
_db.Entries.AddRange(
new Entry { AccountId = account.Id, DisplayId = "2026-001", Type = EntryType.Income, Date = new DateTime(2026, 1, 15), Title = "Gehalt", Amount = 800.00m },
new Entry { AccountId = account.Id, DisplayId = "2026-002", Type = EntryType.Income, Date = new DateTime(2026, 2, 15), Title = "Bonus", Amount = 400.00m },
new Entry { AccountId = account.Id, DisplayId = "2026-003", Type = EntryType.Expense, Date = new DateTime(2026, 1, 20), Title = "Miete", Amount = 200.00m },
new Entry { AccountId = account.Id, DisplayId = "2026-004", Type = EntryType.Expense, Date = new DateTime(2026, 2, 20), Title = "Lebensmittel", Amount = 250.00m }
);
await _db.SaveChangesAsync();
var balance = await _balanceQueryService.GetAccountBalanceAsync(account.Id);
balance.CarryoverBalance.Should().Be(500.00m);
balance.TotalIncome.Should().Be(1200.00m);
balance.TotalExpense.Should().Be(450.00m);
balance.TotalBalance.Should().Be(1250.00m);
}
[Fact]
public async Task AccountBalance_IncludesTransfersCorrectly()
{
var accountA = new Account { Name = "Account A", CarryoverBalance = 500.00m };
var accountB = new Account { Name = "Account B" };
_db.Accounts.AddRange(accountA, accountB);
await _db.SaveChangesAsync();
_db.Entries.Add(new Entry
{
AccountId = accountA.Id,
DisplayId = "2026-001",
Type = EntryType.Income,
Date = new DateTime(2026, 1, 10),
Title = "Gehalt",
Amount = 1000.00m
});
await _db.SaveChangesAsync();
await _entryService.CreateTransferAsync(accountA.Id, accountB.Id, new DateTime(2026, 2, 1), "Umbuchung nach B", 300.00m);
var balanceA = await _balanceQueryService.GetAccountBalanceAsync(accountA.Id);
balanceA.CarryoverBalance.Should().Be(500.00m);
balanceA.TotalIncome.Should().Be(1000.00m);
balanceA.TotalExpense.Should().Be(300.00m);
balanceA.TotalBalance.Should().Be(1200.00m);
}
[Fact]
public async Task AccountBalance_WithCarryover()
{
var account = new Account { Name = "Konto mit Übertrag", CarryoverBalance = 1000.00m };
_db.Accounts.Add(account);
await _db.SaveChangesAsync();
_db.Entries.AddRange(
new Entry { AccountId = account.Id, DisplayId = "2025-001", Type = EntryType.Income, Date = new DateTime(2025, 6, 1), Title = "Einnahme 2025", Amount = 500.00m },
new Entry { AccountId = account.Id, DisplayId = "2026-001", Type = EntryType.Income, Date = new DateTime(2026, 6, 1), Title = "Einnahme 2026", Amount = 300.00m },
new Entry { AccountId = account.Id, DisplayId = "2026-002", Type = EntryType.Expense, Date = new DateTime(2026, 7, 1), Title = "Ausgabe 2026", Amount = 150.00m }
);
await _db.SaveChangesAsync();
var balance = await _balanceQueryService.GetAccountBalanceAsync(account.Id);
// 1000 + 500 + 300 - 150 = 1650
balance.TotalBalance.Should().Be(1650.00m);
balance.CarryoverBalance.Should().Be(1000.00m);
}
public void Dispose()
{
_db.Database.CloseConnection();
_db.Dispose();
}
}