Échoue car :
- La partie n'existe pas
- Le chasseur n'a plus de balle
- Le chasseur n'est pas dans la partie
- Les chasseurs sont à l'apéro
- La partie de chasse est terminée
Réussi pour :
- Un chassseur présent dans la partie et lui restant des balles
[Fact]
public void CarPartieNexistePasSansException()
{
// TODO extract to Given When Then methods
// Arrange
var partieDeChasseId = UnePartieDeChasseInexistante();
// Act
var result = _useCase.HandleSansException(new Domain.Commands.Tirer(partieDeChasseId, Data.Bernard));
// Assert
result.Should().BeLeft(); // Par convention Left contient le cas d'erreur
result.Left().Should().Be($"La partie de chasse {partieDeChasseId} n'existe pas");
SavedPartieDeChasse().Should().BeNull();
}
public Either<Error, VoidResponse> HandleSansException(Domain.Commands.Tirer command) => throw new NotImplementedException();
[Fact]
public void CarPartieNexistePasSansException()
{
// TODO extract to Given When Then methods
// Arrange
var partieDeChasseId = UnePartieDeChasseInexistante();
// Act
var result = _useCase.HandleSansException(new Domain.Commands.Tirer(partieDeChasseId, Data.Bernard));
// Assert
result.Should().BeLeft(); // Par convention Left contient le cas d'erreur
result.IfLeft(error =>
{
error.Message.Should().Be($"La partie de chasse {partieDeChasseId} n'existe pas");
SavedPartieDeChasse().Should().BeNull();
});
}
public Either<Error, VoidResponse> HandleSansException(Domain.Commands.Tirer command) => new Error($"La partie de chasse {command.PartieDeChasseId} n'existe pas");
using static LanguageExt.Prelude;
public Either<Error, VoidResponse> HandleSansException(Domain.Commands.Tirer command) => AnError($"La partie de chasse {command.PartieDeChasseId} n'existe pas");
public record Error
{
public string Message { get; }
private Error(string message) => Message = message;
public static Error AnError(string message) => new(message);
}
public class Tirer : UseCaseTestWithoutException<UseCases.Tirer, VoidResponse>
{
...
public class Echoue : UseCaseTestWithoutException<UseCases.Tirer, VoidResponse>
{
...
[Fact]
public void CarPartieNexistePasSansException()
{
Given(UnePartieDeChasseInexistante());
When(partieDeChasseId =>
_useCase.HandleSansException(new Domain.Commands.Tirer(partieDeChasseId, Data.Bernard)));
ThenFailWith(
$"La partie de chasse {_partieDeChasseId} n'existe pas",
savedPartieDeChasse => savedPartieDeChasse.Should().BeNull()
);
}
...
}
public abstract class UseCaseTestWithoutException<TUseCase, TSuccessResponse> : UseCaseTest<TUseCase>
{
protected UseCaseTestWithoutException(Func<IPartieDeChasseRepository, Func<DateTime>, TUseCase> useCaseFactory)
: base(useCaseFactory)
{
}
private Func<Guid, Either<Error, TSuccessResponse>>? _act;
protected void When(Func<Guid, Either<Error, TSuccessResponse>>? act) => _act = act;
protected void ThenFailWith(string expectedErrorMessage, Action<PartieDeChasse?>? assertSavedPartieDeChasse)
{
var result = _act!(_partieDeChasseId);
result.Should().BeLeft();
result.IfLeft(r =>
{
r.Message.Should().Be(expectedErrorMessage);
assertSavedPartieDeChasse?.Invoke(SavedPartieDeChasse());
});
}
}
Échoue car :
✅ La partie n'existe pas
- Le chasseur n'est pas dans la partie
- Le chasseur n'a plus de balle
- Les chasseurs sont à l'apéro
- La partie de chasse est terminée
Réussi pour :
- Un chassseur présent dans la partie et lui restant des balles
public Either<Error, VoidResponse> HandleSansException(Domain.Commands.Tirer command)
{
if (_repository.GetById(command.PartieDeChasseId) == null)
{
return AnError($"La partie de chasse {command.PartieDeChasseId} n'existe pas");
}
return AnError($"Chasseur inconnu {command.Chasseur}");
}
public Either<Error, VoidResponse> HandleSansException(Domain.Commands.Tirer command,
Func<DateTime> timeProvider)
{
var partieDeChasse = _repository.GetById(command.PartieDeChasseId);
if (partieDeChasse == null)
return AnError($"La partie de chasse {command.PartieDeChasseId} n'existe pas");
try
{
partieDeChasse.Tirer(command.Chasseur, timeProvider, _repository);
}
catch (ChasseurInconnu)
{
return AnError($"Chasseur inconnu {command.Chasseur}");
}
return VoidResponse.Empty;
}
Échoue car :
✅ La partie n'existe pas
✅ Le chasseur n'est pas dans la partie
- Le chasseur n'a plus de balle
- Les chasseurs sont à l'apéro
- La partie de chasse est terminée
Réussi pour :
- Un chassseur présent dans la partie et lui restant des balles
[Fact]
public void AvecUnChasseurNayantPlusDeBalles()
{
Given(
UnePartieDeChasseExistante(
SurUnTerrainRicheEnGalinettes()
.Avec(Dédé(), Bernard().SansBalles(), Robert())
));
When(id => _useCase.HandleSansException(new Domain.Commands.Tirer(id, Data.Bernard)));
ThenFailWith("Bernard tire -> T'as plus de balles mon vieux, chasse à la main",
savedPartieDeChasse => savedPartieDeChasse.Should().HaveEmittedEvent(Now,
$"Bernard tire -> T'as plus de balles mon vieux, chasse à la main")
);
}
public Either<Error, VoidResponse> HandleSansException(Domain.Commands.Tirer command)
{
var partieDeChasse = _repository.GetById(command.PartieDeChasseId);
if (partieDeChasse == null)
return AnError($"La partie de chasse {command.PartieDeChasseId} n'existe pas");
try
{
partieDeChasse.Tirer(command.Chasseur, _timeProvider, _repository);
}
catch (ChasseurInconnu)
{
return AnError($"Chasseur inconnu {command.Chasseur}");
}
catch (TasPlusDeBallesMonVieuxChasseALaMain)
{
return AnError($"{command.Chasseur} tire -> T'as plus de balles mon vieux, chasse à la main");
}
return VoidResponse.Empty;
}
public Either<Error, VoidResponse> HandleSansException(Domain.Commands.Tirer command)
{
var partieDeChasse = _repository.GetById(command.PartieDeChasseId);
if (partieDeChasse == null)
return AnError($"La partie de chasse {command.PartieDeChasseId} n'existe pas");
return partieDeChasse
.TirerSansException(command.Chasseur, _timeProvider, _repository)
.Map(_ => VoidResponse.Empty);
}
public Either<Error, PartieDeChasse> TirerSansException(
string chasseur,
Func<DateTime> timeProvider,
IPartieDeChasseRepository repository)
=> TirerSansException(chasseur,
timeProvider,
repository,
debutMessageSiPlusDeBalles: $"{chasseur} tire");
// Le Domain renvoie directement l'erreur s'il y en a plutôt que de lancer des exceptions
private Either<Error, PartieDeChasse> TirerSansException(
string chasseur,
Func<DateTime> timeProvider,
IPartieDeChasseRepository repository,
string debutMessageSiPlusDeBalles,
Action<Chasseur>? continueWith = null)
{
if (DuringApéro())
{
EmitEventAndSave($"{chasseur} veut tirer -> On tire pas pendant l'apéro, c'est sacré !!!", timeProvider,
repository);
throw new OnTirePasPendantLapéroCestSacré();
}
if (DéjàTerminée())
{
EmitEventAndSave($"{chasseur} veut tirer -> On tire pas quand la partie est terminée", timeProvider,
repository);
throw new OnTirePasQuandLaPartieEstTerminée();
}
if (!ChasseurExiste(chasseur))
{
return AnError($"Chasseur inconnu {chasseur}");
}
var chasseurQuiTire = RetrieveChasseur(chasseur);
if (!chasseurQuiTire.AEncoreDesBalles())
{
EmitEventAndSave($"{debutMessageSiPlusDeBalles} -> T'as plus de balles mon vieux, chasse à la main",
timeProvider, repository);
return AnError($"{debutMessageSiPlusDeBalles} -> T'as plus de balles mon vieux, chasse à la main");
}
chasseurQuiTire.ATiré();
continueWith?.Invoke(chasseurQuiTire);
EmitEvent($"{chasseur} tire", timeProvider);
return this;
}
Échoue car :
✅ La partie n'existe pas
✅ Le chasseur n'est pas dans la partie
✅ Le chasseur n'a plus de balle
- Les chasseurs sont à l'apéro
- La partie de chasse est terminée
Réussi pour :
- Un chassseur présent dans la partie et lui restant des balles
public Either<Error, PartieDeChasse> TirerSansException(
string chasseur,
Func<DateTime> timeProvider,
IPartieDeChasseRepository repository)
=> TirerSansException(chasseur,
timeProvider,
repository,
debutMessageSiPlusDeBalles: $"{chasseur} tire");
private Either<Error, PartieDeChasse> TirerSansException(
string chasseur,
Func<DateTime> timeProvider,
IPartieDeChasseRepository repository,
string debutMessageSiPlusDeBalles,
Action<Chasseur>? continueWith = null)
{
if (DuringApéro())
{
EmitEventAndSave($"{chasseur} veut tirer -> On tire pas pendant l'apéro, c'est sacré !!!", timeProvider,
repository);
return AnError($"{chasseur} veut tirer -> On tire pas pendant l'apéro, c'est sacré !!!");
}
if (DéjàTerminée())
{
EmitEventAndSave($"{chasseur} veut tirer -> On tire pas quand la partie est terminée", timeProvider,
repository);
return AnError($"{chasseur} veut tirer -> On tire pas quand la partie est terminée");
}
if (!ChasseurExiste(chasseur))
{
return AnError($"Chasseur inconnu {chasseur}");
}
var chasseurQuiTire = RetrieveChasseur(chasseur);
if (!chasseurQuiTire.AEncoreDesBalles())
{
EmitEventAndSave($"{debutMessageSiPlusDeBalles} -> T'as plus de balles mon vieux, chasse à la main",
timeProvider, repository);
return AnError($"{debutMessageSiPlusDeBalles} -> T'as plus de balles mon vieux, chasse à la main");
}
chasseurQuiTire.ATiré();
continueWith?.Invoke(chasseurQuiTire);
EmitEvent($"{chasseur} tire", timeProvider);
return this;
}
private Either<Error, PartieDeChasse> TirerSansException(
string chasseur,
Func<DateTime> timeProvider,
IPartieDeChasseRepository repository,
string debutMessageSiPlusDeBalles,
Action<Chasseur>? continueWith = null)
{
if (DuringApéro())
{
var message = $"{chasseur} veut tirer -> On tire pas pendant l'apéro, c'est sacré !!!";
return EmitAndReturn(timeProvider, repository, message);
}
...
}
private Either<Error, PartieDeChasse> EmitAndReturn(Func<DateTime> timeProvider, IPartieDeChasseRepository repository, string message)
{
EmitEventAndSave(message, timeProvider,
repository);
return AnError(message);
}
private Either<Error, PartieDeChasse> TirerSansException(
string chasseur,
Func<DateTime> timeProvider,
string debutMessageSiPlusDeBalles,
Action<Chasseur>? continueWith = null)
{
if (DuringApéro())
{
return EmitAndReturn(
$"{chasseur} veut tirer -> On tire pas pendant l'apéro, c'est sacré !!!",
timeProvider);
}
if (DéjàTerminée())
{
return EmitAndReturn($"{chasseur} veut tirer -> On tire pas quand la partie est terminée",
timeProvider);
}
if (!ChasseurExiste(chasseur))
{
return EmitAndReturn($"Chasseur inconnu {chasseur}", timeProvider);
}
var chasseurQuiTire = RetrieveChasseur(chasseur);
if (!chasseurQuiTire.AEncoreDesBalles())
{
return EmitAndReturn($"{debutMessageSiPlusDeBalles} -> T'as plus de balles mon vieux, chasse à la main",
timeProvider);
}
chasseurQuiTire.ATiré();
continueWith?.Invoke(chasseurQuiTire);
EmitEvent($"{chasseur} tire", timeProvider);
return this;
}
private Either<Error, PartieDeChasse> EmitAndReturn(string message, Func<DateTime> timeProvider)
{
EmitEvent(message, timeProvider);
return AnError(message);
}
// Use Case
public Either<Error, VoidResponse> HandleSansException(Domain.Commands.Tirer command)
{
var partieDeChasse = _repository.GetById(command.PartieDeChasseId);
if (partieDeChasse == null)
return AnError($"La partie de chasse {command.PartieDeChasseId} n'existe pas");
var result = partieDeChasse
.TirerSansException(command.Chasseur, _timeProvider)
.Map(_ => VoidResponse.Empty);
// On force le Save de la Partie de Chasse quelque soit le retour (Succès ou pas)
_repository.Save(partieDeChasse);
return result;
}
public interface IPartieDeChasseRepository
{
...
// Renvoie auelque chose ou pas
Option<PartieDeChasse> GetByIdOption(Guid partieDeChasseId);
}
public Either<Error, VoidResponse> HandleSansException(Domain.Commands.Tirer command)
{
PartieDeChasse? foundPartieDeChasse = null;
var result = _repository.GetByIdOption(command.PartieDeChasseId)
// affecte la Partie De Chasse
.Do(p => foundPartieDeChasse = p)
.ToEither(() => AnError($"La partie de chasse {command.PartieDeChasseId} n'existe pas"))
// Monadic Binding
// Permet d'"aplatir" : avec Map on aurait 1 Either<Error, Either<Error, VoidResponse>>
.Bind(partieDeChasse => partieDeChasse.TirerSansException(command.Chasseur, _timeProvider));
if (foundPartieDeChasse != null) _repository.Save(foundPartieDeChasse);
return result;
}
public abstract class PartieDeChasseUseCase<TRequest, TResponse> : IUseCase<TRequest, TResponse>
where TRequest : PartieDeChasseCommand
{
private readonly IPartieDeChasseRepository _repository;
private readonly Func<PartieDeChasse, TRequest, Either<Error, TResponse>> _handler;
protected PartieDeChasseUseCase(
IPartieDeChasseRepository repository,
Func<PartieDeChasse, TRequest, Either<Error, TResponse>> handler)
{
_repository = repository;
_handler = handler;
}
public Either<Error, TResponse> Handle(TRequest command) =>
_repository
.GetById(command.PartieDeChasseId)
.ToEither(() => AnError($"La partie de chasse {command.PartieDeChasseId} n'existe pas"))
.Bind(p => HandleCommand(p, command));
private Either<Error, TResponse> HandleCommand(PartieDeChasse partieDeChasse, TRequest command) =>
_handler(partieDeChasse, command)
// Let is a scope function inspired by Kotlin : https://kotlinlang.org/docs/scope-functions.html
.Let(_ => _repository.Save(partieDeChasse));
protected static Either<Error, VoidResponse> ToEmpty(Either<Error, PartieDeChasse> either)
=> either.Map(_ => VoidResponse.Empty);
}
public sealed class Tirer : PartieDeChasseUseCase<Domain.Commands.Tirer, VoidResponse>
{
public Tirer(IPartieDeChasseRepository repository, Func<DateTime> timeProvider)
: base(repository,
(partieDeChasse, command) => ToEmpty(partieDeChasse.Tirer(command.Chasseur, timeProvider)))
{
}
}