Unit Test the Domain Layer
๐ฏ Goal
Develop comprehensive unit tests for the Domain layer entities and value objects to ensure business rules and validation logic work correctly according to Clean Architecture principles.
๐ Prerequisites
Before beginning this exercise, you should:
- Have completed Exercise 1 (Setting Up a Clean Architecture Solution)
- Have completed Exercise 2 (Creating a Simple Product Entity)
- Understand basic C# unit testing concepts
- Be familiar with object-oriented principles and domain-driven design
๐ Learning Objectives
By the end of this exercise, you will:
- Set up a unit testing project with xUnit for the Domain layer
- Write tests for domain entities to validate business rules
- Write tests for value objects to validate immutability and behavior
- Implement parameterized tests using Theory and InlineData attributes
- Use the AAA pattern (Arrange, Act, Assert) to structure tests
- Test edge cases and boundary values for comprehensive coverage
- Understand the role of unit testing in Clean Architecture
๐ Why This Matters
In real-world applications, unit testing the domain layer is crucial because:
- It verifies that business rules and invariants are correctly implemented
- It provides documentation of expected behavior for other developers
- It enables safe refactoring by catching regressions early
- It ensures that the core business logic remains correct regardless of external changes
- It forces clear separation of concerns when domain objects need to be testable in isolation
๐ Step-by-Step Instructions
Step 1: Create a Unit Test Project
Introduction: First, we’ll set up a dedicated unit test project for our Domain layer. Keeping tests in a separate project helps maintain a clean separation between production and test code, while still allowing our tests full access to the classes they need to verify.
Open a terminal and navigate to your solution root folder.
Create a new
testsdirectory at the solution level and a new xUnit project:# Create tests directory mkdir -p tests/MerchStore.Domain.UnitTests # Create xUnit test project dotnet new xunit -o tests/MerchStore.Domain.UnitTestsAdd the test project to the solution:
dotnet sln add tests/MerchStore.Domain.UnitTests/MerchStore.Domain.UnitTests.csprojAdd a reference to the Domain project:
dotnet add tests/MerchStore.Domain.UnitTests/MerchStore.Domain.UnitTests.csproj reference src/MerchStore.Domain/MerchStore.Domain.csproj
๐ก Information
- Test Project Organization: Keeping tests in a separate directory creates a clear separation between source code and test code
- Project Naming: Following the convention of [ProjectName].Tests helps maintain consistency
- xUnit: A lightweight, extensible testing framework for .NET that’s commonly used in modern .NET applications
- Project References: The test project only needs to reference the specific project it’s testing, not the entire solution
Step 2: Create Test Class Structure
Introduction: A well-organized test project makes it easier to find and maintain tests. We’ll mirror the structure of our Domain project to make it clear which tests correspond to which domain objects. This approach also makes it easier to ensure complete test coverage as new domain objects are added.
Create a directory structure in the test project that mirrors the Domain project:
mkdir -p tests/MerchStore.Domain.UnitTests/Entities mkdir -p tests/MerchStore.Domain.UnitTests/ValueObjects mkdir -p tests/MerchStore.Domain.UnitTests/CommonRemove the default test file that gets created with the project:
rm -f tests/MerchStore.Domain.UnitTests/UnitTest1.cs
๐ก Information
- Mirror Structure: Mirroring the source code structure in your test project makes it easier to find corresponding tests
- Focused Tests: This organization helps you focus tests on specific components (entities, value objects, etc.)
Step 3: Write Unit Tests for the Money Value Object
Introduction: Our Money value object is a critical part of the domain model, representing financial values with proper currency information. Testing this thoroughly is essential because financial calculations must be reliable and consistent. We’ll write tests that verify its construction, validation, and operations.
Create a new file MoneyTests.cs in the tests/MerchStore.Domain.UnitTests/ValueObjects directory:
tests/MerchStore.Domain.UnitTests/ValueObjects/MoneyTests.cs
using MerchStore.Domain.ValueObjects;
namespace MerchStore.Domain.UnitTests.ValueObjects;
public class MoneyTests
{
[Theory]
[InlineData(10.99, "USD")]
[InlineData(0, "EUR")]
[InlineData(999999.99, "SEK")]
public void Constructor_WithValidParameters_CreatesMoneyObject(decimal amount, string currency)
{
// Act
var money = new Money(amount, currency);
// Assert
Assert.Equal(amount, money.Amount);
Assert.Equal(currency.ToUpper(), money.Currency);
}
[Theory]
[InlineData(-1, "USD", "amount")]
[InlineData(-0.01, "EUR", "amount")]
public void Constructor_WithNegativeAmount_ThrowsArgumentException(decimal amount, string currency, string paramName)
{
// Act & Assert
var exception = Assert.Throws<ArgumentException>(() => new Money(amount, currency));
Assert.Equal(paramName, exception.ParamName);
}
[Theory]
[InlineData(10.0, "", "currency")]
[InlineData(10.0, null, "currency")]
[InlineData(10.0, "US", "currency")]
[InlineData(10.0, "USDD", "currency")]
public void Constructor_WithInvalidCurrency_ThrowsArgumentException(decimal amount, string? currency, string paramName)
{
// Act & Assert
var exception = Assert.Throws<ArgumentException>(() => new Money(amount, currency!));
Assert.Equal(paramName, exception.ParamName);
}
[Fact]
public void FromSEK_WithValidAmount_CreatesSEKMoney()
{
// Arrange
decimal amount = 15.75m;
// Act
var money = Money.FromSEK(amount);
// Assert
Assert.Equal(amount, money.Amount);
Assert.Equal("SEK", money.Currency);
}
[Fact]
public void AddOperator_WithSameCurrency_AddsMoney()
{
// Arrange
var money1 = new Money(10.5m, "USD");
var money2 = new Money(5.25m, "USD");
var expectedSum = 15.75m;
// Act
var result = money1 + money2;
// Assert
Assert.Equal(expectedSum, result.Amount);
Assert.Equal("USD", result.Currency);
}
[Fact]
public void AddOperator_WithDifferentCurrencies_ThrowsInvalidOperationException()
{
// Arrange
var money1 = new Money(10.5m, "USD");
var money2 = new Money(5.25m, "EUR");
// Act & Assert
Assert.Throws<InvalidOperationException>(() => money1 + money2);
}
[Fact]
public void ToString_ReturnsFormattedString()
{
// Arrange
var money = new Money(10.5m, "USD");
var expected = "10.50 USD";
// Act
var result = money.ToString();
// Assert
Assert.Equal(expected, result);
}
[Fact]
public void RecordEquality_WithEqualValues_ReturnsTrue()
{
// Arrange
var money1 = new Money(10.5m, "USD");
var money2 = new Money(10.5m, "USD");
// Act & Assert
Assert.Equal(money1, money2);
}
[Fact]
public void RecordEquality_WithDifferentValues_ReturnsFalse()
{
// Arrange
var money1 = new Money(10.5m, "USD");
var money2 = new Money(10.5m, "EUR");
// Act & Assert
Assert.NotEqual(money1, money2);
}
}
๐ก Information
- Fact vs Theory: Use
[Fact]for simple tests and[Theory]with[InlineData]for parameterized tests- Value Object Properties: Testing that Money has proper value semantics (equality based on properties)
- Exception Testing: Verify that attempts to create invalid money values fail properly
- Boundary Testing: Testing with zero amounts as an edge case
- Operation Testing: Ensuring operations like addition work correctly according to business rules
Step 4: Write Unit Tests for the Product Entity
Introduction: The Product entity contains core business logic for our merchandise store. Our tests will verify that products enforce important business rules like validation of names, descriptions, prices, and stock quantities. We’ll also test domain operations like updating product details and managing inventory.
Create a new file ProductTests.cs in the tests/MerchStore.Domain.UnitTests/Entities directory:
tests/MerchStore.Domain.UnitTests/Entities/ProductTests.cs
using MerchStore.Domain.Entities;
using MerchStore.Domain.ValueObjects;
namespace MerchStore.Domain.UnitTests.Entities;
public class ProductTests
{
// Helper method to create a valid product for testing
private Product CreateValidProduct()
{
return new Product(
"Test Product",
"Test Description",
new Uri("https://example.com/image.jpg"),
new Money(19.99m, "USD"),
10);
}
[Fact]
public void Constructor_WithValidParameters_CreatesProduct()
{
// Arrange
string name = "Test Product";
string description = "Test Description";
var imageUrl = new Uri("https://example.com/image.jpg");
var price = new Money(19.99m, "USD");
int stockQuantity = 10;
// Act
var product = new Product(name, description, imageUrl, price, stockQuantity);
// Assert
Assert.Equal(name, product.Name);
Assert.Equal(description, product.Description);
Assert.Equal(imageUrl, product.ImageUrl);
Assert.Equal(price, product.Price);
Assert.Equal(stockQuantity, product.StockQuantity);
Assert.NotEqual(Guid.Empty, product.Id);
}
[Theory]
[InlineData("", "Test Description", "name")]
[InlineData(null, "Test Description", "name")]
[InlineData("Test Product", "", "description")]
[InlineData("Test Product", null, "description")]
public void Constructor_WithInvalidNameOrDescription_ThrowsArgumentException(string? name, string? description, string paramName)
{
// Arrange
var imageUrl = new Uri("https://example.com/image.jpg");
var price = new Money(19.99m, "USD");
int stockQuantity = 10;
// Act & Assert
var exception = Assert.Throws<ArgumentException>(() =>
new Product(name!, description!, imageUrl, price, stockQuantity));
Assert.Equal(paramName, exception.ParamName);
}
[Theory]
[InlineData("A very long product name that exceeds the maximum allowed length of 100 characters which is meant to test validation logic", "Test Description", "name")]
[InlineData("Test Product", "A very long product description that exceeds the maximum allowed length. It goes on and on with unnecessary details and filler content just to make sure we hit the 500 character limit that we've set for our validation logic. It keeps going with more and more text that doesn't really add any value but just takes up space to ensure we exceed the limit. We're adding even more text here to make absolutely certain that this description is too long for our product entity. This should definitely trigger the validation logic that checks for description length and throw an appropriate exception with the correct parameter name to help developers identify and fix the issue quickly.", "description")]
public void Constructor_WithTooLongNameOrDescription_ThrowsArgumentException(string name, string description, string paramName)
{
// Arrange
var imageUrl = new Uri("https://example.com/image.jpg");
var price = new Money(19.99m, "USD");
int stockQuantity = 10;
// Act & Assert
var exception = Assert.Throws<ArgumentException>(() =>
new Product(name, description, imageUrl, price, stockQuantity));
Assert.Equal(paramName, exception.ParamName);
}
[Fact]
public void Constructor_WithNullPrice_ThrowsArgumentNullException()
{
// Arrange
string name = "Test Product";
string description = "Test Description";
var imageUrl = new Uri("https://example.com/image.jpg");
Money price = null!; // Simulating null price
int stockQuantity = 10;
// Act & Assert
Assert.Throws<ArgumentNullException>(() =>
new Product(name, description, imageUrl, price, stockQuantity));
}
[Fact]
public void Constructor_WithNegativeStockQuantity_ThrowsArgumentException()
{
// Arrange
string name = "Test Product";
string description = "Test Description";
var imageUrl = new Uri("https://example.com/image.jpg");
var price = new Money(19.99m, "USD");
int stockQuantity = -1;
// Act & Assert
var exception = Assert.Throws<ArgumentException>(() =>
new Product(name, description, imageUrl, price, stockQuantity));
Assert.Equal("stockQuantity", exception.ParamName);
}
[Fact]
public void Constructor_WithInvalidImageUrl_ThrowsArgumentException()
{
// Arrange
string name = "Test Product";
string description = "Test Description";
var imageUrl = new Uri("file:///C:/invalid/path.txt");
var price = new Money(19.99m, "USD");
int stockQuantity = 10;
// Act & Assert
var exception = Assert.Throws<ArgumentException>(() =>
new Product(name, description, imageUrl, price, stockQuantity));
Assert.Contains("URL must use HTTP or HTTPS", exception.Message);
}
[Fact]
public void UpdateDetails_WithValidParameters_UpdatesProduct()
{
// Arrange
var product = CreateValidProduct();
string newName = "Updated Product";
string newDescription = "Updated Description";
var newImageUrl = new Uri("https://example.com/new-image.jpg");
// Act
product.UpdateDetails(newName, newDescription, newImageUrl);
// Assert
Assert.Equal(newName, product.Name);
Assert.Equal(newDescription, product.Description);
Assert.Equal(newImageUrl, product.ImageUrl);
}
[Theory]
[InlineData("", "Updated Description", "name")]
[InlineData(null, "Updated Description", "name")]
[InlineData("Updated Product", "", "description")]
[InlineData("Updated Product", null, "description")]
public void UpdateDetails_WithInvalidParameters_ThrowsArgumentException(string? newName, string? newDescription, string paramName)
{
// Arrange
var product = CreateValidProduct();
var newImageUrl = new Uri("https://example.com/new-image.jpg");
// Act & Assert
var exception = Assert.Throws<ArgumentException>(() =>
product.UpdateDetails(newName!, newDescription!, newImageUrl));
Assert.Equal(paramName, exception.ParamName);
}
[Fact]
public void UpdatePrice_WithValidPrice_UpdatesPrice()
{
// Arrange
var product = CreateValidProduct();
var newPrice = new Money(29.99m, "USD");
// Act
product.UpdatePrice(newPrice);
// Assert
Assert.Equal(newPrice, product.Price);
}
[Fact]
public void UpdatePrice_WithNullPrice_ThrowsArgumentNullException()
{
// Arrange
var product = CreateValidProduct();
// Act & Assert
Assert.Throws<ArgumentNullException>(() => product.UpdatePrice(null!));
}
[Fact]
public void UpdateStock_WithValidQuantity_UpdatesStockQuantity()
{
// Arrange
var product = CreateValidProduct();
int newQuantity = 20;
// Act
product.UpdateStock(newQuantity);
// Assert
Assert.Equal(newQuantity, product.StockQuantity);
}
[Fact]
public void UpdateStock_WithNegativeQuantity_ThrowsArgumentException()
{
// Arrange
var product = CreateValidProduct();
int newQuantity = -1;
// Act & Assert
var exception = Assert.Throws<ArgumentException>(() =>
product.UpdateStock(newQuantity));
Assert.Equal("quantity", exception.ParamName);
}
[Fact]
public void DecrementStock_WithValidQuantity_DecreasesStock()
{
// Arrange
var product = CreateValidProduct();
int initialStock = product.StockQuantity;
int decrementAmount = 3;
// Act
bool result = product.DecrementStock(decrementAmount);
// Assert
Assert.True(result);
Assert.Equal(initialStock - decrementAmount, product.StockQuantity);
}
[Fact]
public void DecrementStock_WithDefaultQuantity_DecreasesByOne()
{
// Arrange
var product = CreateValidProduct();
int initialStock = product.StockQuantity;
// Act
bool result = product.DecrementStock();
// Assert
Assert.True(result);
Assert.Equal(initialStock - 1, product.StockQuantity);
}
[Fact]
public void DecrementStock_WithInsufficientStock_ReturnsFalse()
{
// Arrange
var product = CreateValidProduct();
int initialStock = product.StockQuantity;
int decrementAmount = initialStock + 1;
// Act
bool result = product.DecrementStock(decrementAmount);
// Assert
Assert.False(result);
Assert.Equal(initialStock, product.StockQuantity); // Stock should remain unchanged
}
[Fact]
public void DecrementStock_WithNegativeQuantity_ThrowsArgumentException()
{
// Arrange
var product = CreateValidProduct();
// Act & Assert
Assert.Throws<ArgumentException>(() => product.DecrementStock(-1));
}
[Fact]
public void IncrementStock_WithValidQuantity_IncreasesStock()
{
// Arrange
var product = CreateValidProduct();
int initialStock = product.StockQuantity;
int incrementAmount = 5;
// Act
product.IncrementStock(incrementAmount);
// Assert
Assert.Equal(initialStock + incrementAmount, product.StockQuantity);
}
[Fact]
public void IncrementStock_WithZeroQuantity_ThrowsArgumentException()
{
// Arrange
var product = CreateValidProduct();
// Act & Assert
Assert.Throws<ArgumentException>(() => product.IncrementStock(0));
}
[Fact]
public void IncrementStock_WithNegativeQuantity_ThrowsArgumentException()
{
// Arrange
var product = CreateValidProduct();
// Act & Assert
Assert.Throws<ArgumentException>(() => product.IncrementStock(-1));
}
}
๐ก Information
- Helper Method: Using a helper method reduces duplication when many tests need a valid product instance
- Entity Creation Tests: Verifying that entities are created with proper ID and property values
- Business Rule Validation: Testing that entity methods enforce business rules
- Edge Cases: Testing boundary conditions like insufficient stock or negative quantities
- Test Isolation: Each test runs independently without relying on state from other tests
Step 5: Write Unit Tests for the Entity Base Class
Introduction: The abstract Entity base class provides fundamental identity and equality behavior for all entities in our domain. Testing this properly requires creating a concrete test implementation. These tests ensure the base functionality works correctly before we rely on it in our more specific entity classes.
Create a new file EntityTests.cs in the tests/MerchStore.Domain.UnitTests/Common directory:
tests/MerchStore.Domain.UnitTests/Common/EntityTests.cs
using MerchStore.Domain.Common;
namespace MerchStore.Domain.UnitTests.Common;
// Helper class to test the abstract Entity<TId> base class
public class TestEntity : Entity<Guid>
{
public TestEntity(Guid id) : base(id) { }
protected TestEntity() { }
}
public class EntityTests
{
[Fact]
public void Constructor_WithValidId_SetsId()
{
// Arrange
var id = Guid.NewGuid();
// Act
var entity = new TestEntity(id);
// Assert
Assert.Equal(id, entity.Id);
}
[Fact]
public void Constructor_WithDefaultId_ThrowsArgumentException()
{
// Act & Assert
Assert.Throws<ArgumentException>(() => new TestEntity(Guid.Empty));
}
[Fact]
public void Equals_WithSameId_ReturnsTrue()
{
// Arrange
var id = Guid.NewGuid();
var entity1 = new TestEntity(id);
var entity2 = new TestEntity(id);
// Act & Assert
Assert.True(entity1.Equals(entity2));
}
[Fact]
public void Equals_WithDifferentId_ReturnsFalse()
{
// Arrange
var entity1 = new TestEntity(Guid.NewGuid());
var entity2 = new TestEntity(Guid.NewGuid());
// Act & Assert
Assert.False(entity1.Equals(entity2));
}
[Fact]
public void EqualsOperator_WithSameId_ReturnsTrue()
{
// Arrange
var id = Guid.NewGuid();
var entity1 = new TestEntity(id);
var entity2 = new TestEntity(id);
// Act & Assert
Assert.True(entity1 == entity2);
}
[Fact]
public void NotEqualsOperator_WithDifferentId_ReturnsTrue()
{
// Arrange
var entity1 = new TestEntity(Guid.NewGuid());
var entity2 = new TestEntity(Guid.NewGuid());
// Act & Assert
Assert.True(entity1 != entity2);
}
[Fact]
public void GetHashCode_WithSameId_ReturnsSameHashCode()
{
// Arrange
var id = Guid.NewGuid();
var entity1 = new TestEntity(id);
var entity2 = new TestEntity(id);
// Act & Assert
Assert.Equal(entity1.GetHashCode(), entity2.GetHashCode());
}
}
๐ก Information
- Testing Abstract Classes: To test an abstract class, we create a concrete test implementation
- Identity Testing: Testing that entities with the same ID are considered equal
- Operator Overloading: Verifying that == and != operators work correctly
- Hash Code Consistency: Ensuring that equal entities produce equal hash codes
Step 6: Run the Unit Tests
Introduction: After writing our tests, we need to run them to verify that our domain objects behave as expected. This step confirms that your business rules are correctly implemented and that your domain objects properly enforce validation and maintain their invariants.
Run the tests to verify that your domain entities and value objects behave as expected:
dotnet test tests/MerchStore.Domain.UnitTests
โ Expected Results
- All tests should pass, confirming that your domain objects correctly enforce business rules
- The console output should show no failed tests
โ ๏ธ Common Mistakes
- Not covering edge cases like empty strings, null values, or boundary conditions
- Testing only the happy path and ignoring validation scenarios
- Creating difficult-to-maintain tests with excessive setup code
- Duplicating test code instead of using helper methods or [Theory] with [InlineData]
๐งช Final Tests
Before concluding this exercise, make sure:
- All unit tests pass without errors
- You’ve covered:
- Entity creation and validation
- Value object behavior and immutability
- Business rules enforcement
- Edge cases and boundary conditions
๐ง Troubleshooting
If you encounter issues:
- Make sure your test project correctly references the Domain project
- Verify that your domain entities and value objects match the implementation being tested
- Check that you’re using the correct assertion methods for each scenario
- Look for incorrect parameter names in exception tests
๐ Optional Challenge
Want to take your learning further? Try:
- Implementing test data generators to create more varied test scenarios
- Adding tests for repositories using mock implementations
- Creating a code coverage report to identify untested code paths
- Implementing tests for more complex domain logic like business rules spanning multiple entities
๐ Further Reading
- xUnit Documentation - Official documentation for the xUnit testing framework
- Unit Testing Best Practices - Microsoft’s guidelines for effective unit testing
- Test-Driven Development (TDD) - An approach to development where tests are written before code
Done! ๐
Great job! You’ve successfully created a comprehensive test suite for your domain layer. These tests verify that your Money value object and Product entity correctly enforce business rules and handle both valid and invalid inputs appropriately. This foundation will help ensure that your domain model maintains its integrity as the application grows. ๐
Appendix A: The Importance of Unit Testing in DDD and Clean Architecture
Core Principles
Unit testing is not merely a technical practice but a fundamental pillar of both Domain-Driven Design (DDD) and Clean Architecture for several key reasons:
1. Domain Logic Verification
In DDD, the domain model encapsulates complex business rules and invariants. Unit tests serve as executable specifications that verify these rules are correctly implemented. When business analysts describe a rule like “Products cannot have negative stock quantities,” unit tests ensure this rule is enforced consistently throughout the application lifecycle.
2. Architectural Boundary Enforcement
Clean Architecture mandates that dependencies point inward, with the domain layer at the center having no external dependencies. Unit tests help enforce this principle by:
- Ensuring domain entities can be tested in isolation without infrastructure concerns
- Verifying that business rules aren’t accidentally leaked into outer layers
- Detecting when architecture boundaries are violated through test failures
3. Refactoring Safety Net
As systems evolve, domain models often need refinement. Comprehensive unit tests provide confidence when refactoring by:
- Quickly identifying when changes break existing behavior
- Allowing safe experimentation with alternative domain models
- Ensuring that optimizations preserve correctness
4. Living Documentation
Well-written unit tests serve as executable documentation that demonstrates:
- How domain objects are intended to be used
- What business rules are in effect
- What edge cases have been considered
- What behaviors should be preserved during future changes
Practical Benefits in DDD/Clean Architecture Projects
1. Faster Development Cycle
Contrary to the misconception that unit testing slows development, it actually accelerates it in DDD contexts:
- Immediate feedback on domain logic correctness
- Reduced debugging time for complex business rules
- Faster identification of regression issues
- More focused, incremental development
2. Improved Domain Model Design
The process of writing unit tests often reveals design flaws:
- Entities with too many responsibilities become difficult to test, highlighting potential Single Responsibility Principle violations
- Complex test setup might indicate excessive coupling between domain concepts
- Difficult-to-test code often points to domain modeling issues
3. Early Detection of Business Rule Violations
Unit tests catch business rule violations at development time rather than in production:
- Validation logic gaps are exposed through focused tests
- Boundary conditions and edge cases are systematically verified
- Inconsistent rule application across the domain becomes apparent
4. Facilitates Onboarding New Team Members
Clean, well-tested domain models help new team members understand the business domain:
- Tests demonstrate expected behaviors and constraints
- Test failures provide immediate feedback when business rules are violated
- Unit tests serve as executable examples of correct domain object usage
Best Practices for Domain Layer Unit Testing
1. Focus on Business Rules
Effective domain tests focus on business rules rather than technical details:
- Test method names should describe business scenarios, not implementation details
- Test cases should validate business outcomes, not internal implementation
- Edge cases should reflect real business scenarios
2. Test Domain Objects in Isolation
True unit tests verify domain objects in isolation:
- Use test doubles (mocks/stubs) for dependencies when needed
- Focus on a single domain concept per test class
- Test aggregate roots as complete units
3. Arrange-Act-Assert Pattern
Structure tests using the Arrange-Act-Assert pattern for clarity:
- Arrange: Set up the domain objects in an appropriate state
- Act: Execute the method or operation being tested
- Assert: Verify that the resulting state or behavior matches expectations
4. Test Both Happy Paths and Edge Cases
Comprehensive tests include:
- Happy path scenarios that verify normal operation
- Validation scenarios that confirm business rules are enforced
- Edge cases that test boundary conditions
- Error scenarios that verify exceptions are thrown appropriately
Conclusion
Unit testing is not an optional add-on but an essential practice in DDD and Clean Architecture. It serves multiple purposes: verifying business rules, enforcing architectural boundaries, enabling safe refactoring, and providing living documentation. By investing in thorough domain layer unit tests, development teams build a foundation for maintainable, correct, and evolvable systems that accurately reflect business needs.
Great job! You’ve successfully created a comprehensive test suite for your domain layer. These tests verify that your Money value object and Product entity correctly enforce business rules and handle both valid and invalid inputs appropriately. This foundation will help ensure that your domain model maintains its integrity as the application grows. ๐