add tests
This commit is contained in:
@@ -55,4 +55,5 @@ nunit-*.xml
|
||||
|
||||
.idea
|
||||
.vscode
|
||||
*.user
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
using CsharpOop.Domain;
|
||||
|
||||
namespace CsharpOop.Tests;
|
||||
|
||||
/// Tests covering domain-layer invariants on the Account aggregate directly,
|
||||
/// independent of the application layer.
|
||||
public class AccountTests
|
||||
{
|
||||
[Fact]
|
||||
public void Opening_an_account_with_a_negative_balance_throws()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||
new Account(new AccountId(Guid.NewGuid()), new Money(-1m)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Withdrawing_a_zero_amount_throws()
|
||||
{
|
||||
var account = new Account(new AccountId(Guid.NewGuid()), new Money(100m));
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => account.Withdraw(new Money(0m)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Withdrawing_a_negative_amount_throws()
|
||||
{
|
||||
var account = new Account(new AccountId(Guid.NewGuid()), new Money(100m));
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => account.Withdraw(new Money(-10m)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Withdrawing_more_than_the_balance_throws()
|
||||
{
|
||||
var account = new Account(new AccountId(Guid.NewGuid()), new Money(100m));
|
||||
|
||||
Assert.Throws<InsufficientBalanceException>(() => account.Withdraw(new Money(101m)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Successive_withdrawals_are_each_applied_to_the_running_balance()
|
||||
{
|
||||
var account = new Account(new AccountId(Guid.NewGuid()), new Money(300m));
|
||||
|
||||
account.Withdraw(new Money(100m));
|
||||
account.Withdraw(new Money(100m));
|
||||
|
||||
Assert.Equal(100m, account.Balance.Amount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using CsharpOop.Applications;
|
||||
using CsharpOop.Contracts;
|
||||
using CsharpOop.Domain;
|
||||
using CsharpOop.Infrastructure;
|
||||
|
||||
namespace CsharpOop.Tests;
|
||||
|
||||
/// Tests covering the application-layer use case surface:
|
||||
/// what the WithdrawMoney feature does from the caller's perspective.
|
||||
public class WithdrawMoneyHandlerTests
|
||||
{
|
||||
private static (WithdrawMoneyHandler handler, InMemoryAccountRepository repository) BuildHandler()
|
||||
{
|
||||
var repository = new InMemoryAccountRepository();
|
||||
var handler = new WithdrawMoneyHandler(repository);
|
||||
return (handler, repository);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Withdrawing_from_an_account_reduces_its_balance_by_the_withdrawn_amount()
|
||||
{
|
||||
var (handler, repository) = BuildHandler();
|
||||
var accountId = Guid.NewGuid();
|
||||
repository.Save(new Account(new AccountId(accountId), new Money(200m)));
|
||||
|
||||
handler.Handle(new WithdrawMoneyCommand { AccountId = accountId, Amount = 75m });
|
||||
|
||||
var account = repository.GetById(new AccountId(accountId))!;
|
||||
Assert.Equal(125m, account.Balance.Amount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Withdrawing_the_entire_balance_leaves_the_account_at_zero()
|
||||
{
|
||||
var (handler, repository) = BuildHandler();
|
||||
var accountId = Guid.NewGuid();
|
||||
repository.Save(new Account(new AccountId(accountId), new Money(100m)));
|
||||
|
||||
handler.Handle(new WithdrawMoneyCommand { AccountId = accountId, Amount = 100m });
|
||||
|
||||
var account = repository.GetById(new AccountId(accountId))!;
|
||||
Assert.Equal(0m, account.Balance.Amount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Withdrawing_from_a_non_existent_account_throws()
|
||||
{
|
||||
var (handler, _) = BuildHandler();
|
||||
var command = new WithdrawMoneyCommand { AccountId = Guid.NewGuid(), Amount = 50m };
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => handler.Handle(command));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Withdrawing_more_than_the_available_balance_throws()
|
||||
{
|
||||
var (handler, repository) = BuildHandler();
|
||||
var accountId = Guid.NewGuid();
|
||||
repository.Save(new Account(new AccountId(accountId), new Money(50m)));
|
||||
|
||||
Assert.Throws<InsufficientBalanceException>(() =>
|
||||
handler.Handle(new WithdrawMoneyCommand { AccountId = accountId, Amount = 100m }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void After_a_failed_withdrawal_the_balance_is_unchanged()
|
||||
{
|
||||
var (handler, repository) = BuildHandler();
|
||||
var accountId = Guid.NewGuid();
|
||||
repository.Save(new Account(new AccountId(accountId), new Money(50m)));
|
||||
|
||||
try { handler.Handle(new WithdrawMoneyCommand { AccountId = accountId, Amount = 999m }); }
|
||||
catch (InsufficientBalanceException) { }
|
||||
|
||||
var account = repository.GetById(new AccountId(accountId))!;
|
||||
Assert.Equal(50m, account.Balance.Amount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<RootNamespace>csharp_oop.Tests</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\csharp-oop\csharp-oop.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -27,8 +27,9 @@ public sealed class Account : AggregateRoot<AccountId>
|
||||
if (amount.Amount <= 0)
|
||||
throw new InvalidOperationException("Withdrawal amount must be positive.");
|
||||
|
||||
// Sometimes, validation is modelled with custom exceptions instead
|
||||
if (Balance.Amount - amount.Amount < 0)
|
||||
throw new InvalidOperationException("Balance cannot go below zero.");
|
||||
throw new InsufficientBalanceException(Balance, amount);
|
||||
|
||||
Balance = Balance.Subtract(amount);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace CsharpOop.Domain;
|
||||
|
||||
/// Custom domain exception thrown when a withdrawal would cause the balance to go below zero
|
||||
public sealed class InsufficientBalanceException : InvalidOperationException
|
||||
{
|
||||
public Money CurrentBalance { get; }
|
||||
public Money RequestedAmount { get; }
|
||||
|
||||
public InsufficientBalanceException(Money currentBalance, Money requestedAmount)
|
||||
: base($"Insufficient balance. Current: {currentBalance.Amount:0.00}, Requested: {requestedAmount.Amount:0.00}")
|
||||
{
|
||||
CurrentBalance = currentBalance;
|
||||
RequestedAmount = requestedAmount;
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "OOP", "OOP", "{CEC0EC27-0CEC-90C6-CABA-E58AB278E4DA}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "csharp-oop", "csharp-oop\csharp-oop.csproj", "{E00F4E4F-8D52-49CE-8754-ED8378A5278F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "csharp-oop.Tests", "csharp-oop.Tests\csharp-oop.Tests.csproj", "{9FD0C8F4-A23B-4C68-A365-86E7EF5623D8}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{E00F4E4F-8D52-49CE-8754-ED8378A5278F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E00F4E4F-8D52-49CE-8754-ED8378A5278F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E00F4E4F-8D52-49CE-8754-ED8378A5278F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{E00F4E4F-8D52-49CE-8754-ED8378A5278F}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{E00F4E4F-8D52-49CE-8754-ED8378A5278F}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{E00F4E4F-8D52-49CE-8754-ED8378A5278F}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{E00F4E4F-8D52-49CE-8754-ED8378A5278F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E00F4E4F-8D52-49CE-8754-ED8378A5278F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E00F4E4F-8D52-49CE-8754-ED8378A5278F}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{E00F4E4F-8D52-49CE-8754-ED8378A5278F}.Release|x64.Build.0 = Release|Any CPU
|
||||
{E00F4E4F-8D52-49CE-8754-ED8378A5278F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{E00F4E4F-8D52-49CE-8754-ED8378A5278F}.Release|x86.Build.0 = Release|Any CPU
|
||||
{9FD0C8F4-A23B-4C68-A365-86E7EF5623D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9FD0C8F4-A23B-4C68-A365-86E7EF5623D8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9FD0C8F4-A23B-4C68-A365-86E7EF5623D8}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{9FD0C8F4-A23B-4C68-A365-86E7EF5623D8}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{9FD0C8F4-A23B-4C68-A365-86E7EF5623D8}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{9FD0C8F4-A23B-4C68-A365-86E7EF5623D8}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{9FD0C8F4-A23B-4C68-A365-86E7EF5623D8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9FD0C8F4-A23B-4C68-A365-86E7EF5623D8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9FD0C8F4-A23B-4C68-A365-86E7EF5623D8}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{9FD0C8F4-A23B-4C68-A365-86E7EF5623D8}.Release|x64.Build.0 = Release|Any CPU
|
||||
{9FD0C8F4-A23B-4C68-A365-86E7EF5623D8}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{9FD0C8F4-A23B-4C68-A365-86E7EF5623D8}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{E00F4E4F-8D52-49CE-8754-ED8378A5278F} = {CEC0EC27-0CEC-90C6-CABA-E58AB278E4DA}
|
||||
{9FD0C8F4-A23B-4C68-A365-86E7EF5623D8} = {CEC0EC27-0CEC-90C6-CABA-E58AB278E4DA}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
Reference in New Issue
Block a user