.claude/skills/write-tests/SKILL.md
Writes xUnit tests for SBA use cases in the Fatturazione invoicing system. Use when creating tests for use cases, services, or validators. Follows the three-phase testing pattern (load failures, validation failures, execution success/failure) with NSubstitute mocking and FluentAssertions. Preserves the project's existing test conventions.
npx skillsauth add lazyoft/raymond-invoices write-testsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You are writing xUnit tests for the Fatturazione invoicing system, specifically targeting SBA use cases that follow the three-phase pattern (Load, Validate, Execute).
Tests mirror the three phases of a use case. Each phase has distinct failure modes, and tests should cover all of them. A well-tested use case has:
Should(), Be(), BeEmpty(), etc.)Substitute.For<T>(), Returns(), Received())tests/Fatturazione.Domain.Tests/ -- domain services, validators, use casestests/Fatturazione.Api.Tests/ -- endpoint integration tests{SubjectUnderTest}Tests.cs// Arrange, // Act, // Assert comments[SetUp] attribute)_sut_{name}Mock or _{name}ServiceMock#region blocks to group related tests[Fact] for single-case tests, [Theory] with [InlineData] for parameterizedFor a use case named IssueInvoice, create:
tests/Fatturazione.Domain.Tests/UseCases/IssueInvoiceTests.cs
using Fatturazione.Domain.Exceptions;
using Fatturazione.Domain.Models;
using Fatturazione.Domain.Services;
using Fatturazione.Domain.UseCases;
using Fatturazione.Infrastructure.Repositories;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
namespace Fatturazione.Domain.Tests.UseCases;
/// <summary>
/// Tests for IssueInvoice use case.
/// Art. 21 DPR 633/72 - Fatturazione delle operazioni.
/// </summary>
public class IssueInvoiceTests
{
private readonly IInvoiceRepository _invoiceRepositoryMock;
private readonly IClientRepository _clientRepositoryMock;
private readonly IInvoiceNumberingService _numberingServiceMock;
private readonly IInvoiceCalculationService _calculationServiceMock;
private readonly ILogger<IssueInvoice> _loggerMock;
private readonly IssueInvoice _sut;
public IssueInvoiceTests()
{
_invoiceRepositoryMock = Substitute.For<IInvoiceRepository>();
_clientRepositoryMock = Substitute.For<IClientRepository>();
_numberingServiceMock = Substitute.For<IInvoiceNumberingService>();
_calculationServiceMock = Substitute.For<IInvoiceCalculationService>();
_loggerMock = Substitute.For<ILogger<IssueInvoice>>();
_sut = new IssueInvoice(
_invoiceRepositoryMock,
_clientRepositoryMock,
_numberingServiceMock,
_calculationServiceMock,
_loggerMock);
}
// ── Helpers ─────────────────────────────────────────────────────
#region Helper Methods
/// <summary>
/// Creates a valid draft invoice ready for issuance.
/// </summary>
private static Invoice CreateDraftInvoice(Guid? id = null, Guid? clientId = null)
{
var invoiceId = id ?? Guid.NewGuid();
var cId = clientId ?? Guid.NewGuid();
return new Invoice
{
Id = invoiceId,
InvoiceDate = DateTime.Today,
DueDate = DateTime.Today.AddDays(30),
ClientId = cId,
Status = InvoiceStatus.Draft,
Items = new List<InvoiceItem>
{
new InvoiceItem
{
Id = Guid.NewGuid(),
Description = "Consulenza",
Quantity = 1,
UnitPrice = 1000m,
IvaRate = IvaRate.Standard
}
}
};
}
/// <summary>
/// Creates a standard client for testing.
/// </summary>
private static Client CreateClient(Guid? id = null)
{
return new Client
{
Id = id ?? Guid.NewGuid(),
RagioneSociale = "Test SRL",
PartitaIva = "12345678903",
ClientType = ClientType.Company,
SubjectToRitenuta = false
};
}
/// <summary>
/// Creates a standard request for testing.
/// </summary>
private static IssueInvoiceRequest CreateRequest(Guid invoiceId)
{
return new IssueInvoiceRequest(invoiceId, ActorId: Guid.NewGuid());
}
#endregion
// ── Phase 1: Load Failure Tests ─────────────────────────────────
#region Load Failures
[Fact]
public async Task Execute_InvoiceNotFound_ThrowsNotFoundException()
{
// Arrange
var request = CreateRequest(Guid.NewGuid());
_invoiceRepositoryMock.GetByIdAsync(request.InvoiceId)
.Returns((Invoice?)null);
// Act
var act = () => _sut.Execute(request);
// Assert
await act.Should().ThrowAsync<NotFoundException>()
.WithMessage("*non trovato*");
}
[Fact]
public async Task Execute_ClientNotFound_ThrowsNotFoundException()
{
// Arrange
var invoice = CreateDraftInvoice();
var request = CreateRequest(invoice.Id);
_invoiceRepositoryMock.GetByIdAsync(invoice.Id).Returns(invoice);
_clientRepositoryMock.GetByIdAsync(invoice.ClientId).Returns((Client?)null);
// Act
var act = () => _sut.Execute(request);
// Assert
await act.Should().ThrowAsync<NotFoundException>()
.WithMessage("*Cliente*non trovato*");
}
#endregion
// ── Phase 2: Validation Failure Tests ───────────────────────────
#region Validation Failures
[Fact]
public async Task Execute_InvoiceAlreadyIssued_ThrowsForbiddenOperationException()
{
// Arrange
var invoice = CreateDraftInvoice();
invoice.Status = InvoiceStatus.Issued; // Already issued
var client = CreateClient(invoice.ClientId);
var request = CreateRequest(invoice.Id);
_invoiceRepositoryMock.GetByIdAsync(invoice.Id).Returns(invoice);
_clientRepositoryMock.GetByIdAsync(invoice.ClientId).Returns(client);
_invoiceRepositoryMock.GetLastInvoiceNumberAsync().Returns("2026/001");
// Act
var act = () => _sut.Execute(request);
// Assert
await act.Should().ThrowAsync<ForbiddenOperationException>()
.WithMessage("*transizione*non consentita*");
}
[Theory]
[InlineData(InvoiceStatus.Sent)]
[InlineData(InvoiceStatus.Paid)]
[InlineData(InvoiceStatus.Cancelled)]
[InlineData(InvoiceStatus.Overdue)]
public async Task Execute_InvalidSourceStatus_ThrowsForbiddenOperationException(
InvoiceStatus invalidStatus)
{
// Arrange
var invoice = CreateDraftInvoice();
invoice.Status = invalidStatus;
var client = CreateClient(invoice.ClientId);
var request = CreateRequest(invoice.Id);
_invoiceRepositoryMock.GetByIdAsync(invoice.Id).Returns(invoice);
_clientRepositoryMock.GetByIdAsync(invoice.ClientId).Returns(client);
_invoiceRepositoryMock.GetLastInvoiceNumberAsync().Returns("2026/001");
// Act
var act = () => _sut.Execute(request);
// Assert
await act.Should().ThrowAsync<ForbiddenOperationException>();
}
#endregion
// ── Phase 3: Execution Success Tests ────────────────────────────
#region Execution Success
[Fact]
public async Task Execute_ValidDraftInvoice_ReturnsIssuedInvoice()
{
// Arrange
var invoice = CreateDraftInvoice();
var client = CreateClient(invoice.ClientId);
var request = CreateRequest(invoice.Id);
_invoiceRepositoryMock.GetByIdAsync(invoice.Id).Returns(invoice);
_clientRepositoryMock.GetByIdAsync(invoice.ClientId).Returns(client);
_invoiceRepositoryMock.GetLastInvoiceNumberAsync().Returns("2026/001");
_numberingServiceMock.GenerateNextInvoiceNumber("2026/001").Returns("2026/002");
_invoiceRepositoryMock.UpdateAsync(Arg.Any<Invoice>())
.Returns(callInfo => callInfo.Arg<Invoice>());
// Act
var response = await _sut.Execute(request);
// Assert
response.Invoice.Status.Should().Be(InvoiceStatus.Issued);
response.InvoiceNumber.Should().Be("2026/002");
}
[Fact]
public async Task Execute_ValidDraftInvoice_CalculatesTotalsBeforeIssuing()
{
// Arrange
var invoice = CreateDraftInvoice();
var client = CreateClient(invoice.ClientId);
var request = CreateRequest(invoice.Id);
_invoiceRepositoryMock.GetByIdAsync(invoice.Id).Returns(invoice);
_clientRepositoryMock.GetByIdAsync(invoice.ClientId).Returns(client);
_invoiceRepositoryMock.GetLastInvoiceNumberAsync().Returns((string?)null);
_numberingServiceMock.GenerateNextInvoiceNumber(null).Returns("2026/001");
_invoiceRepositoryMock.UpdateAsync(Arg.Any<Invoice>())
.Returns(callInfo => callInfo.Arg<Invoice>());
// Act
await _sut.Execute(request);
// Assert -- verify calculation was called
_calculationServiceMock.Received(1).CalculateInvoiceTotals(
Arg.Is<Invoice>(i => i.Id == invoice.Id));
}
[Fact]
public async Task Execute_ValidDraftInvoice_PersistsUpdatedInvoice()
{
// Arrange
var invoice = CreateDraftInvoice();
var client = CreateClient(invoice.ClientId);
var request = CreateRequest(invoice.Id);
_invoiceRepositoryMock.GetByIdAsync(invoice.Id).Returns(invoice);
_clientRepositoryMock.GetByIdAsync(invoice.ClientId).Returns(client);
_invoiceRepositoryMock.GetLastInvoiceNumberAsync().Returns((string?)null);
_numberingServiceMock.GenerateNextInvoiceNumber(null).Returns("2026/001");
_invoiceRepositoryMock.UpdateAsync(Arg.Any<Invoice>())
.Returns(callInfo => callInfo.Arg<Invoice>());
// Act
await _sut.Execute(request);
// Assert -- verify persistence
await _invoiceRepositoryMock.Received(1).UpdateAsync(
Arg.Is<Invoice>(i =>
i.Status == InvoiceStatus.Issued &&
i.InvoiceNumber == "2026/001"));
}
[Fact]
public async Task Execute_FirstInvoice_AssignsFirstNumber()
{
// Arrange -- no previous invoices
var invoice = CreateDraftInvoice();
var client = CreateClient(invoice.ClientId);
var request = CreateRequest(invoice.Id);
_invoiceRepositoryMock.GetByIdAsync(invoice.Id).Returns(invoice);
_clientRepositoryMock.GetByIdAsync(invoice.ClientId).Returns(client);
_invoiceRepositoryMock.GetLastInvoiceNumberAsync().Returns((string?)null);
_numberingServiceMock.GenerateNextInvoiceNumber(null).Returns("2026/001");
_invoiceRepositoryMock.UpdateAsync(Arg.Any<Invoice>())
.Returns(callInfo => callInfo.Arg<Invoice>());
// Act
var response = await _sut.Execute(request);
// Assert
response.InvoiceNumber.Should().Be("2026/001");
}
#endregion
// ── Phase 3: Execution Edge Cases ───────────────────────────────
#region Execution Edge Cases
[Fact]
public async Task Execute_ValidInvoice_SetsClientOnInvoiceBeforeCalculation()
{
// Arrange
var invoice = CreateDraftInvoice();
var client = CreateClient(invoice.ClientId);
var request = CreateRequest(invoice.Id);
_invoiceRepositoryMock.GetByIdAsync(invoice.Id).Returns(invoice);
_clientRepositoryMock.GetByIdAsync(invoice.ClientId).Returns(client);
_invoiceRepositoryMock.GetLastInvoiceNumberAsync().Returns("2026/005");
_numberingServiceMock.GenerateNextInvoiceNumber("2026/005").Returns("2026/006");
_invoiceRepositoryMock.UpdateAsync(Arg.Any<Invoice>())
.Returns(callInfo => callInfo.Arg<Invoice>());
// Act
await _sut.Execute(request);
// Assert -- client must be set on invoice for correct calculation
_calculationServiceMock.Received(1).CalculateInvoiceTotals(
Arg.Is<Invoice>(i => i.Client == client));
}
#endregion
// ── Interaction Verification ────────────────────────────────────
#region Interaction Verification
[Fact]
public async Task Execute_InvoiceNotFound_DoesNotCallRepository()
{
// Arrange
var request = CreateRequest(Guid.NewGuid());
_invoiceRepositoryMock.GetByIdAsync(request.InvoiceId).Returns((Invoice?)null);
// Act
var act = () => _sut.Execute(request);
await act.Should().ThrowAsync<NotFoundException>();
// Assert -- should NOT reach persistence
await _invoiceRepositoryMock.DidNotReceive().UpdateAsync(Arg.Any<Invoice>());
_calculationServiceMock.DidNotReceive().CalculateInvoiceTotals(Arg.Any<Invoice>());
}
[Fact]
public async Task Execute_ValidationFails_DoesNotPersist()
{
// Arrange
var invoice = CreateDraftInvoice();
invoice.Status = InvoiceStatus.Paid; // Cannot transition to Issued
var client = CreateClient(invoice.ClientId);
var request = CreateRequest(invoice.Id);
_invoiceRepositoryMock.GetByIdAsync(invoice.Id).Returns(invoice);
_clientRepositoryMock.GetByIdAsync(invoice.ClientId).Returns(client);
_invoiceRepositoryMock.GetLastInvoiceNumberAsync().Returns("2026/001");
// Act
var act = () => _sut.Execute(request);
await act.Should().ThrowAsync<ForbiddenOperationException>();
// Assert -- should NOT reach persistence
await _invoiceRepositoryMock.DidNotReceive().UpdateAsync(Arg.Any<Invoice>());
}
#endregion
}
tests/Fatturazione.Domain.Tests/
UseCases/
IssueInvoiceTests.cs
CreateCreditNoteTests.cs
TransitionInvoiceTests.cs
Services/
InvoiceCalculationServiceTests.cs (existing)
CreditNoteServiceTests.cs (existing)
Validators/
InvoiceValidatorTests.cs (existing)
| Element | Convention | Example |
|---------|-----------|---------|
| Test class | {SubjectUnderTest}Tests | IssueInvoiceTests |
| Test method | {Method}_{Scenario}_{ExpectedResult} | Execute_InvoiceNotFound_ThrowsNotFoundException |
| Mock fields | _{name}Mock or _{name}ServiceMock | _invoiceRepositoryMock |
| SUT field | _sut | private readonly IssueInvoice _sut; |
| Helper methods | Descriptive static methods | CreateDraftInvoice() |
Organize tests into #region blocks matching the three phases:
#region Helper Methods
// Shared factory methods for test data
#endregion
#region Load Failures
// Tests for Phase 1 failures (NotFoundException)
#endregion
#region Validation Failures
// Tests for Phase 2 failures (ForbiddenOperationException, InvalidInputException)
#endregion
#region Execution Success
// Tests for Phase 3 happy paths
#endregion
#region Execution Edge Cases
// Tests for Phase 3 boundary conditions
#endregion
#region Interaction Verification
// Tests that verify mock interactions (Received/DidNotReceive)
#endregion
ThrowAsync<NotFoundException>[Fact]
public async Task Execute_InvoiceNotFound_ThrowsNotFoundException()
{
// Arrange
_invoiceRepositoryMock.GetByIdAsync(Arg.Any<Guid>()).Returns((Invoice?)null);
// Act
var act = () => _sut.Execute(request);
// Assert
await act.Should().ThrowAsync<NotFoundException>();
await _invoiceRepositoryMock.DidNotReceive().UpdateAsync(Arg.Any<Invoice>());
}
ThrowAsync<ForbiddenOperationException>ThrowAsync<InvalidInputException>[Theory] with [InlineData] for multiple invalid states[Theory]
[InlineData(InvoiceStatus.Sent)]
[InlineData(InvoiceStatus.Paid)]
[InlineData(InvoiceStatus.Cancelled)]
public async Task Execute_InvalidStatus_ThrowsForbiddenOperationException(
InvoiceStatus status)
{
// Arrange
invoice.Status = status;
// ... setup mocks ...
// Act
var act = () => _sut.Execute(request);
// Assert
await act.Should().ThrowAsync<ForbiddenOperationException>();
await _repositoryMock.DidNotReceive().UpdateAsync(Arg.Any<Invoice>());
}
[Fact]
public async Task Execute_ValidInvoice_PersistsWithCorrectStatus()
{
// Arrange
// ... setup all mocks for happy path ...
// Act
var response = await _sut.Execute(request);
// Assert
response.Invoice.Status.Should().Be(InvoiceStatus.Issued);
await _repositoryMock.Received(1).UpdateAsync(
Arg.Is<Invoice>(i => i.Status == InvoiceStatus.Issued));
}
// Entity found
_invoiceRepositoryMock.GetByIdAsync(invoiceId).Returns(invoice);
// Entity not found
_invoiceRepositoryMock.GetByIdAsync(Arg.Any<Guid>()).Returns((Invoice?)null);
// Persist and return
_invoiceRepositoryMock.UpdateAsync(Arg.Any<Invoice>())
.Returns(callInfo => callInfo.Arg<Invoice>());
// Verify persistence
await _invoiceRepositoryMock.Received(1).UpdateAsync(
Arg.Is<Invoice>(i => i.Status == InvoiceStatus.Issued));
// Verify NOT persisted
await _invoiceRepositoryMock.DidNotReceive().UpdateAsync(Arg.Any<Invoice>());
// Calculation service (void method -- just verify call)
_calculationServiceMock.Received(1).CalculateInvoiceTotals(
Arg.Is<Invoice>(i => i.Client != null));
// Service with return value
_numberingServiceMock.GenerateNextInvoiceNumber("2026/001").Returns("2026/002");
// Service returns based on input
_ritenutaServiceMock.AppliesRitenuta(client).Returns(true);
_ritenutaServiceMock.CalculateRitenuta(1000m, client).Returns(200m);
// Logger is injected but typically not asserted on (NSubstitute limitation with extension methods)
// Just ensure it is provided to avoid NullReferenceException
_loggerMock = Substitute.For<ILogger<IssueInvoice>>();
# Run all tests
dotnet test
# Run only use case tests
dotnet test --filter "FullyQualifiedName~UseCases"
# Run a specific test class
dotnet test --filter "FullyQualifiedName~IssueInvoiceTests"
# Run with verbose output
dotnet test --verbosity normal
tests/Fatturazione.Domain.Tests/UseCases/{UseCaseName}Tests.cs{UseCaseName}Tests[Theory] for enum-based rulesFluentAssertions (.Should())await act.Should().ThrowAsync<T>() patterndotnet testdevelopment
Implementa feature nel sistema di fatturazione italiana validando contro normativa fiscale. Usa per aggiungere calcoli IVA, ritenuta d'acconto, split payment, imposta di bollo, gestione fatture PA, regime forfettario, numerazione progressiva, note di credito, o qualsiasi logica che deve rispettare DPR 633/72, DPR 600/73, DPR 642/72. NON usare per bug fix tecnici, refactoring, o modifiche UI senza impatto fiscale.
testing
Implements two-tier error handling for the Fatturazione invoicing system following SBA patterns. Use when adding error handling to use cases, defining new domain exceptions, or setting up structured logging with actor context. Covers expected errors (validation, authorization) vs unexpected errors (infrastructure), logging levels, and exception-to-HTTP-status mapping.
testing
Creates SBA (Story-Based Architecture) use cases in C# following the three-phase pattern (Load, Validate, Execute). Use when adding new business operations like creating invoices, issuing credit notes, transitioning statuses, or any domain action that combines data loading, validation, and execution into a single cohesive unit.
testing
Create, edit, improve, or audit AgentSkills. Use when creating a new skill from scratch or when asked to improve, review, audit, tidy up, or clean up an existing skill or SKILL.md file. Also use when editing or restructuring a skill directory (moving files to references/ or scripts/, removing stale content, validating against the AgentSkills spec). Triggers on phrases like "create a skill", "author a skill", "tidy up a skill", "improve this skill", "review the skill", "clean up the skill", "audit the skill".