Testing Documentation
Comprehensive testing strategy with 47+ existing tests
Testing Strategy Overview
The Shirinzad E-Commerce Platform employs a comprehensive, multi-layered testing strategy to ensure code quality, reliability, and maintainability across all modules.
Testing Pyramid
5% - UI & Integration] --> B[Integration Tests
20% - API & Database] B --> C[Unit Tests
75% - Business Logic] style A fill:#dc2626 style B fill:#d97706 style C fill:#059669
Key Principles
- Unit tests for business logic and domain entities
- Integration tests for application services with in-memory database
- Repository tests for data access layer
- API endpoint tests for HTTP layer
- Automated CI/CD test execution
- Test-driven development (TDD) for critical features
Test Frameworks & Tools
xUnit
Purpose: Test runner and framework
Version: 2.4.2+
- Industry-standard .NET test framework
- Parallel test execution
- Extensible architecture
- Excellent Visual Studio integration
Shouldly
Purpose: Assertion library
Version: 4.0+
- Readable assertion syntax
- Better error messages
- Fluent API
- Natural language assertions
NSubstitute
Purpose: Mocking framework
Version: 5.0+
- Simple, elegant mocking syntax
- Interface and class mocking
- Argument matching
- Return value configuration
Test Projects Structure
Project Organization
1. Shirinzad.Shop.TestBase
Purpose: Shared test infrastructure and utilities
Key Components:
| Component | Description |
|---|---|
ShopTestBase<TModule> |
Base class for all tests with ABP integration |
ShopTestDataSeedContributor |
Seed test data for integration tests |
ShopTestConsts |
Test constants and configuration |
FakeCurrentPrincipalAccessor |
Mock authentication for security tests |
2. Shirinzad.Shop.Domain.Tests
Purpose: Unit tests for domain entities and business logic
Test Count: 8+ tests
Test Coverage:
- Product entity validation and business rules
- Category hierarchy and relationships
- Order state transitions
- Domain events and event handlers
- Value objects validation
Example Tests:
// test/Shirinzad.Shop.Domain.Tests/Catalog/ProductTests.cs
[Fact]
public void Should_Create_Product_With_Valid_Data()
{
// Arrange & Act
var product = new Product(
Guid.NewGuid(),
"Test Product",
"test-product"
);
// Assert
product.Name.ShouldBe("Test Product");
product.Slug.ShouldBe("test-product");
product.IsActive.ShouldBeTrue();
}
[Fact]
public void Should_Calculate_Discount_Percentage_Correctly()
{
// Arrange
var product = new Product(Guid.NewGuid(), "Product", "product");
product.UpdateBasicInfo(
name: "Product",
slug: "product",
price: 100000m,
discountPrice: 75000m
);
// Act
var discountPercentage = product.GetDiscountPercentage();
// Assert
discountPercentage.ShouldBe(25);
}
3. Shirinzad.Shop.Application.Tests
Purpose: Integration tests for application services
Test Count: 35+ tests
Test Coverage by Module:
| Module | Test File | Tests |
|---|---|---|
| Products | ProductAppService_Tests.cs | 14 tests |
| Categories | CategoryAppService_Tests.cs | 6 tests |
| Brands | BrandAppService_Tests.cs | 4 tests |
| Tags | TagAppService_Tests.cs | 3 tests |
| Orders | OrderAppService_Tests.cs | 5 tests |
| Payments | PaymentAppService_Tests.cs | 3 tests |
| Dashboard | DashboardAppService_Tests.cs | 2 tests |
Comprehensive Product Test Example:
public class ProductAppService_Tests : ShopApplicationTestBase<ShopApplicationTestModule>
{
private readonly IProductAppService _productAppService;
private readonly IRepository<Product, Guid> _productRepository;
public ProductAppService_Tests()
{
_productAppService = GetRequiredService<IProductAppService>();
_productRepository = GetRequiredService<IRepository<Product, Guid>>();
}
[Fact]
public async Task Should_Create_Product_With_Valid_Data()
{
// Arrange
var brand = await CreateBrandAsync("Test Brand");
var category = await CreateCategoryAsync("Test Category");
var createDto = new CreateProductDto
{
Name = "Test Product",
Slug = "test-product",
Price = 100000m,
DiscountPrice = 80000m,
Stock = 50,
BrandId = brand.Id,
CategoryIds = new List<Guid> { category.Id },
IsFeatured = true
};
// Act
var result = await _productAppService.CreateAsync(createDto);
// Assert
result.ShouldNotBeNull();
result.Id.ShouldNotBe(Guid.Empty);
result.Name.ShouldBe("Test Product");
result.Price.ShouldBe(100000m);
result.Stock.ShouldBe(50);
// Verify in database
var productInDb = await _productRepository.GetAsync(result.Id);
productInDb.Name.ShouldBe("Test Product");
}
[Fact]
public async Task Should_Update_Product()
{
// Arrange
var product = await CreateProductAsync("Original Product");
var updateDto = new UpdateProductDto
{
Name = "Updated Product",
Price = 200000m
};
// Act
var result = await _productAppService.UpdateAsync(product.Id, updateDto);
// Assert
result.Name.ShouldBe("Updated Product");
result.Price.ShouldBe(200000m);
}
[Fact]
public async Task Should_Get_Product_List_With_Pagination()
{
// Arrange - Create 15 products
for (int i = 1; i <= 15; i++)
{
await CreateProductAsync($"Product {i}");
}
// Act
var result = await _productAppService.GetListAsync(
new PagedAndSortedResultRequestDto
{
SkipCount = 0,
MaxResultCount = 10
}
);
// Assert
result.TotalCount.ShouldBe(15);
result.Items.Count.ShouldBe(10);
}
}
4. Shirinzad.Shop.EntityFrameworkCore.Tests
Purpose: Repository and database tests with in-memory database
Test Count: 3+ tests
Features:
- In-memory SQLite database for fast testing
- Repository pattern validation
- Query performance testing
- Database migration validation
Example Repository Test:
public class SampleRepositoryTests : ShopEntityFrameworkCoreTestBase
{
private readonly IRepository<Product, Guid> _productRepository;
public SampleRepositoryTests()
{
_productRepository = GetRequiredService<IRepository<Product, Guid>>();
}
[Fact]
public async Task Should_Query_Products_Efficiently()
{
// Arrange
await WithUnitOfWorkAsync(async () =>
{
await _productRepository.InsertAsync(
new Product(Guid.NewGuid(), "Product 1", "product-1")
);
});
// Act
var products = await WithUnitOfWorkAsync(async () =>
{
return await _productRepository.GetListAsync();
});
// Assert
products.Count.ShouldBeGreaterThan(0);
}
}
5. Shirinzad.Shop.Web.Tests
Purpose: Web layer and controller tests
Test Count: 2+ tests
Features:
- API endpoint integration tests
- HTTP request/response validation
- Authorization testing
- Controller action tests
Example Controller Test:
public class ConfigController_Tests : ShopWebTestBase
{
[Fact]
public async Task Should_Get_Config_Settings()
{
// Act
var result = await GetResponseAsObjectAsync<ConfigDto>(
"/api/config/shirinzad"
);
// Assert
result.ShouldNotBeNull();
result.SiteName.ShouldNotBeNullOrEmpty();
}
}
Test Coverage Analysis
Current Coverage: 40-50%
| Module | Existing Tests | Coverage | Status |
|---|---|---|---|
| Catalog (Products, Categories, Brands, Tags) | 27 tests | ~70% | Good |
| Orders & Payments | 8 tests | ~50% | Medium |
| Configuration | 2 tests | ~60% | Medium |
| Dashboard | 2 tests | ~40% | Medium |
| Carts | 0 tests | 0% | Missing |
| Discounts | 0 tests | 0% | Missing |
| Shipments | 0 tests | 0% | Missing |
| Reviews | 0 tests | 0% | Missing |
| Wishlist | 0 tests | 0% | Missing |
| Notifications | 0 tests | 0% | Missing |
| Blog & CMS | 0 tests | 0% | Missing |
| Automotive | 0 tests | 0% | Missing |
Testing Gaps & Recommendations
Priority 1: Critical Gaps
- Cart Management: 15-20 tests needed
- Add/remove items
- Update quantities
- Price calculations
- Cart expiration
- Discount Engine: 12-15 tests needed
- Percentage discounts
- Fixed amount discounts
- Coupon codes
- Discount stacking rules
- Payment Processing: 10-12 tests needed
- Payment gateway integration
- Transaction validation
- Refund processing
- Failed payment handling
Priority 2: Important Gaps
- Shipment Tracking: 8-10 tests needed
- Shipment creation
- Status updates
- Carrier integration
- Delivery confirmation
- Review System: 10-12 tests needed
- Review submission
- Rating calculations
- Review moderation
- Helpful votes
- Wishlist: 8-10 tests needed
- Add/remove items
- Multiple wishlists
- Privacy settings
- Move to cart
Priority 3: Enhancement Gaps
- Notifications: 12-15 tests needed
- Email notifications
- SMS notifications
- In-app notifications
- Notification preferences
- Blog & CMS: 15-18 tests needed
- Post CRUD operations
- Category management
- Page management
- SEO metadata
- Automotive Module: 20-25 tests needed
- Vehicle management
- Service scheduling
- Compatibility checking
- VIN decoding
Priority 4: Integration Tests
- End-to-End Scenarios: 15-20 tests
- Complete checkout flow
- Order fulfillment cycle
- User registration to purchase
- Returns and refunds
- Performance Tests: 10-12 tests
- Load testing critical endpoints
- Database query optimization
- Cache effectiveness
- Concurrent user scenarios
- Security Tests: 8-10 tests
- Authentication flows
- Authorization policies
- Rate limiting
- Input validation
Running Tests
Run All Tests
# Run all tests in solution
dotnet test
# Run with detailed output
dotnet test --verbosity detailed
# Run with code coverage
dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura
Run Specific Test Project
# Run domain tests only
dotnet test test/Shirinzad.Shop.Domain.Tests
# Run application tests only
dotnet test test/Shirinzad.Shop.Application.Tests
# Run specific test class
dotnet test --filter "FullyQualifiedName~ProductAppService_Tests"
# Run specific test method
dotnet test --filter "FullyQualifiedName~Should_Create_Product_With_Valid_Data"
Visual Studio Test Explorer
- Open Test Explorer:
Test > Test Explorer - Build solution to discover tests
- Click "Run All" or select specific tests
- View results in Test Explorer window
- Debug failing tests by right-clicking and selecting "Debug"
CI/CD Integration
# GitHub Actions example
name: Run Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: '9.0.x'
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Test
run: dotnet test --no-build --verbosity normal --collect:"XPlat Code Coverage"
- name: Upload coverage reports
uses: codecov/codecov-action@v3
Test Data Seeding
ShopTestDataSeedContributor
The test base project provides a data seeding mechanism for integration tests:
public class ShopTestDataSeedContributor : IDataSeedContributor, ITransientDependency
{
public Task SeedAsync(DataSeedContext context)
{
// Seed additional test data
// This runs before each test execution
return Task.CompletedTask;
}
}
Custom Test Data Creation
// Helper methods in test classes
private async Task<Product> CreateProductAsync(
string name,
string slug,
decimal price = 100000m,
int stock = 10)
{
var product = new Product(Guid.NewGuid(), name, slug);
product.UpdateBasicInfo(
name: name,
slug: slug,
price: price,
stockQuantity: stock
);
return await _productRepository.InsertAsync(product, autoSave: true);
}
private async Task<Category> CreateCategoryAsync(string name)
{
var category = new Category(
Guid.NewGuid(),
name,
name.ToLower().Replace(" ", "-")
);
return await _categoryRepository.InsertAsync(category, autoSave: true);
}
Testing Best Practices
Unit Testing Guidelines
- Follow AAA pattern: Arrange, Act, Assert
- One assertion per test (when possible)
- Use descriptive test names (Should_Do_Something_When_Condition)
- Keep tests isolated and independent
- Mock external dependencies
- Test edge cases and error conditions
- Avoid test interdependencies
Integration Testing Guidelines
- Use in-memory database for speed
- Clean up data after each test
- Test realistic scenarios
- Verify database state changes
- Test transaction rollback
- Include pagination and filtering tests
- Test concurrent operations
Test Naming Conventions
// Good test names
Should_Create_Product_With_Valid_Data()
Should_Throw_Exception_When_Price_Is_Negative()
Should_Return_Empty_List_When_No_Products_Exist()
Should_Update_Stock_Quantity_After_Order()
// Bad test names
Test1()
ProductTest()
CreateProduct()
TestProductCreation()
Assertion Best Practices
// Using Shouldly for readable assertions
// Good
result.ShouldNotBeNull();
result.Name.ShouldBe("Expected Name");
result.Price.ShouldBe(100000m);
result.Items.Count.ShouldBe(5);
// Also good - testing multiple properties
result.ShouldSatisfyAllConditions(
() => result.Name.ShouldBe("Expected"),
() => result.Price.ShouldBeGreaterThan(0),
() => result.IsActive.ShouldBeTrue()
);
Mocking with NSubstitute
Basic Mocking
// Mock repository
var mockRepository = Substitute.For<IRepository<Product, Guid>>();
// Configure return value
mockRepository.GetAsync(Arg.Any<Guid>())
.Returns(new Product(Guid.NewGuid(), "Test Product", "test-product"));
// Verify method was called
await mockRepository.Received(1).GetAsync(Arg.Any<Guid>());
Advanced Mocking Scenarios
// Mock with specific argument matching
mockRepository.GetListAsync(
Arg.Is<Expression<Func<Product, bool>>>(x => x != null)
).Returns(new List<Product> { product1, product2 });
// Mock to throw exception
mockRepository.InsertAsync(Arg.Any<Product>())
.Throws<InvalidOperationException>();
// Mock with callback
mockRepository.When(x => x.DeleteAsync(Arg.Any<Guid>()))
.Do(x => Console.WriteLine("Delete called"));
Testing Roadmap
Phase 1: Foundation (Weeks 1-2)
- Complete Cart module tests (15-20 tests)
- Complete Discount module tests (12-15 tests)
- Add Payment integration tests (10-12 tests)
Phase 2: Core Features (Weeks 3-4)
- Complete Shipment module tests (8-10 tests)
- Complete Review system tests (10-12 tests)
- Complete Wishlist module tests (8-10 tests)
Phase 3: Advanced Features (Weeks 5-6)
- Complete Notification tests (12-15 tests)
- Complete Blog & CMS tests (15-18 tests)
- Complete Automotive module tests (20-25 tests)
Phase 4: Quality Assurance (Weeks 7-8)
- Add end-to-end scenario tests (15-20 tests)
- Add performance tests (10-12 tests)
- Add security tests (8-10 tests)
- Achieve 90%+ code coverage